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