Lexical: Extracted & merged heading & quote nodes
This commit is contained in:
		
							parent
							
								
									f3fa63a5ae
								
							
						
					
					
						commit
						36a4d79120
					
				| 
						 | 
				
			
			@ -8,11 +8,12 @@
 | 
			
		|||
 | 
			
		||||
import {$createLinkNode} from '@lexical/link';
 | 
			
		||||
import {$createListItemNode, $createListNode} from '@lexical/list';
 | 
			
		||||
import {$createHeadingNode, $createQuoteNode} from '@lexical/rich-text';
 | 
			
		||||
import {$createTableNodeWithDimensions} from '@lexical/table';
 | 
			
		||||
import {$createParagraphNode, $createTextNode, $getRoot} from 'lexical';
 | 
			
		||||
 | 
			
		||||
import {initializeUnitTest} from '../utils';
 | 
			
		||||
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {$createQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
function $createEditorContent() {
 | 
			
		||||
  const root = $getRoot();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,7 +10,6 @@ import {createHeadlessEditor} from '@lexical/headless';
 | 
			
		|||
import {AutoLinkNode, LinkNode} from '@lexical/link';
 | 
			
		||||
import {ListItemNode, ListNode} from '@lexical/list';
 | 
			
		||||
 | 
			
		||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
 | 
			
		||||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +35,8 @@ import {
 | 
			
		|||
  LexicalNodeReplacement,
 | 
			
		||||
} from '../../LexicalEditor';
 | 
			
		||||
import {resetRandomKey} from '../../LexicalUtils';
 | 
			
		||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
type TestEnv = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,7 +48,7 @@ export class CommonBlockNode extends ElementNode {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export function copyCommonBlockProperties(from: CommonBlockNode, to: CommonBlockNode): void {
 | 
			
		||||
    to.__id = from.__id;
 | 
			
		||||
    // to.__id = from.__id;
 | 
			
		||||
    to.__alignment = from.__alignment;
 | 
			
		||||
    to.__inset = from.__inset;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ import {
 | 
			
		|||
  $insertDataTransferForRichText,
 | 
			
		||||
} from '@lexical/clipboard';
 | 
			
		||||
import {$createListItemNode, $createListNode} from '@lexical/list';
 | 
			
		||||
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  $createParagraphNode,
 | 
			
		||||
  $createRangeSelection,
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +32,7 @@ import {
 | 
			
		|||
  initializeUnitTest,
 | 
			
		||||
  invariant,
 | 
			
		||||
} from '../../../__tests__/utils';
 | 
			
		||||
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
describe('LexicalTabNode tests', () => {
 | 
			
		||||
  initializeUnitTest((testEnv) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,13 +13,14 @@ import {createHeadlessEditor} from '@lexical/headless';
 | 
			
		|||
import {$generateHtmlFromNodes, $generateNodesFromDOM} from '@lexical/html';
 | 
			
		||||
import {LinkNode} from '@lexical/link';
 | 
			
		||||
import {ListItemNode, ListNode} from '@lexical/list';
 | 
			
		||||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  $createParagraphNode,
 | 
			
		||||
  $createRangeSelection,
 | 
			
		||||
  $createTextNode,
 | 
			
		||||
  $getRoot,
 | 
			
		||||
} from 'lexical';
 | 
			
		||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
describe('HTML', () => {
 | 
			
		||||
  type Input = Array<{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,202 @@
 | 
			
		|||
import {
 | 
			
		||||
    $applyNodeReplacement,
 | 
			
		||||
    $createParagraphNode,
 | 
			
		||||
    type DOMConversionMap,
 | 
			
		||||
    DOMConversionOutput,
 | 
			
		||||
    type DOMExportOutput,
 | 
			
		||||
    type EditorConfig,
 | 
			
		||||
    isHTMLElement,
 | 
			
		||||
    type LexicalEditor,
 | 
			
		||||
    type LexicalNode,
 | 
			
		||||
    type NodeKey,
 | 
			
		||||
    type ParagraphNode,
 | 
			
		||||
    type RangeSelection,
 | 
			
		||||
    type SerializedElementNode,
 | 
			
		||||
    type Spread
 | 
			
		||||
} from "lexical";
 | 
			
		||||
import {addClassNamesToElement} from "@lexical/utils";
 | 
			
		||||
import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
 | 
			
		||||
import {
 | 
			
		||||
    commonPropertiesDifferent, deserializeCommonBlockNode,
 | 
			
		||||
    SerializedCommonBlockNode, setCommonBlockPropsFromElement,
 | 
			
		||||
    updateElementWithCommonBlockProps
 | 
			
		||||
} from "../../nodes/_common";
 | 
			
		||||
 | 
			
		||||
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
			
		||||
 | 
			
		||||
export type SerializedHeadingNode = Spread<
 | 
			
		||||
    {
 | 
			
		||||
        tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
			
		||||
    },
 | 
			
		||||
    SerializedCommonBlockNode
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
/** @noInheritDoc */
 | 
			
		||||
export class HeadingNode extends CommonBlockNode {
 | 
			
		||||
    /** @internal */
 | 
			
		||||
    __tag: HeadingTagType;
 | 
			
		||||
 | 
			
		||||
    static getType(): string {
 | 
			
		||||
        return 'heading';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static clone(node: HeadingNode): HeadingNode {
 | 
			
		||||
        const clone = new HeadingNode(node.__tag, node.__key);
 | 
			
		||||
        copyCommonBlockProperties(node, clone);
 | 
			
		||||
        return clone;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(tag: HeadingTagType, key?: NodeKey) {
 | 
			
		||||
        super(key);
 | 
			
		||||
        this.__tag = tag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getTag(): HeadingTagType {
 | 
			
		||||
        return this.__tag;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // View
 | 
			
		||||
 | 
			
		||||
    createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
        const tag = this.__tag;
 | 
			
		||||
        const element = document.createElement(tag);
 | 
			
		||||
        const theme = config.theme;
 | 
			
		||||
        const classNames = theme.heading;
 | 
			
		||||
        if (classNames !== undefined) {
 | 
			
		||||
            const className = classNames[tag];
 | 
			
		||||
            addClassNamesToElement(element, className);
 | 
			
		||||
        }
 | 
			
		||||
        updateElementWithCommonBlockProps(element, this);
 | 
			
		||||
        return element;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
 | 
			
		||||
        return commonPropertiesDifferent(prevNode, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importDOM(): DOMConversionMap | null {
 | 
			
		||||
        return {
 | 
			
		||||
            h1: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h2: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h3: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h4: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h5: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h6: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
			
		||||
        const {element} = super.exportDOM(editor);
 | 
			
		||||
 | 
			
		||||
        if (element && isHTMLElement(element)) {
 | 
			
		||||
            if (this.isEmpty()) {
 | 
			
		||||
                element.append(document.createElement('br'));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            element,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
 | 
			
		||||
        const node = $createHeadingNode(serializedNode.tag);
 | 
			
		||||
        deserializeCommonBlockNode(serializedNode, node);
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportJSON(): SerializedHeadingNode {
 | 
			
		||||
        return {
 | 
			
		||||
            ...super.exportJSON(),
 | 
			
		||||
            tag: this.getTag(),
 | 
			
		||||
            type: 'heading',
 | 
			
		||||
            version: 1,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Mutation
 | 
			
		||||
    insertNewAfter(
 | 
			
		||||
        selection?: RangeSelection,
 | 
			
		||||
        restoreSelection = true,
 | 
			
		||||
    ): ParagraphNode | HeadingNode {
 | 
			
		||||
        const anchorOffet = selection ? selection.anchor.offset : 0;
 | 
			
		||||
        const lastDesc = this.getLastDescendant();
 | 
			
		||||
        const isAtEnd =
 | 
			
		||||
            !lastDesc ||
 | 
			
		||||
            (selection &&
 | 
			
		||||
                selection.anchor.key === lastDesc.getKey() &&
 | 
			
		||||
                anchorOffet === lastDesc.getTextContentSize());
 | 
			
		||||
        const newElement =
 | 
			
		||||
            isAtEnd || !selection
 | 
			
		||||
                ? $createParagraphNode()
 | 
			
		||||
                : $createHeadingNode(this.getTag());
 | 
			
		||||
        const direction = this.getDirection();
 | 
			
		||||
        newElement.setDirection(direction);
 | 
			
		||||
        this.insertAfter(newElement, restoreSelection);
 | 
			
		||||
        if (anchorOffet === 0 && !this.isEmpty() && selection) {
 | 
			
		||||
            const paragraph = $createParagraphNode();
 | 
			
		||||
            paragraph.select();
 | 
			
		||||
            this.replace(paragraph, true);
 | 
			
		||||
        }
 | 
			
		||||
        return newElement;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    collapseAtStart(): true {
 | 
			
		||||
        const newElement = !this.isEmpty()
 | 
			
		||||
            ? $createHeadingNode(this.getTag())
 | 
			
		||||
            : $createParagraphNode();
 | 
			
		||||
        const children = this.getChildren();
 | 
			
		||||
        children.forEach((child) => newElement.append(child));
 | 
			
		||||
        this.replace(newElement);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    extractWithChild(): boolean {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
    const nodeName = element.nodeName.toLowerCase();
 | 
			
		||||
    let node = null;
 | 
			
		||||
    if (
 | 
			
		||||
        nodeName === 'h1' ||
 | 
			
		||||
        nodeName === 'h2' ||
 | 
			
		||||
        nodeName === 'h3' ||
 | 
			
		||||
        nodeName === 'h4' ||
 | 
			
		||||
        nodeName === 'h5' ||
 | 
			
		||||
        nodeName === 'h6'
 | 
			
		||||
    ) {
 | 
			
		||||
        node = $createHeadingNode(nodeName);
 | 
			
		||||
        setCommonBlockPropsFromElement(element, node);
 | 
			
		||||
    }
 | 
			
		||||
    return {node};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
 | 
			
		||||
    return $applyNodeReplacement(new HeadingNode(headingTag));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isHeadingNode(
 | 
			
		||||
    node: LexicalNode | null | undefined,
 | 
			
		||||
): node is HeadingNode {
 | 
			
		||||
    return node instanceof HeadingNode;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,129 @@
 | 
			
		|||
import {
 | 
			
		||||
    $applyNodeReplacement,
 | 
			
		||||
    $createParagraphNode,
 | 
			
		||||
    type DOMConversionMap,
 | 
			
		||||
    type DOMConversionOutput,
 | 
			
		||||
    type DOMExportOutput,
 | 
			
		||||
    type EditorConfig,
 | 
			
		||||
    ElementNode,
 | 
			
		||||
    isHTMLElement,
 | 
			
		||||
    type LexicalEditor,
 | 
			
		||||
    LexicalNode,
 | 
			
		||||
    type NodeKey,
 | 
			
		||||
    type ParagraphNode,
 | 
			
		||||
    type RangeSelection,
 | 
			
		||||
    SerializedElementNode
 | 
			
		||||
} from "lexical";
 | 
			
		||||
import {addClassNamesToElement} from "@lexical/utils";
 | 
			
		||||
import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
 | 
			
		||||
import {
 | 
			
		||||
    commonPropertiesDifferent, deserializeCommonBlockNode,
 | 
			
		||||
    SerializedCommonBlockNode, setCommonBlockPropsFromElement,
 | 
			
		||||
    updateElementWithCommonBlockProps
 | 
			
		||||
} from "../../nodes/_common";
 | 
			
		||||
 | 
			
		||||
export type SerializedQuoteNode = SerializedCommonBlockNode;
 | 
			
		||||
 | 
			
		||||
/** @noInheritDoc */
 | 
			
		||||
export class QuoteNode extends CommonBlockNode {
 | 
			
		||||
    static getType(): string {
 | 
			
		||||
        return 'quote';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static clone(node: QuoteNode): QuoteNode {
 | 
			
		||||
        const clone = new QuoteNode(node.__key);
 | 
			
		||||
        copyCommonBlockProperties(node, clone);
 | 
			
		||||
        return clone;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    constructor(key?: NodeKey) {
 | 
			
		||||
        super(key);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // View
 | 
			
		||||
 | 
			
		||||
    createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
        const element = document.createElement('blockquote');
 | 
			
		||||
        addClassNamesToElement(element, config.theme.quote);
 | 
			
		||||
        updateElementWithCommonBlockProps(element, this);
 | 
			
		||||
        return element;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
 | 
			
		||||
        return commonPropertiesDifferent(prevNode, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importDOM(): DOMConversionMap | null {
 | 
			
		||||
        return {
 | 
			
		||||
            blockquote: (node: Node) => ({
 | 
			
		||||
                conversion: $convertBlockquoteElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
			
		||||
        const {element} = super.exportDOM(editor);
 | 
			
		||||
 | 
			
		||||
        if (element && isHTMLElement(element)) {
 | 
			
		||||
            if (this.isEmpty()) {
 | 
			
		||||
                element.append(document.createElement('br'));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            element,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
 | 
			
		||||
        const node = $createQuoteNode();
 | 
			
		||||
        deserializeCommonBlockNode(serializedNode, node);
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportJSON(): SerializedQuoteNode {
 | 
			
		||||
        return {
 | 
			
		||||
            ...super.exportJSON(),
 | 
			
		||||
            type: 'quote',
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Mutation
 | 
			
		||||
 | 
			
		||||
    insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
 | 
			
		||||
        const newBlock = $createParagraphNode();
 | 
			
		||||
        const direction = this.getDirection();
 | 
			
		||||
        newBlock.setDirection(direction);
 | 
			
		||||
        this.insertAfter(newBlock, restoreSelection);
 | 
			
		||||
        return newBlock;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    collapseAtStart(): true {
 | 
			
		||||
        const paragraph = $createParagraphNode();
 | 
			
		||||
        const children = this.getChildren();
 | 
			
		||||
        children.forEach((child) => paragraph.append(child));
 | 
			
		||||
        this.replace(paragraph);
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    canMergeWhenEmpty(): true {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createQuoteNode(): QuoteNode {
 | 
			
		||||
    return $applyNodeReplacement(new QuoteNode());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isQuoteNode(
 | 
			
		||||
    node: LexicalNode | null | undefined,
 | 
			
		||||
): node is QuoteNode {
 | 
			
		||||
    return node instanceof QuoteNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
    const node = $createQuoteNode();
 | 
			
		||||
    setCommonBlockPropsFromElement(element, node);
 | 
			
		||||
    return {node};
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -6,11 +6,6 @@
 | 
			
		|||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  $createHeadingNode,
 | 
			
		||||
  $isHeadingNode,
 | 
			
		||||
  HeadingNode,
 | 
			
		||||
} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  $createTextNode,
 | 
			
		||||
  $getRoot,
 | 
			
		||||
| 
						 | 
				
			
			@ -19,6 +14,7 @@ import {
 | 
			
		|||
  RangeSelection,
 | 
			
		||||
} from 'lexical';
 | 
			
		||||
import {initializeUnitTest} from 'lexical/__tests__/utils';
 | 
			
		||||
import {$createHeadingNode, $isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
const editorConfig = Object.freeze({
 | 
			
		||||
  namespace: '',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,9 +6,9 @@
 | 
			
		|||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {$createQuoteNode, QuoteNode} from '@lexical/rich-text';
 | 
			
		||||
import {$createRangeSelection, $getRoot, ParagraphNode} from 'lexical';
 | 
			
		||||
import {initializeUnitTest} from 'lexical/__tests__/utils';
 | 
			
		||||
import {$createQuoteNode, QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
const editorConfig = Object.freeze({
 | 
			
		||||
  namespace: '',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,42 +8,14 @@
 | 
			
		|||
 | 
			
		||||
import type {
 | 
			
		||||
  CommandPayloadType,
 | 
			
		||||
  DOMConversionMap,
 | 
			
		||||
  DOMConversionOutput,
 | 
			
		||||
  DOMExportOutput,
 | 
			
		||||
  EditorConfig,
 | 
			
		||||
  ElementFormatType,
 | 
			
		||||
  LexicalCommand,
 | 
			
		||||
  LexicalEditor,
 | 
			
		||||
  LexicalNode,
 | 
			
		||||
  NodeKey,
 | 
			
		||||
  ParagraphNode,
 | 
			
		||||
  PasteCommandType,
 | 
			
		||||
  RangeSelection,
 | 
			
		||||
  SerializedElementNode,
 | 
			
		||||
  Spread,
 | 
			
		||||
  TextFormatType,
 | 
			
		||||
} from 'lexical';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  $insertDataTransferForRichText,
 | 
			
		||||
  copyToClipboard,
 | 
			
		||||
} from '@lexical/clipboard';
 | 
			
		||||
import {
 | 
			
		||||
  $moveCharacter,
 | 
			
		||||
  $shouldOverrideDefaultCharacterSelection,
 | 
			
		||||
} from '@lexical/selection';
 | 
			
		||||
import {
 | 
			
		||||
  $findMatchingParent,
 | 
			
		||||
  $getNearestBlockElementAncestorOrThrow,
 | 
			
		||||
  addClassNamesToElement,
 | 
			
		||||
  isHTMLElement,
 | 
			
		||||
  mergeRegister,
 | 
			
		||||
  objectKlassEquals,
 | 
			
		||||
} from '@lexical/utils';
 | 
			
		||||
import {
 | 
			
		||||
  $applyNodeReplacement,
 | 
			
		||||
  $createParagraphNode,
 | 
			
		||||
  $createRangeSelection,
 | 
			
		||||
  $createTabNode,
 | 
			
		||||
  $getAdjacentNode,
 | 
			
		||||
| 
						 | 
				
			
			@ -55,7 +27,6 @@ import {
 | 
			
		|||
  $isElementNode,
 | 
			
		||||
  $isNodeSelection,
 | 
			
		||||
  $isRangeSelection,
 | 
			
		||||
  $isRootNode,
 | 
			
		||||
  $isTextNode,
 | 
			
		||||
  $normalizeSelection__EXPERIMENTAL,
 | 
			
		||||
  $selectAll,
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +46,6 @@ import {
 | 
			
		|||
  ElementNode,
 | 
			
		||||
  FORMAT_ELEMENT_COMMAND,
 | 
			
		||||
  FORMAT_TEXT_COMMAND,
 | 
			
		||||
  INDENT_CONTENT_COMMAND,
 | 
			
		||||
  INSERT_LINE_BREAK_COMMAND,
 | 
			
		||||
  INSERT_PARAGRAPH_COMMAND,
 | 
			
		||||
  INSERT_TAB_COMMAND,
 | 
			
		||||
| 
						 | 
				
			
			@ -88,327 +58,22 @@ import {
 | 
			
		|||
  KEY_DELETE_COMMAND,
 | 
			
		||||
  KEY_ENTER_COMMAND,
 | 
			
		||||
  KEY_ESCAPE_COMMAND,
 | 
			
		||||
  OUTDENT_CONTENT_COMMAND,
 | 
			
		||||
  PASTE_COMMAND,
 | 
			
		||||
  REMOVE_TEXT_COMMAND,
 | 
			
		||||
  SELECT_ALL_COMMAND,
 | 
			
		||||
} from 'lexical';
 | 
			
		||||
import caretFromPoint from 'lexical/shared/caretFromPoint';
 | 
			
		||||
import {
 | 
			
		||||
  CAN_USE_BEFORE_INPUT,
 | 
			
		||||
  IS_APPLE_WEBKIT,
 | 
			
		||||
  IS_IOS,
 | 
			
		||||
  IS_SAFARI,
 | 
			
		||||
} from 'lexical/shared/environment';
 | 
			
		||||
 | 
			
		||||
export type SerializedHeadingNode = Spread<
 | 
			
		||||
  {
 | 
			
		||||
    tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
			
		||||
  },
 | 
			
		||||
  SerializedElementNode
 | 
			
		||||
>;
 | 
			
		||||
import {$insertDataTransferForRichText, copyToClipboard,} from '@lexical/clipboard';
 | 
			
		||||
import {$moveCharacter, $shouldOverrideDefaultCharacterSelection,} from '@lexical/selection';
 | 
			
		||||
import {$findMatchingParent, mergeRegister, objectKlassEquals,} from '@lexical/utils';
 | 
			
		||||
import caretFromPoint from 'lexical/shared/caretFromPoint';
 | 
			
		||||
import {CAN_USE_BEFORE_INPUT, IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI,} from 'lexical/shared/environment';
 | 
			
		||||
 | 
			
		||||
export const DRAG_DROP_PASTE: LexicalCommand<Array<File>> = createCommand(
 | 
			
		||||
  'DRAG_DROP_PASTE_FILE',
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export type SerializedQuoteNode = SerializedElementNode;
 | 
			
		||||
 | 
			
		||||
/** @noInheritDoc */
 | 
			
		||||
export class QuoteNode extends ElementNode {
 | 
			
		||||
  static getType(): string {
 | 
			
		||||
    return 'quote';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static clone(node: QuoteNode): QuoteNode {
 | 
			
		||||
    return new QuoteNode(node.__key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(key?: NodeKey) {
 | 
			
		||||
    super(key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // View
 | 
			
		||||
 | 
			
		||||
  createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
    const element = document.createElement('blockquote');
 | 
			
		||||
    addClassNamesToElement(element, config.theme.quote);
 | 
			
		||||
    return element;
 | 
			
		||||
  }
 | 
			
		||||
  updateDOM(prevNode: QuoteNode, dom: HTMLElement): boolean {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static importDOM(): DOMConversionMap | null {
 | 
			
		||||
    return {
 | 
			
		||||
      blockquote: (node: Node) => ({
 | 
			
		||||
        conversion: $convertBlockquoteElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
			
		||||
    const {element} = super.exportDOM(editor);
 | 
			
		||||
 | 
			
		||||
    if (element && isHTMLElement(element)) {
 | 
			
		||||
      if (this.isEmpty()) {
 | 
			
		||||
        element.append(document.createElement('br'));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      element,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static importJSON(serializedNode: SerializedQuoteNode): QuoteNode {
 | 
			
		||||
    const node = $createQuoteNode();
 | 
			
		||||
    return node;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exportJSON(): SerializedElementNode {
 | 
			
		||||
    return {
 | 
			
		||||
      ...super.exportJSON(),
 | 
			
		||||
      type: 'quote',
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Mutation
 | 
			
		||||
 | 
			
		||||
  insertNewAfter(_: RangeSelection, restoreSelection?: boolean): ParagraphNode {
 | 
			
		||||
    const newBlock = $createParagraphNode();
 | 
			
		||||
    const direction = this.getDirection();
 | 
			
		||||
    newBlock.setDirection(direction);
 | 
			
		||||
    this.insertAfter(newBlock, restoreSelection);
 | 
			
		||||
    return newBlock;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapseAtStart(): true {
 | 
			
		||||
    const paragraph = $createParagraphNode();
 | 
			
		||||
    const children = this.getChildren();
 | 
			
		||||
    children.forEach((child) => paragraph.append(child));
 | 
			
		||||
    this.replace(paragraph);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  canMergeWhenEmpty(): true {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createQuoteNode(): QuoteNode {
 | 
			
		||||
  return $applyNodeReplacement(new QuoteNode());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isQuoteNode(
 | 
			
		||||
  node: LexicalNode | null | undefined,
 | 
			
		||||
): node is QuoteNode {
 | 
			
		||||
  return node instanceof QuoteNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
 | 
			
		||||
 | 
			
		||||
/** @noInheritDoc */
 | 
			
		||||
export class HeadingNode extends ElementNode {
 | 
			
		||||
  /** @internal */
 | 
			
		||||
  __tag: HeadingTagType;
 | 
			
		||||
 | 
			
		||||
  static getType(): string {
 | 
			
		||||
    return 'heading';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static clone(node: HeadingNode): HeadingNode {
 | 
			
		||||
    return new HeadingNode(node.__tag, node.__key);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constructor(tag: HeadingTagType, key?: NodeKey) {
 | 
			
		||||
    super(key);
 | 
			
		||||
    this.__tag = tag;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTag(): HeadingTagType {
 | 
			
		||||
    return this.__tag;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // View
 | 
			
		||||
 | 
			
		||||
  createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
    const tag = this.__tag;
 | 
			
		||||
    const element = document.createElement(tag);
 | 
			
		||||
    const theme = config.theme;
 | 
			
		||||
    const classNames = theme.heading;
 | 
			
		||||
    if (classNames !== undefined) {
 | 
			
		||||
      const className = classNames[tag];
 | 
			
		||||
      addClassNamesToElement(element, className);
 | 
			
		||||
    }
 | 
			
		||||
    return element;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static importDOM(): DOMConversionMap | null {
 | 
			
		||||
    return {
 | 
			
		||||
      h1: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      h2: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      h3: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      h4: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      h5: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      h6: (node: Node) => ({
 | 
			
		||||
        conversion: $convertHeadingElement,
 | 
			
		||||
        priority: 0,
 | 
			
		||||
      }),
 | 
			
		||||
      p: (node: Node) => {
 | 
			
		||||
        // domNode is a <p> since we matched it by nodeName
 | 
			
		||||
        const paragraph = node as HTMLParagraphElement;
 | 
			
		||||
        const firstChild = paragraph.firstChild;
 | 
			
		||||
        if (firstChild !== null && isGoogleDocsTitle(firstChild)) {
 | 
			
		||||
          return {
 | 
			
		||||
            conversion: () => ({node: null}),
 | 
			
		||||
            priority: 3,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
      span: (node: Node) => {
 | 
			
		||||
        if (isGoogleDocsTitle(node)) {
 | 
			
		||||
          return {
 | 
			
		||||
            conversion: (domNode: Node) => {
 | 
			
		||||
              return {
 | 
			
		||||
                node: $createHeadingNode('h1'),
 | 
			
		||||
              };
 | 
			
		||||
            },
 | 
			
		||||
            priority: 3,
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
        return null;
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exportDOM(editor: LexicalEditor): DOMExportOutput {
 | 
			
		||||
    const {element} = super.exportDOM(editor);
 | 
			
		||||
 | 
			
		||||
    if (element && isHTMLElement(element)) {
 | 
			
		||||
      if (this.isEmpty()) {
 | 
			
		||||
        element.append(document.createElement('br'));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      element,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
 | 
			
		||||
    return $createHeadingNode(serializedNode.tag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  exportJSON(): SerializedHeadingNode {
 | 
			
		||||
    return {
 | 
			
		||||
      ...super.exportJSON(),
 | 
			
		||||
      tag: this.getTag(),
 | 
			
		||||
      type: 'heading',
 | 
			
		||||
      version: 1,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Mutation
 | 
			
		||||
  insertNewAfter(
 | 
			
		||||
    selection?: RangeSelection,
 | 
			
		||||
    restoreSelection = true,
 | 
			
		||||
  ): ParagraphNode | HeadingNode {
 | 
			
		||||
    const anchorOffet = selection ? selection.anchor.offset : 0;
 | 
			
		||||
    const lastDesc = this.getLastDescendant();
 | 
			
		||||
    const isAtEnd =
 | 
			
		||||
      !lastDesc ||
 | 
			
		||||
      (selection &&
 | 
			
		||||
        selection.anchor.key === lastDesc.getKey() &&
 | 
			
		||||
        anchorOffet === lastDesc.getTextContentSize());
 | 
			
		||||
    const newElement =
 | 
			
		||||
      isAtEnd || !selection
 | 
			
		||||
        ? $createParagraphNode()
 | 
			
		||||
        : $createHeadingNode(this.getTag());
 | 
			
		||||
    const direction = this.getDirection();
 | 
			
		||||
    newElement.setDirection(direction);
 | 
			
		||||
    this.insertAfter(newElement, restoreSelection);
 | 
			
		||||
    if (anchorOffet === 0 && !this.isEmpty() && selection) {
 | 
			
		||||
      const paragraph = $createParagraphNode();
 | 
			
		||||
      paragraph.select();
 | 
			
		||||
      this.replace(paragraph, true);
 | 
			
		||||
    }
 | 
			
		||||
    return newElement;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapseAtStart(): true {
 | 
			
		||||
    const newElement = !this.isEmpty()
 | 
			
		||||
      ? $createHeadingNode(this.getTag())
 | 
			
		||||
      : $createParagraphNode();
 | 
			
		||||
    const children = this.getChildren();
 | 
			
		||||
    children.forEach((child) => newElement.append(child));
 | 
			
		||||
    this.replace(newElement);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  extractWithChild(): boolean {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isGoogleDocsTitle(domNode: Node): boolean {
 | 
			
		||||
  if (domNode.nodeName.toLowerCase() === 'span') {
 | 
			
		||||
    return (domNode as HTMLSpanElement).style.fontSize === '26pt';
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
  const nodeName = element.nodeName.toLowerCase();
 | 
			
		||||
  let node = null;
 | 
			
		||||
  if (
 | 
			
		||||
    nodeName === 'h1' ||
 | 
			
		||||
    nodeName === 'h2' ||
 | 
			
		||||
    nodeName === 'h3' ||
 | 
			
		||||
    nodeName === 'h4' ||
 | 
			
		||||
    nodeName === 'h5' ||
 | 
			
		||||
    nodeName === 'h6'
 | 
			
		||||
  ) {
 | 
			
		||||
    node = $createHeadingNode(nodeName);
 | 
			
		||||
  }
 | 
			
		||||
  return {node};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
  const node = $createQuoteNode();
 | 
			
		||||
  return {node};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
 | 
			
		||||
  return $applyNodeReplacement(new HeadingNode(headingTag));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isHeadingNode(
 | 
			
		||||
  node: LexicalNode | null | undefined,
 | 
			
		||||
): node is HeadingNode {
 | 
			
		||||
  return node instanceof HeadingNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onPasteForRichText(
 | 
			
		||||
  event: CommandPayloadType<typeof PASTE_COMMAND>,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,7 +8,7 @@
 | 
			
		|||
 | 
			
		||||
import {$createLinkNode} from '@lexical/link';
 | 
			
		||||
import {$createListItemNode, $createListNode} from '@lexical/list';
 | 
			
		||||
import {$createHeadingNode, registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  $addNodeStyle,
 | 
			
		||||
  $getSelectionStyleValueForProperty,
 | 
			
		||||
| 
						 | 
				
			
			@ -74,6 +74,7 @@ import {
 | 
			
		|||
} from '../utils';
 | 
			
		||||
import {createEmptyHistoryState, registerHistory} from "@lexical/history";
 | 
			
		||||
import {mergeRegister} from "@lexical/utils";
 | 
			
		||||
import {$createHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
interface ExpectedSelection {
 | 
			
		||||
  anchorPath: number[];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,6 @@
 | 
			
		|||
 */
 | 
			
		||||
 | 
			
		||||
import {$createLinkNode} from '@lexical/link';
 | 
			
		||||
import {$createHeadingNode, $isHeadingNode} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  $getSelectionStyleValueForProperty,
 | 
			
		||||
  $patchStyleText,
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +43,7 @@ import {
 | 
			
		|||
} from 'lexical/__tests__/utils';
 | 
			
		||||
 | 
			
		||||
import {$setAnchorPoint, $setFocusPoint} from '../utils';
 | 
			
		||||
import {$createHeadingNode, $isHeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
Range.prototype.getBoundingClientRect = function (): DOMRect {
 | 
			
		||||
  const rect = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@
 | 
			
		|||
 */
 | 
			
		||||
import {AutoLinkNode, LinkNode} from '@lexical/link';
 | 
			
		||||
import {ListItemNode, ListNode} from '@lexical/list';
 | 
			
		||||
import {HeadingNode, QuoteNode, registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {registerRichText} from '@lexical/rich-text';
 | 
			
		||||
import {
 | 
			
		||||
  applySelectionInputs,
 | 
			
		||||
  pasteHTML,
 | 
			
		||||
| 
						 | 
				
			
			@ -15,6 +15,8 @@ import {
 | 
			
		|||
import {TableCellNode, TableNode, TableRowNode} from '@lexical/table';
 | 
			
		||||
import {$createParagraphNode, $insertNodes, LexicalEditor} from 'lexical';
 | 
			
		||||
import {createTestEditor, initializeClipboard} from 'lexical/__tests__/utils';
 | 
			
		||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
jest.mock('lexical/shared/environment', () => {
 | 
			
		||||
  const originalModule = jest.requireActual('lexical/shared/environment');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,146 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
    DOMConversionMap,
 | 
			
		||||
    DOMConversionOutput,
 | 
			
		||||
    LexicalNode,
 | 
			
		||||
    Spread
 | 
			
		||||
} from "lexical";
 | 
			
		||||
import {EditorConfig} from "lexical/LexicalEditor";
 | 
			
		||||
import {HeadingNode, HeadingTagType, SerializedHeadingNode} from "@lexical/rich-text";
 | 
			
		||||
import {
 | 
			
		||||
    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
 | 
			
		||||
    SerializedCommonBlockNode,
 | 
			
		||||
    setCommonBlockPropsFromElement,
 | 
			
		||||
    updateElementWithCommonBlockProps
 | 
			
		||||
} from "./_common";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type SerializedCustomHeadingNode = Spread<SerializedCommonBlockNode, SerializedHeadingNode>
 | 
			
		||||
 | 
			
		||||
export class CustomHeadingNode extends HeadingNode {
 | 
			
		||||
    __id: string = '';
 | 
			
		||||
    __alignment: CommonBlockAlignment = '';
 | 
			
		||||
    __inset: number = 0;
 | 
			
		||||
 | 
			
		||||
    static getType() {
 | 
			
		||||
        return 'custom-heading';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setId(id: string) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__id = id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getId(): string {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setAlignment(alignment: CommonBlockAlignment) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__alignment = alignment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAlignment(): CommonBlockAlignment {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__alignment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setInset(size: number) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__inset = size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getInset(): number {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__inset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static clone(node: CustomHeadingNode) {
 | 
			
		||||
        const newNode = new CustomHeadingNode(node.__tag, node.__key);
 | 
			
		||||
        newNode.__alignment = node.__alignment;
 | 
			
		||||
        newNode.__inset = node.__inset;
 | 
			
		||||
        return newNode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
        const dom = super.createDOM(config);
 | 
			
		||||
        updateElementWithCommonBlockProps(dom, this);
 | 
			
		||||
        return dom;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateDOM(prevNode: CustomHeadingNode, dom: HTMLElement): boolean {
 | 
			
		||||
        return super.updateDOM(prevNode, dom)
 | 
			
		||||
            || commonPropertiesDifferent(prevNode, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportJSON(): SerializedCustomHeadingNode {
 | 
			
		||||
        return {
 | 
			
		||||
            ...super.exportJSON(),
 | 
			
		||||
            type: 'custom-heading',
 | 
			
		||||
            version: 1,
 | 
			
		||||
            id: this.__id,
 | 
			
		||||
            alignment: this.__alignment,
 | 
			
		||||
            inset: this.__inset,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importJSON(serializedNode: SerializedCustomHeadingNode): CustomHeadingNode {
 | 
			
		||||
        const node = $createCustomHeadingNode(serializedNode.tag);
 | 
			
		||||
        deserializeCommonBlockNode(serializedNode, node);
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importDOM(): DOMConversionMap | null {
 | 
			
		||||
        return {
 | 
			
		||||
            h1: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h2: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h3: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h4: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h5: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
            h6: (node: Node) => ({
 | 
			
		||||
                conversion: $convertHeadingElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
    const nodeName = element.nodeName.toLowerCase();
 | 
			
		||||
    let node = null;
 | 
			
		||||
    if (
 | 
			
		||||
        nodeName === 'h1' ||
 | 
			
		||||
        nodeName === 'h2' ||
 | 
			
		||||
        nodeName === 'h3' ||
 | 
			
		||||
        nodeName === 'h4' ||
 | 
			
		||||
        nodeName === 'h5' ||
 | 
			
		||||
        nodeName === 'h6'
 | 
			
		||||
    ) {
 | 
			
		||||
        node = $createCustomHeadingNode(nodeName);
 | 
			
		||||
        setCommonBlockPropsFromElement(element, node);
 | 
			
		||||
    }
 | 
			
		||||
    return {node};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createCustomHeadingNode(tag: HeadingTagType) {
 | 
			
		||||
    return new CustomHeadingNode(tag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isCustomHeadingNode(node: LexicalNode | null | undefined): node is CustomHeadingNode {
 | 
			
		||||
    return node instanceof CustomHeadingNode;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,115 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
    DOMConversionMap,
 | 
			
		||||
    DOMConversionOutput,
 | 
			
		||||
    LexicalNode,
 | 
			
		||||
    Spread
 | 
			
		||||
} from "lexical";
 | 
			
		||||
import {EditorConfig} from "lexical/LexicalEditor";
 | 
			
		||||
import {QuoteNode, SerializedQuoteNode} from "@lexical/rich-text";
 | 
			
		||||
import {
 | 
			
		||||
    CommonBlockAlignment, commonPropertiesDifferent, deserializeCommonBlockNode,
 | 
			
		||||
    SerializedCommonBlockNode,
 | 
			
		||||
    setCommonBlockPropsFromElement,
 | 
			
		||||
    updateElementWithCommonBlockProps
 | 
			
		||||
} from "./_common";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export type SerializedCustomQuoteNode = Spread<SerializedCommonBlockNode, SerializedQuoteNode>
 | 
			
		||||
 | 
			
		||||
export class CustomQuoteNode extends QuoteNode {
 | 
			
		||||
    __id: string = '';
 | 
			
		||||
    __alignment: CommonBlockAlignment = '';
 | 
			
		||||
    __inset: number = 0;
 | 
			
		||||
 | 
			
		||||
    static getType() {
 | 
			
		||||
        return 'custom-quote';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setId(id: string) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__id = id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getId(): string {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__id;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setAlignment(alignment: CommonBlockAlignment) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__alignment = alignment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAlignment(): CommonBlockAlignment {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__alignment;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setInset(size: number) {
 | 
			
		||||
        const self = this.getWritable();
 | 
			
		||||
        self.__inset = size;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getInset(): number {
 | 
			
		||||
        const self = this.getLatest();
 | 
			
		||||
        return self.__inset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static clone(node: CustomQuoteNode) {
 | 
			
		||||
        const newNode = new CustomQuoteNode(node.__key);
 | 
			
		||||
        newNode.__id = node.__id;
 | 
			
		||||
        newNode.__alignment = node.__alignment;
 | 
			
		||||
        newNode.__inset = node.__inset;
 | 
			
		||||
        return newNode;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    createDOM(config: EditorConfig): HTMLElement {
 | 
			
		||||
        const dom = super.createDOM(config);
 | 
			
		||||
        updateElementWithCommonBlockProps(dom, this);
 | 
			
		||||
        return dom;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateDOM(prevNode: CustomQuoteNode): boolean {
 | 
			
		||||
        return commonPropertiesDifferent(prevNode, this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    exportJSON(): SerializedCustomQuoteNode {
 | 
			
		||||
        return {
 | 
			
		||||
            ...super.exportJSON(),
 | 
			
		||||
            type: 'custom-quote',
 | 
			
		||||
            version: 1,
 | 
			
		||||
            id: this.__id,
 | 
			
		||||
            alignment: this.__alignment,
 | 
			
		||||
            inset: this.__inset,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importJSON(serializedNode: SerializedCustomQuoteNode): CustomQuoteNode {
 | 
			
		||||
        const node = $createCustomQuoteNode();
 | 
			
		||||
        deserializeCommonBlockNode(serializedNode, node);
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static importDOM(): DOMConversionMap | null {
 | 
			
		||||
        return {
 | 
			
		||||
            blockquote: (node: Node) => ({
 | 
			
		||||
                conversion: $convertBlockquoteElement,
 | 
			
		||||
                priority: 0,
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function $convertBlockquoteElement(element: HTMLElement): DOMConversionOutput {
 | 
			
		||||
    const node = $createCustomQuoteNode();
 | 
			
		||||
    setCommonBlockPropsFromElement(element, node);
 | 
			
		||||
    return {node};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $createCustomQuoteNode() {
 | 
			
		||||
    return new CustomQuoteNode();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function $isCustomQuoteNode(node: LexicalNode | null | undefined): node is CustomQuoteNode {
 | 
			
		||||
    return node instanceof CustomQuoteNode;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import {HeadingNode, QuoteNode} from '@lexical/rich-text';
 | 
			
		||||
import {CalloutNode} from './callout';
 | 
			
		||||
import {
 | 
			
		||||
    ElementNode,
 | 
			
		||||
| 
						 | 
				
			
			@ -21,9 +20,9 @@ import {MediaNode} from "./media";
 | 
			
		|||
import {CustomListItemNode} from "./custom-list-item";
 | 
			
		||||
import {CustomTableCellNode} from "./custom-table-cell";
 | 
			
		||||
import {CustomTableRowNode} from "./custom-table-row";
 | 
			
		||||
import {CustomHeadingNode} from "./custom-heading";
 | 
			
		||||
import {CustomQuoteNode} from "./custom-quote";
 | 
			
		||||
import {CustomListNode} from "./custom-list";
 | 
			
		||||
import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Load the nodes for lexical.
 | 
			
		||||
| 
						 | 
				
			
			@ -31,8 +30,8 @@ import {CustomListNode} from "./custom-list";
 | 
			
		|||
export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> | LexicalNodeReplacement)[] {
 | 
			
		||||
    return [
 | 
			
		||||
        CalloutNode,
 | 
			
		||||
        CustomHeadingNode,
 | 
			
		||||
        CustomQuoteNode,
 | 
			
		||||
        HeadingNode,
 | 
			
		||||
        QuoteNode,
 | 
			
		||||
        CustomListNode,
 | 
			
		||||
        CustomListItemNode, // TODO - Alignment?
 | 
			
		||||
        CustomTableNode,
 | 
			
		||||
| 
						 | 
				
			
			@ -46,18 +45,6 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
 | 
			
		|||
        MediaNode, // TODO - Alignment
 | 
			
		||||
        ParagraphNode,
 | 
			
		||||
        LinkNode,
 | 
			
		||||
        {
 | 
			
		||||
            replace: HeadingNode,
 | 
			
		||||
            with: (node: HeadingNode) => {
 | 
			
		||||
                return new CustomHeadingNode(node.__tag);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            replace: QuoteNode,
 | 
			
		||||
            with: (node: QuoteNode) => {
 | 
			
		||||
                return new CustomQuoteNode();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            replace: ListNode,
 | 
			
		||||
            with: (node: ListNode) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,12 +6,12 @@ import {
 | 
			
		|||
    toggleSelectionAsHeading, toggleSelectionAsList,
 | 
			
		||||
    toggleSelectionAsParagraph
 | 
			
		||||
} from "../utils/formats";
 | 
			
		||||
import {HeadingTagType} from "@lexical/rich-text";
 | 
			
		||||
import {EditorUiContext} from "../ui/framework/core";
 | 
			
		||||
import {$getNodeFromSelection} from "../utils/selection";
 | 
			
		||||
import {$isLinkNode, LinkNode} from "@lexical/link";
 | 
			
		||||
import {$showLinkForm} from "../ui/defaults/forms/objects";
 | 
			
		||||
import {showLinkSelector} from "../utils/links";
 | 
			
		||||
import {HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
function headerHandler(editor: LexicalEditor, tag: HeadingTagType): boolean {
 | 
			
		||||
    toggleSelectionAsHeading(editor, tag);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,18 +2,14 @@ import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../
 | 
			
		|||
import {EditorButtonDefinition} from "../../framework/buttons";
 | 
			
		||||
import {EditorUiContext} from "../../framework/core";
 | 
			
		||||
import {$isParagraphNode, BaseSelection, LexicalNode} from "lexical";
 | 
			
		||||
import {
 | 
			
		||||
    $isHeadingNode,
 | 
			
		||||
    $isQuoteNode,
 | 
			
		||||
    HeadingNode,
 | 
			
		||||
    HeadingTagType
 | 
			
		||||
} from "@lexical/rich-text";
 | 
			
		||||
import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../utils/selection";
 | 
			
		||||
import {
 | 
			
		||||
    toggleSelectionAsBlockquote,
 | 
			
		||||
    toggleSelectionAsHeading,
 | 
			
		||||
    toggleSelectionAsParagraph
 | 
			
		||||
} from "../../../utils/formats";
 | 
			
		||||
import {$isHeadingNode, HeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {$isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
 | 
			
		||||
    return {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,13 @@
 | 
			
		|||
import {EditorContainerUiElement} from "../core";
 | 
			
		||||
import {el} from "../../../utils/dom";
 | 
			
		||||
import {EditorFormField} from "../forms";
 | 
			
		||||
import {CustomHeadingNode} from "../../../nodes/custom-heading";
 | 
			
		||||
import {$getAllNodesOfType} from "../../../utils/nodes";
 | 
			
		||||
import {$isHeadingNode} from "@lexical/rich-text";
 | 
			
		||||
import {uniqueIdSmall} from "../../../../services/util";
 | 
			
		||||
import {$isHeadingNode, HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
 | 
			
		||||
export class LinkField extends EditorContainerUiElement {
 | 
			
		||||
    protected input: EditorFormField;
 | 
			
		||||
    protected headerMap = new Map<string, CustomHeadingNode>();
 | 
			
		||||
    protected headerMap = new Map<string, HeadingNode>();
 | 
			
		||||
 | 
			
		||||
    constructor(input: EditorFormField) {
 | 
			
		||||
        super([input]);
 | 
			
		||||
| 
						 | 
				
			
			@ -43,7 +42,7 @@ export class LinkField extends EditorContainerUiElement {
 | 
			
		|||
        return container;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateFormFromHeader(header: CustomHeadingNode) {
 | 
			
		||||
    updateFormFromHeader(header: HeadingNode) {
 | 
			
		||||
        this.getHeaderIdAndText(header).then(({id, text}) => {
 | 
			
		||||
            console.log('updating form', id, text);
 | 
			
		||||
            const modal =  this.getContext().manager.getActiveModal('link');
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +56,7 @@ export class LinkField extends EditorContainerUiElement {
 | 
			
		|||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getHeaderIdAndText(header: CustomHeadingNode): Promise<{id: string, text: string}> {
 | 
			
		||||
    getHeaderIdAndText(header: HeadingNode): Promise<{id: string, text: string}> {
 | 
			
		||||
        return new Promise((res) => {
 | 
			
		||||
            this.getContext().editor.update(() => {
 | 
			
		||||
                let id = header.getId();
 | 
			
		||||
| 
						 | 
				
			
			@ -75,7 +74,7 @@ export class LinkField extends EditorContainerUiElement {
 | 
			
		|||
 | 
			
		||||
    updateDataList(listEl: HTMLElement) {
 | 
			
		||||
        this.getContext().editor.getEditorState().read(() => {
 | 
			
		||||
            const headers = $getAllNodesOfType($isHeadingNode) as CustomHeadingNode[];
 | 
			
		||||
            const headers = $getAllNodesOfType($isHeadingNode) as HeadingNode[];
 | 
			
		||||
 | 
			
		||||
            this.headerMap.clear();
 | 
			
		||||
            const listEls: HTMLElement[] = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,3 @@
 | 
			
		|||
import {$isQuoteNode, HeadingNode, HeadingTagType} from "@lexical/rich-text";
 | 
			
		||||
import {
 | 
			
		||||
    $createParagraphNode,
 | 
			
		||||
    $createTextNode,
 | 
			
		||||
| 
						 | 
				
			
			@ -15,23 +14,23 @@ import {
 | 
			
		|||
    $toggleSelectionBlockNodeType,
 | 
			
		||||
    getLastSelection
 | 
			
		||||
} from "./selection";
 | 
			
		||||
import {$createCustomHeadingNode, $isCustomHeadingNode} from "../nodes/custom-heading";
 | 
			
		||||
import {$createCustomQuoteNode} from "../nodes/custom-quote";
 | 
			
		||||
import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../nodes/code-block";
 | 
			
		||||
import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "../nodes/callout";
 | 
			
		||||
import {insertList, ListNode, ListType, removeList} from "@lexical/list";
 | 
			
		||||
import {$isCustomListNode} from "../nodes/custom-list";
 | 
			
		||||
import {$createLinkNode, $isLinkNode} from "@lexical/link";
 | 
			
		||||
import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode";
 | 
			
		||||
import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
 | 
			
		||||
 | 
			
		||||
const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => {
 | 
			
		||||
    return $isCustomHeadingNode(node) && (node as HeadingNode).getTag() === tag;
 | 
			
		||||
    return $isHeadingNode(node) && node.getTag() === tag;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function toggleSelectionAsHeading(editor: LexicalEditor, tag: HeadingTagType) {
 | 
			
		||||
    editor.update(() => {
 | 
			
		||||
        $toggleSelectionBlockNodeType(
 | 
			
		||||
            (node) => $isHeaderNodeOfTag(node, tag),
 | 
			
		||||
            () => $createCustomHeadingNode(tag),
 | 
			
		||||
            () => $createHeadingNode(tag),
 | 
			
		||||
        )
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +43,7 @@ export function toggleSelectionAsParagraph(editor: LexicalEditor) {
 | 
			
		|||
 | 
			
		||||
export function toggleSelectionAsBlockquote(editor: LexicalEditor) {
 | 
			
		||||
    editor.update(() => {
 | 
			
		||||
        $toggleSelectionBlockNodeType($isQuoteNode, $createCustomQuoteNode);
 | 
			
		||||
        $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue