]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/rich-text/LexicalHeadingNode.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / rich-text / LexicalHeadingNode.ts
1 import {
2     $applyNodeReplacement,
3     $createParagraphNode,
4     type DOMConversionMap,
5     DOMConversionOutput,
6     type DOMExportOutput,
7     type EditorConfig,
8     isHTMLElement,
9     type LexicalEditor,
10     type LexicalNode,
11     type NodeKey,
12     type ParagraphNode,
13     type RangeSelection,
14     type Spread
15 } from "lexical";
16 import {addClassNamesToElement} from "@lexical/utils";
17 import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
18 import {
19     commonPropertiesDifferent, deserializeCommonBlockNode,
20     setCommonBlockPropsFromElement,
21     updateElementWithCommonBlockProps
22 } from "lexical/nodes/common";
23
24 export type HeadingTagType = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
25
26 export type SerializedHeadingNode = Spread<
27     {
28         tag: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
29     },
30     SerializedCommonBlockNode
31 >;
32
33 /** @noInheritDoc */
34 export class HeadingNode extends CommonBlockNode {
35     /** @internal */
36     __tag: HeadingTagType;
37
38     static getType(): string {
39         return 'heading';
40     }
41
42     static clone(node: HeadingNode): HeadingNode {
43         const clone = new HeadingNode(node.__tag, node.__key);
44         copyCommonBlockProperties(node, clone);
45         return clone;
46     }
47
48     constructor(tag: HeadingTagType, key?: NodeKey) {
49         super(key);
50         this.__tag = tag;
51     }
52
53     getTag(): HeadingTagType {
54         return this.__tag;
55     }
56
57     // View
58
59     createDOM(config: EditorConfig): HTMLElement {
60         const tag = this.__tag;
61         const element = document.createElement(tag);
62         const theme = config.theme;
63         const classNames = theme.heading;
64         if (classNames !== undefined) {
65             const className = classNames[tag];
66             addClassNamesToElement(element, className);
67         }
68         updateElementWithCommonBlockProps(element, this);
69         return element;
70     }
71
72     updateDOM(prevNode: HeadingNode, dom: HTMLElement): boolean {
73         return commonPropertiesDifferent(prevNode, this);
74     }
75
76     static importDOM(): DOMConversionMap | null {
77         return {
78             h1: (node: Node) => ({
79                 conversion: $convertHeadingElement,
80                 priority: 0,
81             }),
82             h2: (node: Node) => ({
83                 conversion: $convertHeadingElement,
84                 priority: 0,
85             }),
86             h3: (node: Node) => ({
87                 conversion: $convertHeadingElement,
88                 priority: 0,
89             }),
90             h4: (node: Node) => ({
91                 conversion: $convertHeadingElement,
92                 priority: 0,
93             }),
94             h5: (node: Node) => ({
95                 conversion: $convertHeadingElement,
96                 priority: 0,
97             }),
98             h6: (node: Node) => ({
99                 conversion: $convertHeadingElement,
100                 priority: 0,
101             }),
102         };
103     }
104
105     exportDOM(editor: LexicalEditor): DOMExportOutput {
106         const {element} = super.exportDOM(editor);
107
108         if (element && isHTMLElement(element)) {
109             if (this.isEmpty()) {
110                 element.append(document.createElement('br'));
111             }
112         }
113
114         return {
115             element,
116         };
117     }
118
119     static importJSON(serializedNode: SerializedHeadingNode): HeadingNode {
120         const node = $createHeadingNode(serializedNode.tag);
121         deserializeCommonBlockNode(serializedNode, node);
122         return node;
123     }
124
125     exportJSON(): SerializedHeadingNode {
126         return {
127             ...super.exportJSON(),
128             tag: this.getTag(),
129             type: 'heading',
130             version: 1,
131         };
132     }
133
134     // Mutation
135     insertNewAfter(
136         selection?: RangeSelection,
137         restoreSelection = true,
138     ): ParagraphNode | HeadingNode {
139         const anchorOffet = selection ? selection.anchor.offset : 0;
140         const lastDesc = this.getLastDescendant();
141         const isAtEnd =
142             !lastDesc ||
143             (selection &&
144                 selection.anchor.key === lastDesc.getKey() &&
145                 anchorOffet === lastDesc.getTextContentSize());
146         const newElement =
147             isAtEnd || !selection
148                 ? $createParagraphNode()
149                 : $createHeadingNode(this.getTag());
150         const direction = this.getDirection();
151         newElement.setDirection(direction);
152         this.insertAfter(newElement, restoreSelection);
153         if (anchorOffet === 0 && !this.isEmpty() && selection) {
154             const paragraph = $createParagraphNode();
155             paragraph.select();
156             this.replace(paragraph, true);
157         }
158         return newElement;
159     }
160
161     collapseAtStart(): true {
162         const newElement = !this.isEmpty()
163             ? $createHeadingNode(this.getTag())
164             : $createParagraphNode();
165         const children = this.getChildren();
166         children.forEach((child) => newElement.append(child));
167         this.replace(newElement);
168         return true;
169     }
170
171     extractWithChild(): boolean {
172         return true;
173     }
174 }
175
176 function $convertHeadingElement(element: HTMLElement): DOMConversionOutput {
177     const nodeName = element.nodeName.toLowerCase();
178     let node = null;
179     if (
180         nodeName === 'h1' ||
181         nodeName === 'h2' ||
182         nodeName === 'h3' ||
183         nodeName === 'h4' ||
184         nodeName === 'h5' ||
185         nodeName === 'h6'
186     ) {
187         node = $createHeadingNode(nodeName);
188         setCommonBlockPropsFromElement(element, node);
189     }
190     return {node};
191 }
192
193 export function $createHeadingNode(headingTag: HeadingTagType): HeadingNode {
194     return $applyNodeReplacement(new HeadingNode(headingTag));
195 }
196
197 export function $isHeadingNode(
198     node: LexicalNode | null | undefined,
199 ): node is HeadingNode {
200     return node instanceof HeadingNode;
201 }