]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts
e8d044b218bc37866f445b6c7facfd83392978f9
[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 {RangeSelection, TEXT_TYPE_TO_FORMAT, TextFormatType} from 'lexical';
23
24 import {
25   $applyNodeReplacement,
26   getCachedClassNameArray,
27   isHTMLElement,
28 } from '../LexicalUtils';
29 import {$isTextNode} from './LexicalTextNode';
30 import {
31   commonPropertiesDifferent, deserializeCommonBlockNode,
32   setCommonBlockPropsFromElement,
33   updateElementWithCommonBlockProps
34 } from "./common";
35 import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
36
37 export type SerializedParagraphNode = Spread<
38   {
39     textFormat: number;
40     textStyle: string;
41   },
42   SerializedCommonBlockNode
43 >;
44
45 /** @noInheritDoc */
46 export class ParagraphNode extends CommonBlockNode {
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     copyCommonBlockProperties(prevNode, this);
98   }
99
100   // View
101
102   createDOM(config: EditorConfig): HTMLElement {
103     const dom = document.createElement('p');
104     const classNames = getCachedClassNameArray(config.theme, 'paragraph');
105     if (classNames !== undefined) {
106       const domClassList = dom.classList;
107       domClassList.add(...classNames);
108     }
109
110     updateElementWithCommonBlockProps(dom, this);
111
112     return dom;
113   }
114   updateDOM(
115     prevNode: ParagraphNode,
116     dom: HTMLElement,
117     config: EditorConfig,
118   ): boolean {
119     return commonPropertiesDifferent(prevNode, this);
120   }
121
122   static importDOM(): DOMConversionMap | null {
123     return {
124       p: (node: Node) => ({
125         conversion: $convertParagraphElement,
126         priority: 0,
127       }),
128     };
129   }
130
131   exportDOM(editor: LexicalEditor): DOMExportOutput {
132     const {element} = super.exportDOM(editor);
133
134     if (element && isHTMLElement(element)) {
135       if (this.isEmpty()) {
136         element.append(document.createElement('br'));
137       }
138     }
139
140     return {
141       element,
142     };
143   }
144
145   static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
146     const node = $createParagraphNode();
147     deserializeCommonBlockNode(serializedNode, node);
148     node.setTextFormat(serializedNode.textFormat);
149     return node;
150   }
151
152   exportJSON(): SerializedParagraphNode {
153     return {
154       ...super.exportJSON(),
155       textFormat: this.getTextFormat(),
156       textStyle: this.getTextStyle(),
157       type: 'paragraph',
158       version: 1,
159     };
160   }
161
162   // Mutation
163
164   insertNewAfter(
165     rangeSelection: RangeSelection,
166     restoreSelection: boolean,
167   ): ParagraphNode {
168     const newElement = $createParagraphNode();
169     newElement.setTextFormat(rangeSelection.format);
170     newElement.setTextStyle(rangeSelection.style);
171     const direction = this.getDirection();
172     newElement.setDirection(direction);
173     newElement.setStyle(this.getTextStyle());
174     this.insertAfter(newElement, restoreSelection);
175     return newElement;
176   }
177
178   collapseAtStart(): boolean {
179     const children = this.getChildren();
180     // If we have an empty (trimmed) first paragraph and try and remove it,
181     // delete the paragraph as long as we have another sibling to go to
182     if (
183       children.length === 0 ||
184       ($isTextNode(children[0]) && children[0].getTextContent().trim() === '')
185     ) {
186       const nextSibling = this.getNextSibling();
187       if (nextSibling !== null) {
188         this.selectNext();
189         this.remove();
190         return true;
191       }
192       const prevSibling = this.getPreviousSibling();
193       if (prevSibling !== null) {
194         this.selectPrevious();
195         this.remove();
196         return true;
197       }
198     }
199     return false;
200   }
201 }
202
203 function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
204   const node = $createParagraphNode();
205   setCommonBlockPropsFromElement(element, node);
206   return {node};
207 }
208
209 export function $createParagraphNode(): ParagraphNode {
210   return $applyNodeReplacement(new ParagraphNode());
211 }
212
213 export function $isParagraphNode(
214   node: LexicalNode | null | undefined,
215 ): node is ParagraphNode {
216   return node instanceof ParagraphNode;
217 }