]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts
9624af67e7eb38b04a3443f4dd88757c567873b8
[bookstack] / resources / js / wysiwyg / lexical / core / nodes / LexicalElementNode.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 {NodeKey, SerializedLexicalNode} from '../LexicalNode';
10 import type {
11   BaseSelection,
12   PointType,
13   RangeSelection,
14 } from '../LexicalSelection';
15 import type {KlassConstructor, Spread} from 'lexical';
16
17 import invariant from 'lexical/shared/invariant';
18
19 import {$isTextNode, TextNode} from '../index';
20 import {
21   DOUBLE_LINE_BREAK,
22
23
24 } from '../LexicalConstants';
25 import {LexicalNode} from '../LexicalNode';
26 import {
27   $getSelection,
28   $internalMakeRangeSelection,
29   $isRangeSelection,
30   moveSelectionPointToSibling,
31 } from '../LexicalSelection';
32 import {errorOnReadOnly, getActiveEditor} from '../LexicalUpdates';
33 import {
34   $getNodeByKey,
35   $isRootOrShadowRoot,
36   removeFromParent,
37 } from '../LexicalUtils';
38
39 export type SerializedElementNode<
40   T extends SerializedLexicalNode = SerializedLexicalNode,
41 > = Spread<
42   {
43     children: Array<T>;
44     direction: 'ltr' | 'rtl' | null;
45   },
46   SerializedLexicalNode
47 >;
48
49 export type ElementFormatType =
50   | 'left'
51   | 'start'
52   | 'center'
53   | 'right'
54   | 'end'
55   | 'justify'
56   | '';
57
58 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
59 export interface ElementNode {
60   getTopLevelElement(): ElementNode | null;
61   getTopLevelElementOrThrow(): ElementNode;
62 }
63
64 /** @noInheritDoc */
65 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
66 export class ElementNode extends LexicalNode {
67   ['constructor']!: KlassConstructor<typeof ElementNode>;
68   /** @internal */
69   __first: null | NodeKey;
70   /** @internal */
71   __last: null | NodeKey;
72   /** @internal */
73   __size: number;
74   /** @internal */
75   __style: string;
76   /** @internal */
77   __dir: 'ltr' | 'rtl' | null;
78
79   constructor(key?: NodeKey) {
80     super(key);
81     this.__first = null;
82     this.__last = null;
83     this.__size = 0;
84     this.__style = '';
85     this.__dir = null;
86   }
87
88   afterCloneFrom(prevNode: this) {
89     super.afterCloneFrom(prevNode);
90     this.__first = prevNode.__first;
91     this.__last = prevNode.__last;
92     this.__size = prevNode.__size;
93     this.__style = prevNode.__style;
94     this.__dir = prevNode.__dir;
95   }
96
97   getStyle(): string {
98     const self = this.getLatest();
99     return self.__style;
100   }
101   getChildren<T extends LexicalNode>(): Array<T> {
102     const children: Array<T> = [];
103     let child: T | null = this.getFirstChild();
104     while (child !== null) {
105       children.push(child);
106       child = child.getNextSibling();
107     }
108     return children;
109   }
110   getChildrenKeys(): Array<NodeKey> {
111     const children: Array<NodeKey> = [];
112     let child: LexicalNode | null = this.getFirstChild();
113     while (child !== null) {
114       children.push(child.__key);
115       child = child.getNextSibling();
116     }
117     return children;
118   }
119   getChildrenSize(): number {
120     const self = this.getLatest();
121     return self.__size;
122   }
123   isEmpty(): boolean {
124     return this.getChildrenSize() === 0;
125   }
126   isDirty(): boolean {
127     const editor = getActiveEditor();
128     const dirtyElements = editor._dirtyElements;
129     return dirtyElements !== null && dirtyElements.has(this.__key);
130   }
131   isLastChild(): boolean {
132     const self = this.getLatest();
133     const parentLastChild = this.getParentOrThrow().getLastChild();
134     return parentLastChild !== null && parentLastChild.is(self);
135   }
136   getAllTextNodes(): Array<TextNode> {
137     const textNodes = [];
138     let child: LexicalNode | null = this.getFirstChild();
139     while (child !== null) {
140       if ($isTextNode(child)) {
141         textNodes.push(child);
142       }
143       if ($isElementNode(child)) {
144         const subChildrenNodes = child.getAllTextNodes();
145         textNodes.push(...subChildrenNodes);
146       }
147       child = child.getNextSibling();
148     }
149     return textNodes;
150   }
151   getFirstDescendant<T extends LexicalNode>(): null | T {
152     let node = this.getFirstChild<T>();
153     while ($isElementNode(node)) {
154       const child = node.getFirstChild<T>();
155       if (child === null) {
156         break;
157       }
158       node = child;
159     }
160     return node;
161   }
162   getLastDescendant<T extends LexicalNode>(): null | T {
163     let node = this.getLastChild<T>();
164     while ($isElementNode(node)) {
165       const child = node.getLastChild<T>();
166       if (child === null) {
167         break;
168       }
169       node = child;
170     }
171     return node;
172   }
173   getDescendantByIndex<T extends LexicalNode>(index: number): null | T {
174     const children = this.getChildren<T>();
175     const childrenLength = children.length;
176     // For non-empty element nodes, we resolve its descendant
177     // (either a leaf node or the bottom-most element)
178     if (index >= childrenLength) {
179       const resolvedNode = children[childrenLength - 1];
180       return (
181         ($isElementNode(resolvedNode) && resolvedNode.getLastDescendant()) ||
182         resolvedNode ||
183         null
184       );
185     }
186     const resolvedNode = children[index];
187     return (
188       ($isElementNode(resolvedNode) && resolvedNode.getFirstDescendant()) ||
189       resolvedNode ||
190       null
191     );
192   }
193   getFirstChild<T extends LexicalNode>(): null | T {
194     const self = this.getLatest();
195     const firstKey = self.__first;
196     return firstKey === null ? null : $getNodeByKey<T>(firstKey);
197   }
198   getFirstChildOrThrow<T extends LexicalNode>(): T {
199     const firstChild = this.getFirstChild<T>();
200     if (firstChild === null) {
201       invariant(false, 'Expected node %s to have a first child.', this.__key);
202     }
203     return firstChild;
204   }
205   getLastChild<T extends LexicalNode>(): null | T {
206     const self = this.getLatest();
207     const lastKey = self.__last;
208     return lastKey === null ? null : $getNodeByKey<T>(lastKey);
209   }
210   getLastChildOrThrow<T extends LexicalNode>(): T {
211     const lastChild = this.getLastChild<T>();
212     if (lastChild === null) {
213       invariant(false, 'Expected node %s to have a last child.', this.__key);
214     }
215     return lastChild;
216   }
217   getChildAtIndex<T extends LexicalNode>(index: number): null | T {
218     const size = this.getChildrenSize();
219     let node: null | T;
220     let i;
221     if (index < size / 2) {
222       node = this.getFirstChild<T>();
223       i = 0;
224       while (node !== null && i <= index) {
225         if (i === index) {
226           return node;
227         }
228         node = node.getNextSibling();
229         i++;
230       }
231       return null;
232     }
233     node = this.getLastChild<T>();
234     i = size - 1;
235     while (node !== null && i >= index) {
236       if (i === index) {
237         return node;
238       }
239       node = node.getPreviousSibling();
240       i--;
241     }
242     return null;
243   }
244   getTextContent(): string {
245     let textContent = '';
246     const children = this.getChildren();
247     const childrenLength = children.length;
248     for (let i = 0; i < childrenLength; i++) {
249       const child = children[i];
250       textContent += child.getTextContent();
251       if (
252         $isElementNode(child) &&
253         i !== childrenLength - 1 &&
254         !child.isInline()
255       ) {
256         textContent += DOUBLE_LINE_BREAK;
257       }
258     }
259     return textContent;
260   }
261   getTextContentSize(): number {
262     let textContentSize = 0;
263     const children = this.getChildren();
264     const childrenLength = children.length;
265     for (let i = 0; i < childrenLength; i++) {
266       const child = children[i];
267       textContentSize += child.getTextContentSize();
268       if (
269         $isElementNode(child) &&
270         i !== childrenLength - 1 &&
271         !child.isInline()
272       ) {
273         textContentSize += DOUBLE_LINE_BREAK.length;
274       }
275     }
276     return textContentSize;
277   }
278   getDirection(): 'ltr' | 'rtl' | null {
279     const self = this.getLatest();
280     return self.__dir;
281   }
282
283   // Mutators
284
285   select(_anchorOffset?: number, _focusOffset?: number): RangeSelection {
286     errorOnReadOnly();
287     const selection = $getSelection();
288     let anchorOffset = _anchorOffset;
289     let focusOffset = _focusOffset;
290     const childrenCount = this.getChildrenSize();
291     if (!this.canBeEmpty()) {
292       if (_anchorOffset === 0 && _focusOffset === 0) {
293         const firstChild = this.getFirstChild();
294         if ($isTextNode(firstChild) || $isElementNode(firstChild)) {
295           return firstChild.select(0, 0);
296         }
297       } else if (
298         (_anchorOffset === undefined || _anchorOffset === childrenCount) &&
299         (_focusOffset === undefined || _focusOffset === childrenCount)
300       ) {
301         const lastChild = this.getLastChild();
302         if ($isTextNode(lastChild) || $isElementNode(lastChild)) {
303           return lastChild.select();
304         }
305       }
306     }
307     if (anchorOffset === undefined) {
308       anchorOffset = childrenCount;
309     }
310     if (focusOffset === undefined) {
311       focusOffset = childrenCount;
312     }
313     const key = this.__key;
314     if (!$isRangeSelection(selection)) {
315       return $internalMakeRangeSelection(
316         key,
317         anchorOffset,
318         key,
319         focusOffset,
320         'element',
321         'element',
322       );
323     } else {
324       selection.anchor.set(key, anchorOffset, 'element');
325       selection.focus.set(key, focusOffset, 'element');
326       selection.dirty = true;
327     }
328     return selection;
329   }
330   selectStart(): RangeSelection {
331     const firstNode = this.getFirstDescendant();
332     return firstNode ? firstNode.selectStart() : this.select();
333   }
334   selectEnd(): RangeSelection {
335     const lastNode = this.getLastDescendant();
336     return lastNode ? lastNode.selectEnd() : this.select();
337   }
338   clear(): this {
339     const writableSelf = this.getWritable();
340     const children = this.getChildren();
341     children.forEach((child) => child.remove());
342     return writableSelf;
343   }
344   append(...nodesToAppend: LexicalNode[]): this {
345     return this.splice(this.getChildrenSize(), 0, nodesToAppend);
346   }
347   setDirection(direction: 'ltr' | 'rtl' | null): this {
348     const self = this.getWritable();
349     self.__dir = direction;
350     return self;
351   }
352   setStyle(style: string): this {
353     const self = this.getWritable();
354     self.__style = style || '';
355     return this;
356   }
357   splice(
358     start: number,
359     deleteCount: number,
360     nodesToInsert: Array<LexicalNode>,
361   ): this {
362     const nodesToInsertLength = nodesToInsert.length;
363     const oldSize = this.getChildrenSize();
364     const writableSelf = this.getWritable();
365     const writableSelfKey = writableSelf.__key;
366     const nodesToInsertKeys = [];
367     const nodesToRemoveKeys = [];
368     const nodeAfterRange = this.getChildAtIndex(start + deleteCount);
369     let nodeBeforeRange = null;
370     let newSize = oldSize - deleteCount + nodesToInsertLength;
371
372     if (start !== 0) {
373       if (start === oldSize) {
374         nodeBeforeRange = this.getLastChild();
375       } else {
376         const node = this.getChildAtIndex(start);
377         if (node !== null) {
378           nodeBeforeRange = node.getPreviousSibling();
379         }
380       }
381     }
382
383     if (deleteCount > 0) {
384       let nodeToDelete =
385         nodeBeforeRange === null
386           ? this.getFirstChild()
387           : nodeBeforeRange.getNextSibling();
388       for (let i = 0; i < deleteCount; i++) {
389         if (nodeToDelete === null) {
390           invariant(false, 'splice: sibling not found');
391         }
392         const nextSibling = nodeToDelete.getNextSibling();
393         const nodeKeyToDelete = nodeToDelete.__key;
394         const writableNodeToDelete = nodeToDelete.getWritable();
395         removeFromParent(writableNodeToDelete);
396         nodesToRemoveKeys.push(nodeKeyToDelete);
397         nodeToDelete = nextSibling;
398       }
399     }
400
401     let prevNode = nodeBeforeRange;
402     for (let i = 0; i < nodesToInsertLength; i++) {
403       const nodeToInsert = nodesToInsert[i];
404       if (prevNode !== null && nodeToInsert.is(prevNode)) {
405         nodeBeforeRange = prevNode = prevNode.getPreviousSibling();
406       }
407       const writableNodeToInsert = nodeToInsert.getWritable();
408       if (writableNodeToInsert.__parent === writableSelfKey) {
409         newSize--;
410       }
411       removeFromParent(writableNodeToInsert);
412       const nodeKeyToInsert = nodeToInsert.__key;
413       if (prevNode === null) {
414         writableSelf.__first = nodeKeyToInsert;
415         writableNodeToInsert.__prev = null;
416       } else {
417         const writablePrevNode = prevNode.getWritable();
418         writablePrevNode.__next = nodeKeyToInsert;
419         writableNodeToInsert.__prev = writablePrevNode.__key;
420       }
421       if (nodeToInsert.__key === writableSelfKey) {
422         invariant(false, 'append: attempting to append self');
423       }
424       // Set child parent to self
425       writableNodeToInsert.__parent = writableSelfKey;
426       nodesToInsertKeys.push(nodeKeyToInsert);
427       prevNode = nodeToInsert;
428     }
429
430     if (start + deleteCount === oldSize) {
431       if (prevNode !== null) {
432         const writablePrevNode = prevNode.getWritable();
433         writablePrevNode.__next = null;
434         writableSelf.__last = prevNode.__key;
435       }
436     } else if (nodeAfterRange !== null) {
437       const writableNodeAfterRange = nodeAfterRange.getWritable();
438       if (prevNode !== null) {
439         const writablePrevNode = prevNode.getWritable();
440         writableNodeAfterRange.__prev = prevNode.__key;
441         writablePrevNode.__next = nodeAfterRange.__key;
442       } else {
443         writableNodeAfterRange.__prev = null;
444       }
445     }
446
447     writableSelf.__size = newSize;
448
449     // In case of deletion we need to adjust selection, unlink removed nodes
450     // and clean up node itself if it becomes empty. None of these needed
451     // for insertion-only cases
452     if (nodesToRemoveKeys.length) {
453       // Adjusting selection, in case node that was anchor/focus will be deleted
454       const selection = $getSelection();
455       if ($isRangeSelection(selection)) {
456         const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);
457         const nodesToInsertKeySet = new Set(nodesToInsertKeys);
458
459         const {anchor, focus} = selection;
460         if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {
461           moveSelectionPointToSibling(
462             anchor,
463             anchor.getNode(),
464             this,
465             nodeBeforeRange,
466             nodeAfterRange,
467           );
468         }
469         if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {
470           moveSelectionPointToSibling(
471             focus,
472             focus.getNode(),
473             this,
474             nodeBeforeRange,
475             nodeAfterRange,
476           );
477         }
478         // Cleanup if node can't be empty
479         if (newSize === 0 && !this.canBeEmpty() && !$isRootOrShadowRoot(this)) {
480           this.remove();
481         }
482       }
483     }
484
485     return writableSelf;
486   }
487   // JSON serialization
488   exportJSON(): SerializedElementNode {
489     return {
490       children: [],
491       direction: this.getDirection(),
492       type: 'element',
493       version: 1,
494     };
495   }
496   // These are intended to be extends for specific element heuristics.
497   insertNewAfter(
498     selection: RangeSelection,
499     restoreSelection?: boolean,
500   ): null | LexicalNode {
501     return null;
502   }
503   canIndent(): boolean {
504     return true;
505   }
506   /*
507    * This method controls the behavior of a the node during backwards
508    * deletion (i.e., backspace) when selection is at the beginning of
509    * the node (offset 0)
510    */
511   collapseAtStart(selection: RangeSelection): boolean {
512     return false;
513   }
514   excludeFromCopy(destination?: 'clone' | 'html'): boolean {
515     return false;
516   }
517   /** @deprecated @internal */
518   canReplaceWith(replacement: LexicalNode): boolean {
519     return true;
520   }
521   /** @deprecated @internal */
522   canInsertAfter(node: LexicalNode): boolean {
523     return true;
524   }
525   canBeEmpty(): boolean {
526     return true;
527   }
528   canInsertTextBefore(): boolean {
529     return true;
530   }
531   canInsertTextAfter(): boolean {
532     return true;
533   }
534   isInline(): boolean {
535     return false;
536   }
537   // A shadow root is a Node that behaves like RootNode. The shadow root (and RootNode) mark the
538   // end of the hiercharchy, most implementations should treat it as there's nothing (upwards)
539   // beyond this point. For example, node.getTopLevelElement(), when performed inside a TableCellNode
540   // will return the immediate first child underneath TableCellNode instead of RootNode.
541   isShadowRoot(): boolean {
542     return false;
543   }
544   /** @deprecated @internal */
545   canMergeWith(node: ElementNode): boolean {
546     return false;
547   }
548   extractWithChild(
549     child: LexicalNode,
550     selection: BaseSelection | null,
551     destination: 'clone' | 'html',
552   ): boolean {
553     return false;
554   }
555
556   /**
557    * Determines whether this node, when empty, can merge with a first block
558    * of nodes being inserted.
559    *
560    * This method is specifically called in {@link RangeSelection.insertNodes}
561    * to determine merging behavior during nodes insertion.
562    *
563    * @example
564    * // In a ListItemNode or QuoteNode implementation:
565    * canMergeWhenEmpty(): true {
566    *  return true;
567    * }
568    */
569   canMergeWhenEmpty(): boolean {
570     return false;
571   }
572 }
573
574 export function $isElementNode(
575   node: LexicalNode | null | undefined,
576 ): node is ElementNode {
577   return node instanceof ElementNode;
578 }
579
580 function isPointRemoved(
581   point: PointType,
582   nodesToRemoveKeySet: Set<NodeKey>,
583   nodesToInsertKeySet: Set<NodeKey>,
584 ): boolean {
585   let node: ElementNode | TextNode | null = point.getNode();
586   while (node) {
587     const nodeKey = node.__key;
588     if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
589       return true;
590     }
591     node = node.getParent();
592   }
593   return false;
594 }