]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/nodes/LexicalParagraphNode.ts
6517d939eda15557683dab11a8eeba66fc200f2e
[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 {RangeSelection} 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   SerializedCommonBlockNode, setCommonBlockPropsFromElement,
33   updateElementWithCommonBlockProps
34 } from "../../../nodes/_common";
35 import {CommonBlockNode, copyCommonBlockProperties} from "lexical/nodes/CommonBlockNode";
36
37 export type SerializedParagraphNode = Spread<
38   {
39     textStyle: string;
40   },
41   SerializedCommonBlockNode
42 >;
43
44 /** @noInheritDoc */
45 export class ParagraphNode extends CommonBlockNode {
46   ['constructor']!: KlassConstructor<typeof ParagraphNode>;
47   /** @internal */
48   __textStyle: string;
49
50   constructor(key?: NodeKey) {
51     super(key);
52     this.__textStyle = '';
53   }
54
55   static getType(): string {
56     return 'paragraph';
57   }
58
59   getTextStyle(): string {
60     const self = this.getLatest();
61     return self.__textStyle;
62   }
63
64   setTextStyle(style: string): this {
65     const self = this.getWritable();
66     self.__textStyle = style;
67     return self;
68   }
69
70   static clone(node: ParagraphNode): ParagraphNode {
71     return new ParagraphNode(node.__key);
72   }
73
74   afterCloneFrom(prevNode: this) {
75     super.afterCloneFrom(prevNode);
76     this.__textStyle = prevNode.__textStyle;
77     copyCommonBlockProperties(prevNode, this);
78   }
79
80   // View
81
82   createDOM(config: EditorConfig): HTMLElement {
83     const dom = document.createElement('p');
84     const classNames = getCachedClassNameArray(config.theme, 'paragraph');
85     if (classNames !== undefined) {
86       const domClassList = dom.classList;
87       domClassList.add(...classNames);
88     }
89
90     updateElementWithCommonBlockProps(dom, this);
91
92     return dom;
93   }
94   updateDOM(
95     prevNode: ParagraphNode,
96     dom: HTMLElement,
97     config: EditorConfig,
98   ): boolean {
99     return commonPropertiesDifferent(prevNode, this);
100   }
101
102   static importDOM(): DOMConversionMap | null {
103     return {
104       p: (node: Node) => ({
105         conversion: $convertParagraphElement,
106         priority: 0,
107       }),
108     };
109   }
110
111   exportDOM(editor: LexicalEditor): DOMExportOutput {
112     const {element} = super.exportDOM(editor);
113
114     if (element && isHTMLElement(element)) {
115       if (this.isEmpty()) {
116         element.append(document.createElement('br'));
117       }
118     }
119
120     return {
121       element,
122     };
123   }
124
125   static importJSON(serializedNode: SerializedParagraphNode): ParagraphNode {
126     const node = $createParagraphNode();
127     deserializeCommonBlockNode(serializedNode, node);
128     return node;
129   }
130
131   exportJSON(): SerializedParagraphNode {
132     return {
133       ...super.exportJSON(),
134       textStyle: this.getTextStyle(),
135       type: 'paragraph',
136       version: 1,
137     };
138   }
139
140   // Mutation
141
142   insertNewAfter(
143     rangeSelection: RangeSelection,
144     restoreSelection: boolean,
145   ): ParagraphNode {
146     const newElement = $createParagraphNode();
147     newElement.setTextStyle(rangeSelection.style);
148     const direction = this.getDirection();
149     newElement.setDirection(direction);
150     newElement.setStyle(this.getTextStyle());
151     this.insertAfter(newElement, restoreSelection);
152     return newElement;
153   }
154
155   collapseAtStart(): boolean {
156     const children = this.getChildren();
157     // If we have an empty (trimmed) first paragraph and try and remove it,
158     // delete the paragraph as long as we have another sibling to go to
159     if (
160       children.length === 0 ||
161       ($isTextNode(children[0]) && children[0].getTextContent().trim() === '')
162     ) {
163       const nextSibling = this.getNextSibling();
164       if (nextSibling !== null) {
165         this.selectNext();
166         this.remove();
167         return true;
168       }
169       const prevSibling = this.getPreviousSibling();
170       if (prevSibling !== null) {
171         this.selectPrevious();
172         this.remove();
173         return true;
174       }
175     }
176     return false;
177   }
178 }
179
180 function $convertParagraphElement(element: HTMLElement): DOMConversionOutput {
181   const node = $createParagraphNode();
182   setCommonBlockPropsFromElement(element, node);
183   return {node};
184 }
185
186 export function $createParagraphNode(): ParagraphNode {
187   return $applyNodeReplacement(new ParagraphNode());
188 }
189
190 export function $isParagraphNode(
191   node: LexicalNode | null | undefined,
192 ): node is ParagraphNode {
193   return node instanceof ParagraphNode;
194 }