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