2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
15 } from './LexicalEditor';
16 import type {NodeKey, NodeMap} from './LexicalNode';
17 import type {ElementNode} from './nodes/LexicalElementNode';
19 import invariant from 'lexical/shared/invariant';
20 import normalizeClassNames from 'lexical/shared/normalizeClassNames';
39 } from './LexicalConstants';
40 import {EditorState} from './LexicalEditorState';
42 $textContentRequiresDoubleLinebreakAtEnd,
44 getElementByKeyOrThrow,
47 } from './LexicalUtils';
49 type IntentionallyMarkedAsDirtyElement = boolean;
51 let subTreeTextContent = '';
52 let subTreeDirectionedTextContent = '';
53 let subTreeTextFormat: number | null = null;
54 let subTreeTextStyle: string = '';
55 let editorTextContent = '';
56 let activeEditorConfig: EditorConfig;
57 let activeEditor: LexicalEditor;
58 let activeEditorNodes: RegisteredNodes;
59 let treatAllNodesAsDirty = false;
60 let activeEditorStateReadOnly = false;
61 let activeMutationListeners: MutationListeners;
62 let activeTextDirection: 'ltr' | 'rtl' | null = null;
63 let activeDirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
64 let activeDirtyLeaves: Set<NodeKey>;
65 let activePrevNodeMap: NodeMap;
66 let activeNextNodeMap: NodeMap;
67 let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
68 let mutatedNodes: MutatedNodes;
70 function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
71 const node = activePrevNodeMap.get(key);
73 if (parentDOM !== null) {
74 const dom = getPrevElementByKeyOrThrow(key);
75 if (dom.parentNode === parentDOM) {
76 parentDOM.removeChild(dom);
80 // This logic is really important, otherwise we will leak DOM nodes
81 // when their corresponding LexicalNodes are removed from the editor state.
82 if (!activeNextNodeMap.has(key)) {
83 activeEditor._keyToDOMMap.delete(key);
86 if ($isElementNode(node)) {
87 const children = createChildrenArray(node, activePrevNodeMap);
88 destroyChildren(children, 0, children.length - 1, null);
91 if (node !== undefined) {
95 activeMutationListeners,
102 function destroyChildren(
103 children: Array<NodeKey>,
106 dom: null | HTMLElement,
108 let startIndex = _startIndex;
110 for (; startIndex <= endIndex; ++startIndex) {
111 const child = children[startIndex];
113 if (child !== undefined) {
114 destroyNode(child, dom);
119 function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
120 domStyle.setProperty('text-align', value);
123 const DEFAULT_INDENT_VALUE = '40px';
125 function setElementIndent(dom: HTMLElement, indent: number): void {
126 const indentClassName = activeEditorConfig.theme.indent;
128 if (typeof indentClassName === 'string') {
129 const elementHasClassName = dom.classList.contains(indentClassName);
131 if (indent > 0 && !elementHasClassName) {
132 dom.classList.add(indentClassName);
133 } else if (indent < 1 && elementHasClassName) {
134 dom.classList.remove(indentClassName);
138 const indentationBaseValue =
139 getComputedStyle(dom).getPropertyValue('--lexical-indent-base-value') ||
140 DEFAULT_INDENT_VALUE;
142 dom.style.setProperty(
143 'padding-inline-start',
144 indent === 0 ? '' : `calc(${indent} * ${indentationBaseValue})`,
148 function setElementFormat(dom: HTMLElement, format: number): void {
149 const domStyle = dom.style;
152 setTextAlign(domStyle, '');
153 } else if (format === IS_ALIGN_LEFT) {
154 setTextAlign(domStyle, 'left');
155 } else if (format === IS_ALIGN_CENTER) {
156 setTextAlign(domStyle, 'center');
157 } else if (format === IS_ALIGN_RIGHT) {
158 setTextAlign(domStyle, 'right');
159 } else if (format === IS_ALIGN_JUSTIFY) {
160 setTextAlign(domStyle, 'justify');
161 } else if (format === IS_ALIGN_START) {
162 setTextAlign(domStyle, 'start');
163 } else if (format === IS_ALIGN_END) {
164 setTextAlign(domStyle, 'end');
168 function $createNode(
170 parentDOM: null | HTMLElement,
171 insertDOM: null | Node,
173 const node = activeNextNodeMap.get(key);
175 if (node === undefined) {
176 invariant(false, 'createNode: node does not exist in nodeMap');
178 const dom = node.createDOM(activeEditorConfig, activeEditor);
179 storeDOMWithKey(key, dom, activeEditor);
181 // This helps preserve the text, and stops spell check tools from
182 // merging or break the spans (which happens if they are missing
184 if ($isTextNode(node)) {
185 dom.setAttribute('data-lexical-text', 'true');
186 } else if ($isDecoratorNode(node)) {
187 dom.setAttribute('data-lexical-decorator', 'true');
190 if ($isElementNode(node)) {
191 const indent = node.__indent;
192 const childrenSize = node.__size;
195 setElementIndent(dom, indent);
197 if (childrenSize !== 0) {
198 const endIndex = childrenSize - 1;
199 const children = createChildrenArray(node, activeNextNodeMap);
200 $createChildrenWithDirection(children, endIndex, node, dom);
202 const format = node.__format;
205 setElementFormat(dom, format);
207 if (!node.isInline()) {
208 reconcileElementTerminatingLineBreak(null, node, dom);
210 if ($textContentRequiresDoubleLinebreakAtEnd(node)) {
211 subTreeTextContent += DOUBLE_LINE_BREAK;
212 editorTextContent += DOUBLE_LINE_BREAK;
215 const text = node.getTextContent();
217 if ($isDecoratorNode(node)) {
218 const decorator = node.decorate(activeEditor, activeEditorConfig);
220 if (decorator !== null) {
221 reconcileDecorator(key, decorator);
223 // Decorators are always non editable
224 dom.contentEditable = 'false';
225 } else if ($isTextNode(node)) {
226 if (!node.isDirectionless()) {
227 subTreeDirectionedTextContent += text;
230 subTreeTextContent += text;
231 editorTextContent += text;
234 if (parentDOM !== null) {
235 if (insertDOM != null) {
236 parentDOM.insertBefore(dom, insertDOM);
238 // @ts-expect-error: internal field
239 const possibleLineBreak = parentDOM.__lexicalLineBreak;
241 if (possibleLineBreak != null) {
242 parentDOM.insertBefore(dom, possibleLineBreak);
244 parentDOM.appendChild(dom);
250 // Freeze the node in DEV to prevent accidental mutations
257 activeMutationListeners,
264 function $createChildrenWithDirection(
265 children: Array<NodeKey>,
267 element: ElementNode,
270 const previousSubTreeDirectionedTextContent = subTreeDirectionedTextContent;
271 subTreeDirectionedTextContent = '';
272 $createChildren(children, element, 0, endIndex, dom, null);
273 reconcileBlockDirection(element, dom);
274 subTreeDirectionedTextContent = previousSubTreeDirectionedTextContent;
277 function $createChildren(
278 children: Array<NodeKey>,
279 element: ElementNode,
282 dom: null | HTMLElement,
283 insertDOM: null | HTMLElement,
285 const previousSubTreeTextContent = subTreeTextContent;
286 subTreeTextContent = '';
287 let startIndex = _startIndex;
289 for (; startIndex <= endIndex; ++startIndex) {
290 $createNode(children[startIndex], dom, insertDOM);
291 const node = activeNextNodeMap.get(children[startIndex]);
292 if (node !== null && $isTextNode(node)) {
293 if (subTreeTextFormat === null) {
294 subTreeTextFormat = node.getFormat();
296 if (subTreeTextStyle === '') {
297 subTreeTextStyle = node.getStyle();
301 if ($textContentRequiresDoubleLinebreakAtEnd(element)) {
302 subTreeTextContent += DOUBLE_LINE_BREAK;
304 // @ts-expect-error: internal field
305 dom.__lexicalTextContent = subTreeTextContent;
306 subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
309 function isLastChildLineBreakOrDecorator(
313 const node = nodeMap.get(childKey);
314 return $isLineBreakNode(node) || ($isDecoratorNode(node) && node.isInline());
317 // If we end an element with a LineBreakNode, then we need to add an additional <br>
318 function reconcileElementTerminatingLineBreak(
319 prevElement: null | ElementNode,
320 nextElement: ElementNode,
323 const prevLineBreak =
324 prevElement !== null &&
325 (prevElement.__size === 0 ||
326 isLastChildLineBreakOrDecorator(
327 prevElement.__last as NodeKey,
330 const nextLineBreak =
331 nextElement.__size === 0 ||
332 isLastChildLineBreakOrDecorator(
333 nextElement.__last as NodeKey,
338 if (!nextLineBreak) {
339 // @ts-expect-error: internal field
340 const element = dom.__lexicalLineBreak;
342 if (element != null) {
344 dom.removeChild(element);
346 if (typeof error === 'object' && error != null) {
347 const msg = `${error.toString()} Parent: ${dom.tagName}, child: ${
350 throw new Error(msg);
357 // @ts-expect-error: internal field
358 dom.__lexicalLineBreak = null;
360 } else if (nextLineBreak) {
361 const element = document.createElement('br');
362 // @ts-expect-error: internal field
363 dom.__lexicalLineBreak = element;
364 dom.appendChild(element);
368 function reconcileParagraphFormat(element: ElementNode): void {
370 $isParagraphNode(element) &&
371 subTreeTextFormat != null &&
372 subTreeTextFormat !== element.__textFormat &&
373 !activeEditorStateReadOnly
375 element.setTextFormat(subTreeTextFormat);
376 element.setTextStyle(subTreeTextStyle);
380 function reconcileParagraphStyle(element: ElementNode): void {
382 $isParagraphNode(element) &&
383 subTreeTextStyle !== '' &&
384 subTreeTextStyle !== element.__textStyle &&
385 !activeEditorStateReadOnly
387 element.setTextStyle(subTreeTextStyle);
391 function reconcileBlockDirection(element: ElementNode, dom: HTMLElement): void {
392 const previousSubTreeDirectionTextContent: string =
393 // @ts-expect-error: internal field
394 dom.__lexicalDirTextContent;
395 // @ts-expect-error: internal field
396 const previousDirection: string = dom.__lexicalDir;
399 previousSubTreeDirectionTextContent !== subTreeDirectionedTextContent ||
400 previousDirection !== activeTextDirection
402 const hasEmptyDirectionedTextContent = subTreeDirectionedTextContent === '';
403 const direction = hasEmptyDirectionedTextContent
404 ? activeTextDirection
405 : getTextDirection(subTreeDirectionedTextContent);
407 if (direction !== previousDirection) {
408 const classList = dom.classList;
409 const theme = activeEditorConfig.theme;
410 let previousDirectionTheme =
411 previousDirection !== null ? theme[previousDirection] : undefined;
412 let nextDirectionTheme =
413 direction !== null ? theme[direction] : undefined;
415 // Remove the old theme classes if they exist
416 if (previousDirectionTheme !== undefined) {
417 if (typeof previousDirectionTheme === 'string') {
418 const classNamesArr = normalizeClassNames(previousDirectionTheme);
419 previousDirectionTheme = theme[previousDirection] = classNamesArr;
422 // @ts-ignore: intentional
423 classList.remove(...previousDirectionTheme);
427 direction === null ||
428 (hasEmptyDirectionedTextContent && direction === 'ltr')
431 dom.removeAttribute('dir');
433 // Apply the new theme classes if they exist
434 if (nextDirectionTheme !== undefined) {
435 if (typeof nextDirectionTheme === 'string') {
436 const classNamesArr = normalizeClassNames(nextDirectionTheme);
437 // @ts-expect-error: intentional
438 nextDirectionTheme = theme[direction] = classNamesArr;
441 if (nextDirectionTheme !== undefined) {
442 classList.add(...nextDirectionTheme);
450 if (!activeEditorStateReadOnly) {
451 const writableNode = element.getWritable();
452 writableNode.__dir = direction;
456 activeTextDirection = direction;
457 // @ts-expect-error: internal field
458 dom.__lexicalDirTextContent = subTreeDirectionedTextContent;
459 // @ts-expect-error: internal field
460 dom.__lexicalDir = direction;
464 function $reconcileChildrenWithDirection(
465 prevElement: ElementNode,
466 nextElement: ElementNode,
469 const previousSubTreeDirectionTextContent = subTreeDirectionedTextContent;
470 subTreeDirectionedTextContent = '';
471 subTreeTextFormat = null;
472 subTreeTextStyle = '';
473 $reconcileChildren(prevElement, nextElement, dom);
474 reconcileBlockDirection(nextElement, dom);
475 reconcileParagraphFormat(nextElement);
476 reconcileParagraphStyle(nextElement);
477 subTreeDirectionedTextContent = previousSubTreeDirectionTextContent;
480 function createChildrenArray(
481 element: ElementNode,
485 let nodeKey = element.__first;
486 while (nodeKey !== null) {
487 const node = nodeMap.get(nodeKey);
488 if (node === undefined) {
489 invariant(false, 'createChildrenArray: node does not exist in nodeMap');
491 children.push(nodeKey);
492 nodeKey = node.__next;
497 function $reconcileChildren(
498 prevElement: ElementNode,
499 nextElement: ElementNode,
502 const previousSubTreeTextContent = subTreeTextContent;
503 const prevChildrenSize = prevElement.__size;
504 const nextChildrenSize = nextElement.__size;
505 subTreeTextContent = '';
507 if (prevChildrenSize === 1 && nextChildrenSize === 1) {
508 const prevFirstChildKey = prevElement.__first as NodeKey;
509 const nextFrstChildKey = nextElement.__first as NodeKey;
510 if (prevFirstChildKey === nextFrstChildKey) {
511 $reconcileNode(prevFirstChildKey, dom);
513 const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);
514 const replacementDOM = $createNode(nextFrstChildKey, null, null);
516 dom.replaceChild(replacementDOM, lastDOM);
518 if (typeof error === 'object' && error != null) {
519 const msg = `${error.toString()} Parent: ${
521 }, new child: {tag: ${
522 replacementDOM.tagName
523 } key: ${nextFrstChildKey}}, old child: {tag: ${
525 }, key: ${prevFirstChildKey}}.`;
526 throw new Error(msg);
531 destroyNode(prevFirstChildKey, null);
533 const nextChildNode = activeNextNodeMap.get(nextFrstChildKey);
534 if ($isTextNode(nextChildNode)) {
535 if (subTreeTextFormat === null) {
536 subTreeTextFormat = nextChildNode.getFormat();
538 if (subTreeTextStyle === '') {
539 subTreeTextStyle = nextChildNode.getStyle();
543 const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);
544 const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);
546 if (prevChildrenSize === 0) {
547 if (nextChildrenSize !== 0) {
552 nextChildrenSize - 1,
557 } else if (nextChildrenSize === 0) {
558 if (prevChildrenSize !== 0) {
559 // @ts-expect-error: internal field
560 const lexicalLineBreak = dom.__lexicalLineBreak;
561 const canUseFastPath = lexicalLineBreak == null;
565 prevChildrenSize - 1,
566 canUseFastPath ? null : dom,
569 if (canUseFastPath) {
570 // Fast path for removing DOM nodes
571 dom.textContent = '';
575 $reconcileNodeChildren(
586 if ($textContentRequiresDoubleLinebreakAtEnd(nextElement)) {
587 subTreeTextContent += DOUBLE_LINE_BREAK;
590 // @ts-expect-error: internal field
591 dom.__lexicalTextContent = subTreeTextContent;
592 subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
595 function $reconcileNode(
597 parentDOM: HTMLElement | null,
599 const prevNode = activePrevNodeMap.get(key);
600 let nextNode = activeNextNodeMap.get(key);
602 if (prevNode === undefined || nextNode === undefined) {
605 'reconcileNode: prevNode or nextNode does not exist in nodeMap',
610 treatAllNodesAsDirty ||
611 activeDirtyLeaves.has(key) ||
612 activeDirtyElements.has(key);
613 const dom = getElementByKeyOrThrow(activeEditor, key);
615 // If the node key points to the same instance in both states
616 // and isn't dirty, we just update the text content cache
617 // and return the existing DOM Node.
618 if (prevNode === nextNode && !isDirty) {
619 if ($isElementNode(prevNode)) {
620 // @ts-expect-error: internal field
621 const previousSubTreeTextContent = dom.__lexicalTextContent;
623 if (previousSubTreeTextContent !== undefined) {
624 subTreeTextContent += previousSubTreeTextContent;
625 editorTextContent += previousSubTreeTextContent;
628 // @ts-expect-error: internal field
629 const previousSubTreeDirectionTextContent = dom.__lexicalDirTextContent;
631 if (previousSubTreeDirectionTextContent !== undefined) {
632 subTreeDirectionedTextContent += previousSubTreeDirectionTextContent;
635 const text = prevNode.getTextContent();
637 if ($isTextNode(prevNode) && !prevNode.isDirectionless()) {
638 subTreeDirectionedTextContent += text;
641 editorTextContent += text;
642 subTreeTextContent += text;
647 // If the node key doesn't point to the same instance in both maps,
648 // it means it were cloned. If they're also dirty, we mark them as mutated.
649 if (prevNode !== nextNode && isDirty) {
653 activeMutationListeners,
659 // Update node. If it returns true, we need to unmount and re-create the node
660 if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) {
661 const replacementDOM = $createNode(key, null, null);
663 if (parentDOM === null) {
664 invariant(false, 'reconcileNode: parentDOM is null');
667 parentDOM.replaceChild(replacementDOM, dom);
668 destroyNode(key, null);
669 return replacementDOM;
672 if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
673 // Reconcile element children
674 const nextIndent = nextNode.__indent;
676 if (nextIndent !== prevNode.__indent) {
677 setElementIndent(dom, nextIndent);
680 const nextFormat = nextNode.__format;
682 if (nextFormat !== prevNode.__format) {
683 setElementFormat(dom, nextFormat);
686 $reconcileChildrenWithDirection(prevNode, nextNode, dom);
687 if (!$isRootNode(nextNode) && !nextNode.isInline()) {
688 reconcileElementTerminatingLineBreak(prevNode, nextNode, dom);
692 if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {
693 subTreeTextContent += DOUBLE_LINE_BREAK;
694 editorTextContent += DOUBLE_LINE_BREAK;
697 const text = nextNode.getTextContent();
699 if ($isDecoratorNode(nextNode)) {
700 const decorator = nextNode.decorate(activeEditor, activeEditorConfig);
702 if (decorator !== null) {
703 reconcileDecorator(key, decorator);
705 } else if ($isTextNode(nextNode) && !nextNode.isDirectionless()) {
706 // Handle text content, for LTR, LTR cases.
707 subTreeDirectionedTextContent += text;
710 subTreeTextContent += text;
711 editorTextContent += text;
715 !activeEditorStateReadOnly &&
716 $isRootNode(nextNode) &&
717 nextNode.__cachedText !== editorTextContent
719 // Cache the latest text content.
720 const nextRootNode = nextNode.getWritable();
721 nextRootNode.__cachedText = editorTextContent;
722 nextNode = nextRootNode;
726 // Freeze the node in DEV to prevent accidental mutations
727 Object.freeze(nextNode);
733 function reconcileDecorator(key: NodeKey, decorator: unknown): void {
734 let pendingDecorators = activeEditor._pendingDecorators;
735 const currentDecorators = activeEditor._decorators;
737 if (pendingDecorators === null) {
738 if (currentDecorators[key] === decorator) {
742 pendingDecorators = cloneDecorators(activeEditor);
745 pendingDecorators[key] = decorator;
748 function getFirstChild(element: HTMLElement): Node | null {
749 return element.firstChild;
752 function getNextSibling(element: HTMLElement): Node | null {
753 let nextSibling = element.nextSibling;
755 nextSibling !== null &&
756 nextSibling === activeEditor._blockCursorElement
758 nextSibling = nextSibling.nextSibling;
763 function $reconcileNodeChildren(
764 nextElement: ElementNode,
765 prevChildren: Array<NodeKey>,
766 nextChildren: Array<NodeKey>,
767 prevChildrenLength: number,
768 nextChildrenLength: number,
771 const prevEndIndex = prevChildrenLength - 1;
772 const nextEndIndex = nextChildrenLength - 1;
773 let prevChildrenSet: Set<NodeKey> | undefined;
774 let nextChildrenSet: Set<NodeKey> | undefined;
775 let siblingDOM: null | Node = getFirstChild(dom);
779 while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
780 const prevKey = prevChildren[prevIndex];
781 const nextKey = nextChildren[nextIndex];
783 if (prevKey === nextKey) {
784 siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
788 if (prevChildrenSet === undefined) {
789 prevChildrenSet = new Set(prevChildren);
792 if (nextChildrenSet === undefined) {
793 nextChildrenSet = new Set(nextChildren);
796 const nextHasPrevKey = nextChildrenSet.has(prevKey);
797 const prevHasNextKey = prevChildrenSet.has(nextKey);
799 if (!nextHasPrevKey) {
801 siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey));
802 destroyNode(prevKey, dom);
804 } else if (!prevHasNextKey) {
806 $createNode(nextKey, dom, siblingDOM);
810 const childDOM = getElementByKeyOrThrow(activeEditor, nextKey);
812 if (childDOM === siblingDOM) {
813 siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
815 if (siblingDOM != null) {
816 dom.insertBefore(childDOM, siblingDOM);
818 dom.appendChild(childDOM);
821 $reconcileNode(nextKey, dom);
829 const node = activeNextNodeMap.get(nextKey);
830 if (node !== null && $isTextNode(node)) {
831 if (subTreeTextFormat === null) {
832 subTreeTextFormat = node.getFormat();
834 if (subTreeTextStyle === '') {
835 subTreeTextStyle = node.getStyle();
840 const appendNewChildren = prevIndex > prevEndIndex;
841 const removeOldChildren = nextIndex > nextEndIndex;
843 if (appendNewChildren && !removeOldChildren) {
844 const previousNode = nextChildren[nextEndIndex + 1];
846 previousNode === undefined
848 : activeEditor.getElementByKey(previousNode);
857 } else if (removeOldChildren && !appendNewChildren) {
858 destroyChildren(prevChildren, prevIndex, prevEndIndex, dom);
862 export function $reconcileRoot(
863 prevEditorState: EditorState,
864 nextEditorState: EditorState,
865 editor: LexicalEditor,
866 dirtyType: 0 | 1 | 2,
867 dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
868 dirtyLeaves: Set<NodeKey>,
870 // We cache text content to make retrieval more efficient.
871 // The cache must be rebuilt during reconciliation to account for any changes.
872 subTreeTextContent = '';
873 editorTextContent = '';
874 subTreeDirectionedTextContent = '';
875 // Rather than pass around a load of arguments through the stack recursively
876 // we instead set them as bindings within the scope of the module.
877 treatAllNodesAsDirty = dirtyType === FULL_RECONCILE;
878 activeTextDirection = null;
879 activeEditor = editor;
880 activeEditorConfig = editor._config;
881 activeEditorNodes = editor._nodes;
882 activeMutationListeners = activeEditor._listeners.mutation;
883 activeDirtyElements = dirtyElements;
884 activeDirtyLeaves = dirtyLeaves;
885 activePrevNodeMap = prevEditorState._nodeMap;
886 activeNextNodeMap = nextEditorState._nodeMap;
887 activeEditorStateReadOnly = nextEditorState._readOnly;
888 activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
889 // We keep track of mutated nodes so we can trigger mutation
890 // listeners later in the update cycle.
891 const currentMutatedNodes = new Map();
892 mutatedNodes = currentMutatedNodes;
893 $reconcileNode('root', null);
894 // We don't want a bunch of void checks throughout the scope
895 // so instead we make it seem that these values are always set.
896 // We also want to make sure we clear them down, otherwise we
899 activeEditor = undefined;
901 activeEditorNodes = undefined;
903 activeDirtyElements = undefined;
905 activeDirtyLeaves = undefined;
907 activePrevNodeMap = undefined;
909 activeNextNodeMap = undefined;
911 activeEditorConfig = undefined;
913 activePrevKeyToDOMMap = undefined;
915 mutatedNodes = undefined;
917 return currentMutatedNodes;
920 export function storeDOMWithKey(
923 editor: LexicalEditor,
925 const keyToDOMMap = editor._keyToDOMMap;
926 // @ts-ignore We intentionally add this to the Node.
927 dom['__lexicalKey_' + editor._key] = key;
928 keyToDOMMap.set(key, dom);
931 function getPrevElementByKeyOrThrow(key: NodeKey): HTMLElement {
932 const element = activePrevKeyToDOMMap.get(key);
934 if (element === undefined) {
937 'Reconciliation: could not find DOM element for node key %s',