201 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			201 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| import {
 | |
|     $applyNodeReplacement,
 | |
|     $createParagraphNode,
 | |
|     type DOMConversionMap,
 | |
|     DOMConversionOutput,
 | |
|     type DOMExportOutput,
 | |
|     type EditorConfig,
 | |
|     isHTMLElement,
 | |
|     type LexicalEditor,
 | |
|     type LexicalNode,
 | |
|     type NodeKey,
 | |
|     type ParagraphNode,
 | |
|     type RangeSelection,
 | |
|     type Spread
 | |
| } from "lexical";
 | |
| import {addClassNamesToElement} from "@lexical/utils";
 | |
| import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
 | |
| import {
 | |
|     commonPropertiesDifferent, deserializeCommonBlockNode,
 | |
|     setCommonBlockPropsFromElement,
 | |
|     updateElementWithCommonBlockProps
 | |
| } from "lexical/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;
 | |
| } |