]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
09d01bffd43f83f7a3252430152bcda3a13f650d
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalReconciler.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   LexicalEditor,
12   MutatedNodes,
13   MutationListeners,
14   RegisteredNodes,
15 } from './LexicalEditor';
16 import type {NodeKey, NodeMap} from './LexicalNode';
17 import type {ElementNode} from './nodes/LexicalElementNode';
18
19 import invariant from 'lexical/shared/invariant';
20 import normalizeClassNames from 'lexical/shared/normalizeClassNames';
21
22 import {
23   $isDecoratorNode,
24   $isElementNode,
25   $isLineBreakNode,
26   $isParagraphNode,
27   $isRootNode,
28   $isTextNode,
29 } from '.';
30 import {
31   DOUBLE_LINE_BREAK,
32   FULL_RECONCILE,
33   IS_ALIGN_CENTER,
34   IS_ALIGN_END,
35   IS_ALIGN_JUSTIFY,
36   IS_ALIGN_LEFT,
37   IS_ALIGN_RIGHT,
38   IS_ALIGN_START,
39 } from './LexicalConstants';
40 import {EditorState} from './LexicalEditorState';
41 import {
42   $textContentRequiresDoubleLinebreakAtEnd,
43   cloneDecorators,
44   getElementByKeyOrThrow,
45   setMutatedNode,
46 } from './LexicalUtils';
47
48 type IntentionallyMarkedAsDirtyElement = boolean;
49
50 let subTreeTextContent = '';
51 let subTreeTextFormat: number | null = null;
52 let subTreeTextStyle: string = '';
53 let editorTextContent = '';
54 let activeEditorConfig: EditorConfig;
55 let activeEditor: LexicalEditor;
56 let activeEditorNodes: RegisteredNodes;
57 let treatAllNodesAsDirty = false;
58 let activeEditorStateReadOnly = false;
59 let activeMutationListeners: MutationListeners;
60 let activeDirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
61 let activeDirtyLeaves: Set<NodeKey>;
62 let activePrevNodeMap: NodeMap;
63 let activeNextNodeMap: NodeMap;
64 let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
65 let mutatedNodes: MutatedNodes;
66
67 function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
68   const node = activePrevNodeMap.get(key);
69
70   if (parentDOM !== null) {
71     const dom = getPrevElementByKeyOrThrow(key);
72     if (dom.parentNode === parentDOM) {
73       parentDOM.removeChild(dom);
74     }
75   }
76
77   // This logic is really important, otherwise we will leak DOM nodes
78   // when their corresponding LexicalNodes are removed from the editor state.
79   if (!activeNextNodeMap.has(key)) {
80     activeEditor._keyToDOMMap.delete(key);
81   }
82
83   if ($isElementNode(node)) {
84     const children = createChildrenArray(node, activePrevNodeMap);
85     destroyChildren(children, 0, children.length - 1, null);
86   }
87
88   if (node !== undefined) {
89     setMutatedNode(
90       mutatedNodes,
91       activeEditorNodes,
92       activeMutationListeners,
93       node,
94       'destroyed',
95     );
96   }
97 }
98
99 function destroyChildren(
100   children: Array<NodeKey>,
101   _startIndex: number,
102   endIndex: number,
103   dom: null | HTMLElement,
104 ): void {
105   let startIndex = _startIndex;
106
107   for (; startIndex <= endIndex; ++startIndex) {
108     const child = children[startIndex];
109
110     if (child !== undefined) {
111       destroyNode(child, dom);
112     }
113   }
114 }
115
116 function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
117   domStyle.setProperty('text-align', value);
118 }
119
120 const DEFAULT_INDENT_VALUE = '40px';
121
122 function setElementIndent(dom: HTMLElement, indent: number): void {
123   const indentClassName = activeEditorConfig.theme.indent;
124
125   if (typeof indentClassName === 'string') {
126     const elementHasClassName = dom.classList.contains(indentClassName);
127
128     if (indent > 0 && !elementHasClassName) {
129       dom.classList.add(indentClassName);
130     } else if (indent < 1 && elementHasClassName) {
131       dom.classList.remove(indentClassName);
132     }
133   }
134
135   const indentationBaseValue =
136     getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') ||
137     DEFAULT_INDENT_VALUE;
138
139   dom.style.setProperty(
140     'padding-inline-start',
141     indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`,
142   );
143 }
144
145 function setElementFormat(dom: HTMLElement, format: number): void {
146   const domStyle = dom.style;
147
148   if (format === 0) {
149     setTextAlign(domStyle, '');
150   } else if (format === IS_ALIGN_LEFT) {
151     setTextAlign(domStyle, 'left');
152   } else if (format === IS_ALIGN_CENTER) {
153     setTextAlign(domStyle, 'center');
154   } else if (format === IS_ALIGN_RIGHT) {
155     setTextAlign(domStyle, 'right');
156   } else if (format === IS_ALIGN_JUSTIFY) {
157     setTextAlign(domStyle, 'justify');
158   } else if (format === IS_ALIGN_START) {
159     setTextAlign(domStyle, 'start');
160   } else if (format === IS_ALIGN_END) {
161     setTextAlign(domStyle, 'end');
162   }
163 }
164
165 function $createNode(
166   key: NodeKey,
167   parentDOM: null | HTMLElement,
168   insertDOM: null | Node,
169 ): HTMLElement {
170   const node = activeNextNodeMap.get(key);
171
172   if (node === undefined) {
173     invariant(false, 'createNode: node does not exist in nodeMap');
174   }
175   const dom = node.createDOM(activeEditorConfig, activeEditor);
176   storeDOMWithKey(key, dom, activeEditor);
177
178   // This helps preserve the text, and stops spell check tools from
179   // merging or break the spans (which happens if they are missing
180   // this attribute).
181   if ($isTextNode(node)) {
182     dom.setAttribute('data-lexical-text', 'true');
183   } else if ($isDecoratorNode(node)) {
184     dom.setAttribute('data-lexical-decorator', 'true');
185   }
186
187   if ($isElementNode(node)) {
188     const indent = node.__indent;
189     const childrenSize = node.__size;
190
191     if (indent !== 0) {
192       setElementIndent(dom, indent);
193     }
194     if (childrenSize !== 0) {
195       const endIndex = childrenSize - 1;
196       const children = createChildrenArray(node, activeNextNodeMap);
197       $createChildren(children, node, 0, endIndex, dom, null);
198     }
199     const format = node.__format;
200
201     if (format !== 0) {
202       setElementFormat(dom, format);
203     }
204     if (!node.isInline()) {
205       reconcileElementTerminatingLineBreak(null, node, dom);
206     }
207     if ($textContentRequiresDoubleLinebreakAtEnd(node)) {
208       subTreeTextContent += DOUBLE_LINE_BREAK;
209       editorTextContent += DOUBLE_LINE_BREAK;
210     }
211   } else {
212     const text = node.getTextContent();
213
214     if ($isDecoratorNode(node)) {
215       const decorator = node.decorate(activeEditor, activeEditorConfig);
216
217       if (decorator !== null) {
218         reconcileDecorator(key, decorator);
219       }
220       // Decorators are always non editable
221       dom.contentEditable = 'false';
222     }
223     subTreeTextContent += text;
224     editorTextContent += text;
225   }
226
227   if (parentDOM !== null) {
228     if (insertDOM != null) {
229       parentDOM.insertBefore(dom, insertDOM);
230     } else {
231       // @ts-expect-error: internal field
232       const possibleLineBreak = parentDOM.__lexicalLineBreak;
233
234       if (possibleLineBreak != null) {
235         parentDOM.insertBefore(dom, possibleLineBreak);
236       } else {
237         parentDOM.appendChild(dom);
238       }
239     }
240   }
241
242   if (__DEV__) {
243     // Freeze the node in DEV to prevent accidental mutations
244     Object.freeze(node);
245   }
246
247   setMutatedNode(
248     mutatedNodes,
249     activeEditorNodes,
250     activeMutationListeners,
251     node,
252     'created',
253   );
254   return dom;
255 }
256
257 function $createChildren(
258   children: Array<NodeKey>,
259   element: ElementNode,
260   _startIndex: number,
261   endIndex: number,
262   dom: null | HTMLElement,
263   insertDOM: null | HTMLElement,
264 ): void {
265   const previousSubTreeTextContent = subTreeTextContent;
266   subTreeTextContent = '';
267   let startIndex = _startIndex;
268
269   for (; startIndex <= endIndex; ++startIndex) {
270     $createNode(children[startIndex], dom, insertDOM);
271     const node = activeNextNodeMap.get(children[startIndex]);
272     if (node !== null && $isTextNode(node)) {
273       if (subTreeTextFormat === null) {
274         subTreeTextFormat = node.getFormat();
275       }
276       if (subTreeTextStyle === '') {
277         subTreeTextStyle = node.getStyle();
278       }
279     }
280   }
281   if ($textContentRequiresDoubleLinebreakAtEnd(element)) {
282     subTreeTextContent += DOUBLE_LINE_BREAK;
283   }
284   // @ts-expect-error: internal field
285   dom.__lexicalTextContent = subTreeTextContent;
286   subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
287 }
288
289 function isLastChildLineBreakOrDecorator(
290   childKey: NodeKey,
291   nodeMap: NodeMap,
292 ): boolean {
293   const node = nodeMap.get(childKey);
294   return $isLineBreakNode(node) || ($isDecoratorNode(node) && node.isInline());
295 }
296
297 // If we end an element with a LineBreakNode, then we need to add an additional <br>
298 function reconcileElementTerminatingLineBreak(
299   prevElement: null | ElementNode,
300   nextElement: ElementNode,
301   dom: HTMLElement,
302 ): void {
303   const prevLineBreak =
304     prevElement !== null &&
305     (prevElement.__size === 0 ||
306       isLastChildLineBreakOrDecorator(
307         prevElement.__last as NodeKey,
308         activePrevNodeMap,
309       ));
310   const nextLineBreak =
311     nextElement.__size === 0 ||
312     isLastChildLineBreakOrDecorator(
313       nextElement.__last as NodeKey,
314       activeNextNodeMap,
315     );
316
317   if (prevLineBreak) {
318     if (!nextLineBreak) {
319       // @ts-expect-error: internal field
320       const element = dom.__lexicalLineBreak;
321
322       if (element != null) {
323         try {
324           dom.removeChild(element);
325         } catch (error) {
326           if (typeof error === 'object' && error != null) {
327             const msg = `${error.toString()} Parent: ${dom.tagName}, child: ${
328               element.tagName
329             }.`;
330             throw new Error(msg);
331           } else {
332             throw error;
333           }
334         }
335       }
336
337       // @ts-expect-error: internal field
338       dom.__lexicalLineBreak = null;
339     }
340   } else if (nextLineBreak) {
341     const element = document.createElement('br');
342     // @ts-expect-error: internal field
343     dom.__lexicalLineBreak = element;
344     dom.appendChild(element);
345   }
346 }
347
348 function reconcileParagraphFormat(element: ElementNode): void {
349   if (
350     $isParagraphNode(element) &&
351     subTreeTextFormat != null &&
352     subTreeTextFormat !== element.__textFormat &&
353     !activeEditorStateReadOnly
354   ) {
355     element.setTextFormat(subTreeTextFormat);
356     element.setTextStyle(subTreeTextStyle);
357   }
358 }
359
360 function reconcileParagraphStyle(element: ElementNode): void {
361   if (
362     $isParagraphNode(element) &&
363     subTreeTextStyle !== '' &&
364     subTreeTextStyle !== element.__textStyle &&
365     !activeEditorStateReadOnly
366   ) {
367     element.setTextStyle(subTreeTextStyle);
368   }
369 }
370
371 function $reconcileChildrenWithDirection(
372   prevElement: ElementNode,
373   nextElement: ElementNode,
374   dom: HTMLElement,
375 ): void {
376   subTreeTextFormat = null;
377   subTreeTextStyle = '';
378   $reconcileChildren(prevElement, nextElement, dom);
379   reconcileParagraphFormat(nextElement);
380   reconcileParagraphStyle(nextElement);
381 }
382
383 function createChildrenArray(
384   element: ElementNode,
385   nodeMap: NodeMap,
386 ): Array<NodeKey> {
387   const children = [];
388   let nodeKey = element.__first;
389   while (nodeKey !== null) {
390     const node = nodeMap.get(nodeKey);
391     if (node === undefined) {
392       invariant(false, 'createChildrenArray: node does not exist in nodeMap');
393     }
394     children.push(nodeKey);
395     nodeKey = node.__next;
396   }
397   return children;
398 }
399
400 function $reconcileChildren(
401   prevElement: ElementNode,
402   nextElement: ElementNode,
403   dom: HTMLElement,
404 ): void {
405   const previousSubTreeTextContent = subTreeTextContent;
406   const prevChildrenSize = prevElement.__size;
407   const nextChildrenSize = nextElement.__size;
408   subTreeTextContent = '';
409
410   if (prevChildrenSize === 1 && nextChildrenSize === 1) {
411     const prevFirstChildKey = prevElement.__first as NodeKey;
412     const nextFrstChildKey = nextElement.__first as NodeKey;
413     if (prevFirstChildKey === nextFrstChildKey) {
414       $reconcileNode(prevFirstChildKey, dom);
415     } else {
416       const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);
417       const replacementDOM = $createNode(nextFrstChildKey, null, null);
418       try {
419         dom.replaceChild(replacementDOM, lastDOM);
420       } catch (error) {
421         if (typeof error === 'object' && error != null) {
422           const msg = `${error.toString()} Parent: ${
423             dom.tagName
424           }, new child: {tag: ${
425             replacementDOM.tagName
426           } key: ${nextFrstChildKey}}, old child: {tag: ${
427             lastDOM.tagName
428           }, key: ${prevFirstChildKey}}.`;
429           throw new Error(msg);
430         } else {
431           throw error;
432         }
433       }
434       destroyNode(prevFirstChildKey, null);
435     }
436     const nextChildNode = activeNextNodeMap.get(nextFrstChildKey);
437     if ($isTextNode(nextChildNode)) {
438       if (subTreeTextFormat === null) {
439         subTreeTextFormat = nextChildNode.getFormat();
440       }
441       if (subTreeTextStyle === '') {
442         subTreeTextStyle = nextChildNode.getStyle();
443       }
444     }
445   } else {
446     const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);
447     const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);
448
449     if (prevChildrenSize === 0) {
450       if (nextChildrenSize !== 0) {
451         $createChildren(
452           nextChildren,
453           nextElement,
454           0,
455           nextChildrenSize - 1,
456           dom,
457           null,
458         );
459       }
460     } else if (nextChildrenSize === 0) {
461       if (prevChildrenSize !== 0) {
462         // @ts-expect-error: internal field
463         const lexicalLineBreak = dom.__lexicalLineBreak;
464         const canUseFastPath = lexicalLineBreak == null;
465         destroyChildren(
466           prevChildren,
467           0,
468           prevChildrenSize - 1,
469           canUseFastPath ? null : dom,
470         );
471
472         if (canUseFastPath) {
473           // Fast path for removing DOM nodes
474           dom.textContent = '';
475         }
476       }
477     } else {
478       $reconcileNodeChildren(
479         nextElement,
480         prevChildren,
481         nextChildren,
482         prevChildrenSize,
483         nextChildrenSize,
484         dom,
485       );
486     }
487   }
488
489   if ($textContentRequiresDoubleLinebreakAtEnd(nextElement)) {
490     subTreeTextContent += DOUBLE_LINE_BREAK;
491   }
492
493   // @ts-expect-error: internal field
494   dom.__lexicalTextContent = subTreeTextContent;
495   subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
496 }
497
498 function $reconcileNode(
499   key: NodeKey,
500   parentDOM: HTMLElement | null,
501 ): HTMLElement {
502   const prevNode = activePrevNodeMap.get(key);
503   let nextNode = activeNextNodeMap.get(key);
504
505   if (prevNode === undefined || nextNode === undefined) {
506     invariant(
507       false,
508       'reconcileNode: prevNode or nextNode does not exist in nodeMap',
509     );
510   }
511
512   const isDirty =
513     treatAllNodesAsDirty ||
514     activeDirtyLeaves.has(key) ||
515     activeDirtyElements.has(key);
516   const dom = getElementByKeyOrThrow(activeEditor, key);
517
518   // If the node key points to the same instance in both states
519   // and isn't dirty, we just update the text content cache
520   // and return the existing DOM Node.
521   if (prevNode === nextNode && !isDirty) {
522     if ($isElementNode(prevNode)) {
523       // @ts-expect-error: internal field
524       const previousSubTreeTextContent = dom.__lexicalTextContent;
525
526       if (previousSubTreeTextContent !== undefined) {
527         subTreeTextContent += previousSubTreeTextContent;
528         editorTextContent += previousSubTreeTextContent;
529       }
530     } else {
531       const text = prevNode.getTextContent();
532
533       editorTextContent += text;
534       subTreeTextContent += text;
535     }
536
537     return dom;
538   }
539   // If the node key doesn't point to the same instance in both maps,
540   // it means it were cloned. If they're also dirty, we mark them as mutated.
541   if (prevNode !== nextNode && isDirty) {
542     setMutatedNode(
543       mutatedNodes,
544       activeEditorNodes,
545       activeMutationListeners,
546       nextNode,
547       'updated',
548     );
549   }
550
551   // Update node. If it returns true, we need to unmount and re-create the node
552   if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) {
553     const replacementDOM = $createNode(key, null, null);
554
555     if (parentDOM === null) {
556       invariant(false, 'reconcileNode: parentDOM is null');
557     }
558
559     parentDOM.replaceChild(replacementDOM, dom);
560     destroyNode(key, null);
561     return replacementDOM;
562   }
563
564   if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
565     // Reconcile element children
566     const nextIndent = nextNode.__indent;
567
568     if (nextIndent !== prevNode.__indent) {
569       setElementIndent(dom, nextIndent);
570     }
571
572     const nextFormat = nextNode.__format;
573
574     if (nextFormat !== prevNode.__format) {
575       setElementFormat(dom, nextFormat);
576     }
577     if (isDirty) {
578       $reconcileChildrenWithDirection(prevNode, nextNode, dom);
579       if (!$isRootNode(nextNode) && !nextNode.isInline()) {
580         reconcileElementTerminatingLineBreak(prevNode, nextNode, dom);
581       }
582     }
583
584     if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {
585       subTreeTextContent += DOUBLE_LINE_BREAK;
586       editorTextContent += DOUBLE_LINE_BREAK;
587     }
588   } else {
589     const text = nextNode.getTextContent();
590
591     if ($isDecoratorNode(nextNode)) {
592       const decorator = nextNode.decorate(activeEditor, activeEditorConfig);
593
594       if (decorator !== null) {
595         reconcileDecorator(key, decorator);
596       }
597     }
598
599     subTreeTextContent += text;
600     editorTextContent += text;
601   }
602
603   if (
604     !activeEditorStateReadOnly &&
605     $isRootNode(nextNode) &&
606     nextNode.__cachedText !== editorTextContent
607   ) {
608     // Cache the latest text content.
609     const nextRootNode = nextNode.getWritable();
610     nextRootNode.__cachedText = editorTextContent;
611     nextNode = nextRootNode;
612   }
613
614   if (__DEV__) {
615     // Freeze the node in DEV to prevent accidental mutations
616     Object.freeze(nextNode);
617   }
618
619   return dom;
620 }
621
622 function reconcileDecorator(key: NodeKey, decorator: unknown): void {
623   let pendingDecorators = activeEditor._pendingDecorators;
624   const currentDecorators = activeEditor._decorators;
625
626   if (pendingDecorators === null) {
627     if (currentDecorators[key] === decorator) {
628       return;
629     }
630
631     pendingDecorators = cloneDecorators(activeEditor);
632   }
633
634   pendingDecorators[key] = decorator;
635 }
636
637 function getFirstChild(element: HTMLElement): Node | null {
638   return element.firstChild;
639 }
640
641 function getNextSibling(element: HTMLElement): Node | null {
642   let nextSibling = element.nextSibling;
643   if (
644     nextSibling !== null &&
645     nextSibling === activeEditor._blockCursorElement
646   ) {
647     nextSibling = nextSibling.nextSibling;
648   }
649   return nextSibling;
650 }
651
652 function $reconcileNodeChildren(
653   nextElement: ElementNode,
654   prevChildren: Array<NodeKey>,
655   nextChildren: Array<NodeKey>,
656   prevChildrenLength: number,
657   nextChildrenLength: number,
658   dom: HTMLElement,
659 ): void {
660   const prevEndIndex = prevChildrenLength - 1;
661   const nextEndIndex = nextChildrenLength - 1;
662   let prevChildrenSet: Set<NodeKey> | undefined;
663   let nextChildrenSet: Set<NodeKey> | undefined;
664   let siblingDOM: null | Node = getFirstChild(dom);
665   let prevIndex = 0;
666   let nextIndex = 0;
667
668   while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
669     const prevKey = prevChildren[prevIndex];
670     const nextKey = nextChildren[nextIndex];
671
672     if (prevKey === nextKey) {
673       siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
674       prevIndex++;
675       nextIndex++;
676     } else {
677       if (prevChildrenSet === undefined) {
678         prevChildrenSet = new Set(prevChildren);
679       }
680
681       if (nextChildrenSet === undefined) {
682         nextChildrenSet = new Set(nextChildren);
683       }
684
685       const nextHasPrevKey = nextChildrenSet.has(prevKey);
686       const prevHasNextKey = prevChildrenSet.has(nextKey);
687
688       if (!nextHasPrevKey) {
689         // Remove prev
690         siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey));
691         destroyNode(prevKey, dom);
692         prevIndex++;
693       } else if (!prevHasNextKey) {
694         // Create next
695         $createNode(nextKey, dom, siblingDOM);
696         nextIndex++;
697       } else {
698         // Move next
699         const childDOM = getElementByKeyOrThrow(activeEditor, nextKey);
700
701         if (childDOM === siblingDOM) {
702           siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
703         } else {
704           if (siblingDOM != null) {
705             dom.insertBefore(childDOM, siblingDOM);
706           } else {
707             dom.appendChild(childDOM);
708           }
709
710           $reconcileNode(nextKey, dom);
711         }
712
713         prevIndex++;
714         nextIndex++;
715       }
716     }
717
718     const node = activeNextNodeMap.get(nextKey);
719     if (node !== null && $isTextNode(node)) {
720       if (subTreeTextFormat === null) {
721         subTreeTextFormat = node.getFormat();
722       }
723       if (subTreeTextStyle === '') {
724         subTreeTextStyle = node.getStyle();
725       }
726     }
727   }
728
729   const appendNewChildren = prevIndex > prevEndIndex;
730   const removeOldChildren = nextIndex > nextEndIndex;
731
732   if (appendNewChildren && !removeOldChildren) {
733     const previousNode = nextChildren[nextEndIndex + 1];
734     const insertDOM =
735       previousNode === undefined
736         ? null
737         : activeEditor.getElementByKey(previousNode);
738     $createChildren(
739       nextChildren,
740       nextElement,
741       nextIndex,
742       nextEndIndex,
743       dom,
744       insertDOM,
745     );
746   } else if (removeOldChildren && !appendNewChildren) {
747     destroyChildren(prevChildren, prevIndex, prevEndIndex, dom);
748   }
749 }
750
751 export function $reconcileRoot(
752   prevEditorState: EditorState,
753   nextEditorState: EditorState,
754   editor: LexicalEditor,
755   dirtyType: 0 | 1 | 2,
756   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
757   dirtyLeaves: Set<NodeKey>,
758 ): MutatedNodes {
759   // We cache text content to make retrieval more efficient.
760   // The cache must be rebuilt during reconciliation to account for any changes.
761   subTreeTextContent = '';
762   editorTextContent = '';
763   // Rather than pass around a load of arguments through the stack recursively
764   // we instead set them as bindings within the scope of the module.
765   treatAllNodesAsDirty = dirtyType === FULL_RECONCILE;
766   activeEditor = editor;
767   activeEditorConfig = editor._config;
768   activeEditorNodes = editor._nodes;
769   activeMutationListeners = activeEditor._listeners.mutation;
770   activeDirtyElements = dirtyElements;
771   activeDirtyLeaves = dirtyLeaves;
772   activePrevNodeMap = prevEditorState._nodeMap;
773   activeNextNodeMap = nextEditorState._nodeMap;
774   activeEditorStateReadOnly = nextEditorState._readOnly;
775   activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
776   // We keep track of mutated nodes so we can trigger mutation
777   // listeners later in the update cycle.
778   const currentMutatedNodes = new Map();
779   mutatedNodes = currentMutatedNodes;
780   $reconcileNode('root', null);
781   // We don't want a bunch of void checks throughout the scope
782   // so instead we make it seem that these values are always set.
783   // We also want to make sure we clear them down, otherwise we
784   // can leak memory.
785   // @ts-ignore
786   activeEditor = undefined;
787   // @ts-ignore
788   activeEditorNodes = undefined;
789   // @ts-ignore
790   activeDirtyElements = undefined;
791   // @ts-ignore
792   activeDirtyLeaves = undefined;
793   // @ts-ignore
794   activePrevNodeMap = undefined;
795   // @ts-ignore
796   activeNextNodeMap = undefined;
797   // @ts-ignore
798   activeEditorConfig = undefined;
799   // @ts-ignore
800   activePrevKeyToDOMMap = undefined;
801   // @ts-ignore
802   mutatedNodes = undefined;
803
804   return currentMutatedNodes;
805 }
806
807 export function storeDOMWithKey(
808   key: NodeKey,
809   dom: HTMLElement,
810   editor: LexicalEditor,
811 ): void {
812   const keyToDOMMap = editor._keyToDOMMap;
813   // @ts-ignore We intentionally add this to the Node.
814   dom['__lexicalKey_' + editor._key] = key;
815   keyToDOMMap.set(key, dom);
816 }
817
818 function getPrevElementByKeyOrThrow(key: NodeKey): HTMLElement {
819   const element = activePrevKeyToDOMMap.get(key);
820
821   if (element === undefined) {
822     invariant(
823       false,
824       'Reconciliation: could not find DOM element for node key %s',
825       key,
826     );
827   }
828
829   return element;
830 }