]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts
4e69dc21c3cadf11da3a911c9fe3ee7c294ca6a2
[bookstack] / resources / js / wysiwyg / lexical / core / nodes / LexicalParagraphNode.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {
10   EditorConfig,
11   KlassConstructor,
12   LexicalEditor,
13   Spread,
14 } from '../LexicalEditor';
15 import type {
16   DOMConversionMap,
17   DOMConversionOutput,
18   DOMExportOutput,
19   LexicalNode,
20   NodeKey,
21 } from '../LexicalNode';
22 import type {
23   ElementFormatType,
24   SerializedElementNode,
25 } from './LexicalElementNode';
26 import type {RangeSelection} from 'lexical';
27
28 import {TEXT_TYPE_TO_FORMAT} from '../LexicalConstants';
29 import {
30   $applyNodeReplacement,
31   getCachedClassNameArray,
32   isHTMLElement,
33 } from '../LexicalUtils';
34 import {ElementNode} from './LexicalElementNode';
35 import {$isTextNode, TextFormatType} from './LexicalTextNode';
36
37 export type SerializedParagraphNode = Spread<
38   {
39     textFormat: number;
40     textStyle: string;
41   },
42   SerializedElementNode
43 >;
44
45 /** @noInheritDoc */
46 export class ParagraphNode extends ElementNode {
47   ['constructor']!: KlassConstructor<typeof ParagraphNode>;
48   /** @internal */
49   __textFormat: number;
50   __textStyle: string;
51
52   constructor(key?: NodeKey) {
53     super(key);
54     this.__textFormat = 0;
55     this.__textStyle = '';
56   }
57
58   static getType(): string {
59     return 'paragraph';
60   }
61
62   getTextFormat(): number {
63     const self = this.getLatest();
64     return self.__textFormat;
65   }
66
67   setTextFormat(type: number): this {
68     const self = this.getWritable();
69     self.__textFormat = type;
70     return self;
71   }
72
73   hasTextFormat(type: TextFormatType): boolean {
74     const formatFlag = TEXT_TYPE_TO_FORMAT[type];
75     return (this.getTextFormat() & formatFlag) !== 0;
76   }
77
78   getTextStyle(): string {
79     const self = this.getLatest();
80     return self.__textStyle;
81   }
82
83   setTextStyle(style: string): this {
84     const self = this.getWritable();
85     self.__textStyle = style;
86     return self;
87   }
88
89   static clone(node: ParagraphNode): ParagraphNode {
90     return new ParagraphNode(node.__key);
91   }
92
93   afterCloneFrom(prevNode: this) {
94     super.afterCloneFrom(prevNode);
95     this.__textFormat = prevNode.__textFormat;
96     this.__textStyle = prevNode.__textStyle;
97   }
98
99   // View
100
101   createDOM(config: EditorConfig): HTMLElement {
102     const dom = document.createElement('p');
103     const classNames = getCachedClassNameArray(config.theme, 'paragraph');
104     if (classNames !== undefined) {
105       const domClassList = dom.classList;
106       domClassList.add(...classNames);
107     }
108     return dom;
109   }
110   updateDOM(
111     prevNode: ParagraphNode,
112     dom: HTMLElement,
113     config: EditorConfig,
114   ): boolean {
115     return false;
116   }
117
118   static importDOM(): DOMConversionMap | null {
119     return {
120       p: (node: Node) => ({
121         conversion: $convertParagraphElement,
122         priority: 0,
123       }),
124     };
125   }
126
127   exportDOM(editor: LexicalEditor): DOMExportOutput {
128     const {element} = super.exportDOM(editor);
129
130     if (element && isHTMLElement(element)) {
131       if (this.isEmpty()) {
132         element.append(document.createElement('br'));
133       }
134
135       const formatType = this.getFormatType();
136       element.style.textAlign = formatType;
137
138       const indent = this.getIndent();
139       if (indent > 0) {
140         // padding-inline-start is not widely supported in email HTML, but
141         // Lexical Reconciler uses padding-inline-start. Using text-indent instead.
142         element.style.textIndent = `${indent * 20}px`;
143       }
144     }
145
146     return {
147       element,
148     };
149   }
150
151   static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
152     const node = $createParagraphNode();
153     node.setFormat(serializedNode.format);
154     node.setIndent(serializedNode.indent);
155     node.setTextFormat(serializedNode.textFormat);
156     return node;
157   }
158
159   exportJSON(): SerializedParagraphNode {
160     return {
161       ...super.exportJSON(),
162       textFormat: this.getTextFormat(),
163       textStyle: this.getTextStyle(),
164       type: 'paragraph',
165       version: 1,
166     };
167   }
168
169   // Mutation
170
171   insertNewAfter(
172     rangeSelection: RangeSelection,
173     restoreSelection: boolean,
174   ): ParagraphNode {
175     const newElement = $createParagraphNode();
176     newElement.setTextFormat(rangeSelection.format);
177     newElement.setTextStyle(rangeSelection.style);
178     const direction = this.getDirection();
179     newElement.setDirection(direction);
180     newElement.setFormat(this.getFormatType());
181     newElement.setStyle(this.getTextStyle());
182     this.insertAfter(newElement, restoreSelection);
183     return newElement;
184   }
185
186   collapseAtStart(): boolean {
187     const children = this.getChildren();
188     // If we have an empty (trimmed) first paragraph and try and remove it,
189     // delete the paragraph as long as we have another sibling to go to
190     if (
191       children.length === 0 ||
192       ($isTextNode(children[0]) && children[0].getTextContent().trim() === '')
193     ) {
194       const nextSibling = this.getNextSibling();
195       if (nextSibling !== null) {
196         this.selectNext();
197         this.remove();
198         return true;
199       }
200       const prevSibling = this.getPreviousSibling();
201       if (prevSibling !== null) {
202         this.selectPrevious();
203         this.remove();
204         return true;
205       }
206     }
207     return false;
208   }
209 }
210
211 function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
212   const node = $createParagraphNode();
213   if (element.style) {
214     node.setFormat(element.style.textAlign as ElementFormatType);
215     const indent = parseInt(element.style.textIndent, 10) / 20;
216     if (indent > 0) {
217       node.setIndent(indent);
218     }
219   }
220   return {node};
221 }
222
223 export function $createParagraphNode(): ParagraphNode {
224   return $applyNodeReplacement(new ParagraphNode());
225 }
226
227 export function $isParagraphNode(
228   node: LexicalNode | null | undefined,
229 ): node is ParagraphNode {
230   return node instanceof ParagraphNode;
231 }