764 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			764 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| /**
 | |
|  * Copyright (c) Meta Platforms, Inc. and affiliates.
 | |
|  *
 | |
|  * This source code is licensed under the MIT license found in the
 | |
|  * LICENSE file in the root directory of this source tree.
 | |
|  *
 | |
|  */
 | |
| 
 | |
| import type {
 | |
|   EditorConfig,
 | |
|   LexicalEditor,
 | |
|   MutatedNodes,
 | |
|   MutationListeners,
 | |
|   RegisteredNodes,
 | |
| } from './LexicalEditor';
 | |
| import type {NodeKey, NodeMap} from './LexicalNode';
 | |
| import type {ElementNode} from './nodes/LexicalElementNode';
 | |
| 
 | |
| import invariant from 'lexical/shared/invariant';
 | |
| 
 | |
| import {
 | |
|   $isDecoratorNode,
 | |
|   $isElementNode,
 | |
|   $isLineBreakNode,
 | |
|   $isParagraphNode,
 | |
|   $isRootNode,
 | |
|   $isTextNode,
 | |
| } from '.';
 | |
| import {
 | |
|   DOUBLE_LINE_BREAK,
 | |
|   FULL_RECONCILE,
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| 
 | |
| } from './LexicalConstants';
 | |
| import {EditorState} from './LexicalEditorState';
 | |
| import {
 | |
|   $textContentRequiresDoubleLinebreakAtEnd,
 | |
|   cloneDecorators,
 | |
|   getElementByKeyOrThrow,
 | |
|   setMutatedNode,
 | |
| } from './LexicalUtils';
 | |
| 
 | |
| type IntentionallyMarkedAsDirtyElement = boolean;
 | |
| 
 | |
| let subTreeTextContent = '';
 | |
| let subTreeTextFormat: number | null = null;
 | |
| let subTreeTextStyle: string = '';
 | |
| let editorTextContent = '';
 | |
| let activeEditorConfig: EditorConfig;
 | |
| let activeEditor: LexicalEditor;
 | |
| let activeEditorNodes: RegisteredNodes;
 | |
| let treatAllNodesAsDirty = false;
 | |
| let activeEditorStateReadOnly = false;
 | |
| let activeMutationListeners: MutationListeners;
 | |
| let activeDirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
 | |
| let activeDirtyLeaves: Set<NodeKey>;
 | |
| let activePrevNodeMap: NodeMap;
 | |
| let activeNextNodeMap: NodeMap;
 | |
| let activePrevKeyToDOMMap: Map<NodeKey, HTMLElement>;
 | |
| let mutatedNodes: MutatedNodes;
 | |
| 
 | |
| function destroyNode(key: NodeKey, parentDOM: null | HTMLElement): void {
 | |
|   const node = activePrevNodeMap.get(key);
 | |
| 
 | |
|   if (parentDOM !== null) {
 | |
|     const dom = getPrevElementByKeyOrThrow(key);
 | |
|     if (dom.parentNode === parentDOM) {
 | |
|       parentDOM.removeChild(dom);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // This logic is really important, otherwise we will leak DOM nodes
 | |
|   // when their corresponding LexicalNodes are removed from the editor state.
 | |
|   if (!activeNextNodeMap.has(key)) {
 | |
|     activeEditor._keyToDOMMap.delete(key);
 | |
|   }
 | |
| 
 | |
|   if ($isElementNode(node)) {
 | |
|     const children = createChildrenArray(node, activePrevNodeMap);
 | |
|     destroyChildren(children, 0, children.length - 1, null);
 | |
|   }
 | |
| 
 | |
|   if (node !== undefined) {
 | |
|     setMutatedNode(
 | |
|       mutatedNodes,
 | |
|       activeEditorNodes,
 | |
|       activeMutationListeners,
 | |
|       node,
 | |
|       'destroyed',
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| function destroyChildren(
 | |
|   children: Array<NodeKey>,
 | |
|   _startIndex: number,
 | |
|   endIndex: number,
 | |
|   dom: null | HTMLElement,
 | |
| ): void {
 | |
|   let startIndex = _startIndex;
 | |
| 
 | |
|   for (; startIndex <= endIndex; ++startIndex) {
 | |
|     const child = children[startIndex];
 | |
| 
 | |
|     if (child !== undefined) {
 | |
|       destroyNode(child, dom);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| function setTextAlign(domStyle: CSSStyleDeclaration, value: string): void {
 | |
|   domStyle.setProperty('text-align', value);
 | |
| }
 | |
| 
 | |
| function $createNode(
 | |
|   key: NodeKey,
 | |
|   parentDOM: null | HTMLElement,
 | |
|   insertDOM: null | Node,
 | |
| ): HTMLElement {
 | |
|   const node = activeNextNodeMap.get(key);
 | |
| 
 | |
|   if (node === undefined) {
 | |
|     invariant(false, 'createNode: node does not exist in nodeMap');
 | |
|   }
 | |
|   const dom = node.createDOM(activeEditorConfig, activeEditor);
 | |
|   storeDOMWithKey(key, dom, activeEditor);
 | |
| 
 | |
|   // This helps preserve the text, and stops spell check tools from
 | |
|   // merging or break the spans (which happens if they are missing
 | |
|   // this attribute).
 | |
|   if ($isTextNode(node)) {
 | |
|     dom.setAttribute('data-lexical-text', 'true');
 | |
|   } else if ($isDecoratorNode(node)) {
 | |
|     dom.setAttribute('data-lexical-decorator', 'true');
 | |
|   }
 | |
| 
 | |
|   if ($isElementNode(node)) {
 | |
|     const childrenSize = node.__size;
 | |
| 
 | |
|     if (childrenSize !== 0) {
 | |
|       const endIndex = childrenSize - 1;
 | |
|       const children = createChildrenArray(node, activeNextNodeMap);
 | |
|       $createChildren(children, node, 0, endIndex, dom, null);
 | |
|     }
 | |
| 
 | |
|     if (!node.isInline()) {
 | |
|       reconcileElementTerminatingLineBreak(null, node, dom);
 | |
|     }
 | |
|     if ($textContentRequiresDoubleLinebreakAtEnd(node)) {
 | |
|       subTreeTextContent += DOUBLE_LINE_BREAK;
 | |
|       editorTextContent += DOUBLE_LINE_BREAK;
 | |
|     }
 | |
|   } else {
 | |
|     const text = node.getTextContent();
 | |
| 
 | |
|     if ($isDecoratorNode(node)) {
 | |
|       const decorator = node.decorate(activeEditor, activeEditorConfig);
 | |
| 
 | |
|       if (decorator !== null) {
 | |
|         reconcileDecorator(key, decorator);
 | |
|       }
 | |
|       // Decorators are always non editable
 | |
|       dom.contentEditable = 'false';
 | |
|     }
 | |
|     subTreeTextContent += text;
 | |
|     editorTextContent += text;
 | |
|   }
 | |
| 
 | |
|   if (parentDOM !== null) {
 | |
|     if (insertDOM != null) {
 | |
|       parentDOM.insertBefore(dom, insertDOM);
 | |
|     } else {
 | |
|       // @ts-expect-error: internal field
 | |
|       const possibleLineBreak = parentDOM.__lexicalLineBreak;
 | |
| 
 | |
|       if (possibleLineBreak != null) {
 | |
|         parentDOM.insertBefore(dom, possibleLineBreak);
 | |
|       } else {
 | |
|         parentDOM.appendChild(dom);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (__DEV__) {
 | |
|     // Freeze the node in DEV to prevent accidental mutations
 | |
|     Object.freeze(node);
 | |
|   }
 | |
| 
 | |
|   setMutatedNode(
 | |
|     mutatedNodes,
 | |
|     activeEditorNodes,
 | |
|     activeMutationListeners,
 | |
|     node,
 | |
|     'created',
 | |
|   );
 | |
|   return dom;
 | |
| }
 | |
| 
 | |
| function $createChildren(
 | |
|   children: Array<NodeKey>,
 | |
|   element: ElementNode,
 | |
|   _startIndex: number,
 | |
|   endIndex: number,
 | |
|   dom: null | HTMLElement,
 | |
|   insertDOM: null | HTMLElement,
 | |
| ): void {
 | |
|   const previousSubTreeTextContent = subTreeTextContent;
 | |
|   subTreeTextContent = '';
 | |
|   let startIndex = _startIndex;
 | |
| 
 | |
|   for (; startIndex <= endIndex; ++startIndex) {
 | |
|     $createNode(children[startIndex], dom, insertDOM);
 | |
|     const node = activeNextNodeMap.get(children[startIndex]);
 | |
|     if (node !== null && $isTextNode(node)) {
 | |
|       if (subTreeTextFormat === null) {
 | |
|         subTreeTextFormat = node.getFormat();
 | |
|       }
 | |
|       if (subTreeTextStyle === '') {
 | |
|         subTreeTextStyle = node.getStyle();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
|   if ($textContentRequiresDoubleLinebreakAtEnd(element)) {
 | |
|     subTreeTextContent += DOUBLE_LINE_BREAK;
 | |
|   }
 | |
|   // @ts-expect-error: internal field
 | |
|   dom.__lexicalTextContent = subTreeTextContent;
 | |
|   subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
 | |
| }
 | |
| 
 | |
| function isLastChildLineBreakOrDecorator(
 | |
|   childKey: NodeKey,
 | |
|   nodeMap: NodeMap,
 | |
| ): boolean {
 | |
|   const node = nodeMap.get(childKey);
 | |
|   return $isLineBreakNode(node) || ($isDecoratorNode(node) && node.isInline());
 | |
| }
 | |
| 
 | |
| // If we end an element with a LineBreakNode, then we need to add an additional <br>
 | |
| function reconcileElementTerminatingLineBreak(
 | |
|   prevElement: null | ElementNode,
 | |
|   nextElement: ElementNode,
 | |
|   dom: HTMLElement,
 | |
| ): void {
 | |
|   const prevLineBreak =
 | |
|     prevElement !== null &&
 | |
|     (prevElement.__size === 0 ||
 | |
|       isLastChildLineBreakOrDecorator(
 | |
|         prevElement.__last as NodeKey,
 | |
|         activePrevNodeMap,
 | |
|       ));
 | |
|   const nextLineBreak =
 | |
|     nextElement.__size === 0 ||
 | |
|     isLastChildLineBreakOrDecorator(
 | |
|       nextElement.__last as NodeKey,
 | |
|       activeNextNodeMap,
 | |
|     );
 | |
| 
 | |
|   if (prevLineBreak) {
 | |
|     if (!nextLineBreak) {
 | |
|       // @ts-expect-error: internal field
 | |
|       const element = dom.__lexicalLineBreak;
 | |
| 
 | |
|       if (element != null) {
 | |
|         try {
 | |
|           dom.removeChild(element);
 | |
|         } catch (error) {
 | |
|           if (typeof error === 'object' && error != null) {
 | |
|             const msg = `${error.toString()} Parent: ${dom.tagName}, child: ${
 | |
|               element.tagName
 | |
|             }.`;
 | |
|             throw new Error(msg);
 | |
|           } else {
 | |
|             throw error;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // @ts-expect-error: internal field
 | |
|       dom.__lexicalLineBreak = null;
 | |
|     }
 | |
|   } else if (nextLineBreak) {
 | |
|     const element = document.createElement('br');
 | |
|     // @ts-expect-error: internal field
 | |
|     dom.__lexicalLineBreak = element;
 | |
|     dom.appendChild(element);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function reconcileParagraphFormat(element: ElementNode): void {
 | |
|   if (
 | |
|     $isParagraphNode(element) &&
 | |
|     subTreeTextFormat != null &&
 | |
|     !activeEditorStateReadOnly
 | |
|   ) {
 | |
|     element.setTextStyle(subTreeTextStyle);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function reconcileParagraphStyle(element: ElementNode): void {
 | |
|   if (
 | |
|     $isParagraphNode(element) &&
 | |
|     subTreeTextStyle !== '' &&
 | |
|     subTreeTextStyle !== element.__textStyle &&
 | |
|     !activeEditorStateReadOnly
 | |
|   ) {
 | |
|     element.setTextStyle(subTreeTextStyle);
 | |
|   }
 | |
| }
 | |
| 
 | |
| function $reconcileChildrenWithDirection(
 | |
|   prevElement: ElementNode,
 | |
|   nextElement: ElementNode,
 | |
|   dom: HTMLElement,
 | |
| ): void {
 | |
|   subTreeTextFormat = null;
 | |
|   subTreeTextStyle = '';
 | |
|   $reconcileChildren(prevElement, nextElement, dom);
 | |
|   reconcileParagraphFormat(nextElement);
 | |
|   reconcileParagraphStyle(nextElement);
 | |
| }
 | |
| 
 | |
| function createChildrenArray(
 | |
|   element: ElementNode,
 | |
|   nodeMap: NodeMap,
 | |
| ): Array<NodeKey> {
 | |
|   const children = [];
 | |
|   let nodeKey = element.__first;
 | |
|   while (nodeKey !== null) {
 | |
|     const node = nodeMap.get(nodeKey);
 | |
|     if (node === undefined) {
 | |
|       invariant(false, 'createChildrenArray: node does not exist in nodeMap');
 | |
|     }
 | |
|     children.push(nodeKey);
 | |
|     nodeKey = node.__next;
 | |
|   }
 | |
|   return children;
 | |
| }
 | |
| 
 | |
| function $reconcileChildren(
 | |
|   prevElement: ElementNode,
 | |
|   nextElement: ElementNode,
 | |
|   dom: HTMLElement,
 | |
| ): void {
 | |
|   const previousSubTreeTextContent = subTreeTextContent;
 | |
|   const prevChildrenSize = prevElement.__size;
 | |
|   const nextChildrenSize = nextElement.__size;
 | |
|   subTreeTextContent = '';
 | |
| 
 | |
|   if (prevChildrenSize === 1 && nextChildrenSize === 1) {
 | |
|     const prevFirstChildKey = prevElement.__first as NodeKey;
 | |
|     const nextFrstChildKey = nextElement.__first as NodeKey;
 | |
|     if (prevFirstChildKey === nextFrstChildKey) {
 | |
|       $reconcileNode(prevFirstChildKey, dom);
 | |
|     } else {
 | |
|       const lastDOM = getPrevElementByKeyOrThrow(prevFirstChildKey);
 | |
|       const replacementDOM = $createNode(nextFrstChildKey, null, null);
 | |
|       try {
 | |
|         dom.replaceChild(replacementDOM, lastDOM);
 | |
|       } catch (error) {
 | |
|         if (typeof error === 'object' && error != null) {
 | |
|           const msg = `${error.toString()} Parent: ${
 | |
|             dom.tagName
 | |
|           }, new child: {tag: ${
 | |
|             replacementDOM.tagName
 | |
|           } key: ${nextFrstChildKey}}, old child: {tag: ${
 | |
|             lastDOM.tagName
 | |
|           }, key: ${prevFirstChildKey}}.`;
 | |
|           throw new Error(msg);
 | |
|         } else {
 | |
|           throw error;
 | |
|         }
 | |
|       }
 | |
|       destroyNode(prevFirstChildKey, null);
 | |
|     }
 | |
|     const nextChildNode = activeNextNodeMap.get(nextFrstChildKey);
 | |
|     if ($isTextNode(nextChildNode)) {
 | |
|       if (subTreeTextFormat === null) {
 | |
|         subTreeTextFormat = nextChildNode.getFormat();
 | |
|       }
 | |
|       if (subTreeTextStyle === '') {
 | |
|         subTreeTextStyle = nextChildNode.getStyle();
 | |
|       }
 | |
|     }
 | |
|   } else {
 | |
|     const prevChildren = createChildrenArray(prevElement, activePrevNodeMap);
 | |
|     const nextChildren = createChildrenArray(nextElement, activeNextNodeMap);
 | |
| 
 | |
|     if (prevChildrenSize === 0) {
 | |
|       if (nextChildrenSize !== 0) {
 | |
|         $createChildren(
 | |
|           nextChildren,
 | |
|           nextElement,
 | |
|           0,
 | |
|           nextChildrenSize - 1,
 | |
|           dom,
 | |
|           null,
 | |
|         );
 | |
|       }
 | |
|     } else if (nextChildrenSize === 0) {
 | |
|       if (prevChildrenSize !== 0) {
 | |
|         // @ts-expect-error: internal field
 | |
|         const lexicalLineBreak = dom.__lexicalLineBreak;
 | |
|         const canUseFastPath = lexicalLineBreak == null;
 | |
|         destroyChildren(
 | |
|           prevChildren,
 | |
|           0,
 | |
|           prevChildrenSize - 1,
 | |
|           canUseFastPath ? null : dom,
 | |
|         );
 | |
| 
 | |
|         if (canUseFastPath) {
 | |
|           // Fast path for removing DOM nodes
 | |
|           dom.textContent = '';
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       $reconcileNodeChildren(
 | |
|         nextElement,
 | |
|         prevChildren,
 | |
|         nextChildren,
 | |
|         prevChildrenSize,
 | |
|         nextChildrenSize,
 | |
|         dom,
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if ($textContentRequiresDoubleLinebreakAtEnd(nextElement)) {
 | |
|     subTreeTextContent += DOUBLE_LINE_BREAK;
 | |
|   }
 | |
| 
 | |
|   // @ts-expect-error: internal field
 | |
|   dom.__lexicalTextContent = subTreeTextContent;
 | |
|   subTreeTextContent = previousSubTreeTextContent + subTreeTextContent;
 | |
| }
 | |
| 
 | |
| function $reconcileNode(
 | |
|   key: NodeKey,
 | |
|   parentDOM: HTMLElement | null,
 | |
| ): HTMLElement {
 | |
|   const prevNode = activePrevNodeMap.get(key);
 | |
|   let nextNode = activeNextNodeMap.get(key);
 | |
| 
 | |
|   if (prevNode === undefined || nextNode === undefined) {
 | |
|     invariant(
 | |
|       false,
 | |
|       'reconcileNode: prevNode or nextNode does not exist in nodeMap',
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   const isDirty =
 | |
|     treatAllNodesAsDirty ||
 | |
|     activeDirtyLeaves.has(key) ||
 | |
|     activeDirtyElements.has(key);
 | |
|   const dom = getElementByKeyOrThrow(activeEditor, key);
 | |
| 
 | |
|   // If the node key points to the same instance in both states
 | |
|   // and isn't dirty, we just update the text content cache
 | |
|   // and return the existing DOM Node.
 | |
|   if (prevNode === nextNode && !isDirty) {
 | |
|     if ($isElementNode(prevNode)) {
 | |
|       // @ts-expect-error: internal field
 | |
|       const previousSubTreeTextContent = dom.__lexicalTextContent;
 | |
| 
 | |
|       if (previousSubTreeTextContent !== undefined) {
 | |
|         subTreeTextContent += previousSubTreeTextContent;
 | |
|         editorTextContent += previousSubTreeTextContent;
 | |
|       }
 | |
|     } else {
 | |
|       const text = prevNode.getTextContent();
 | |
| 
 | |
|       editorTextContent += text;
 | |
|       subTreeTextContent += text;
 | |
|     }
 | |
| 
 | |
|     return dom;
 | |
|   }
 | |
|   // If the node key doesn't point to the same instance in both maps,
 | |
|   // it means it were cloned. If they're also dirty, we mark them as mutated.
 | |
|   if (prevNode !== nextNode && isDirty) {
 | |
|     setMutatedNode(
 | |
|       mutatedNodes,
 | |
|       activeEditorNodes,
 | |
|       activeMutationListeners,
 | |
|       nextNode,
 | |
|       'updated',
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // Update node. If it returns true, we need to unmount and re-create the node
 | |
|   if (nextNode.updateDOM(prevNode, dom, activeEditorConfig)) {
 | |
|     const replacementDOM = $createNode(key, null, null);
 | |
| 
 | |
|     if (parentDOM === null) {
 | |
|       invariant(false, 'reconcileNode: parentDOM is null');
 | |
|     }
 | |
| 
 | |
|     parentDOM.replaceChild(replacementDOM, dom);
 | |
|     destroyNode(key, null);
 | |
|     return replacementDOM;
 | |
|   }
 | |
| 
 | |
|   if ($isElementNode(prevNode) && $isElementNode(nextNode)) {
 | |
|     // Reconcile element children
 | |
|     if (isDirty) {
 | |
|       $reconcileChildrenWithDirection(prevNode, nextNode, dom);
 | |
|       if (!$isRootNode(nextNode) && !nextNode.isInline()) {
 | |
|         reconcileElementTerminatingLineBreak(prevNode, nextNode, dom);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if ($textContentRequiresDoubleLinebreakAtEnd(nextNode)) {
 | |
|       subTreeTextContent += DOUBLE_LINE_BREAK;
 | |
|       editorTextContent += DOUBLE_LINE_BREAK;
 | |
|     }
 | |
|   } else {
 | |
|     const text = nextNode.getTextContent();
 | |
| 
 | |
|     if ($isDecoratorNode(nextNode)) {
 | |
|       const decorator = nextNode.decorate(activeEditor, activeEditorConfig);
 | |
| 
 | |
|       if (decorator !== null) {
 | |
|         reconcileDecorator(key, decorator);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     subTreeTextContent += text;
 | |
|     editorTextContent += text;
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     !activeEditorStateReadOnly &&
 | |
|     $isRootNode(nextNode) &&
 | |
|     nextNode.__cachedText !== editorTextContent
 | |
|   ) {
 | |
|     // Cache the latest text content.
 | |
|     const nextRootNode = nextNode.getWritable();
 | |
|     nextRootNode.__cachedText = editorTextContent;
 | |
|     nextNode = nextRootNode;
 | |
|   }
 | |
| 
 | |
|   if (__DEV__) {
 | |
|     // Freeze the node in DEV to prevent accidental mutations
 | |
|     Object.freeze(nextNode);
 | |
|   }
 | |
| 
 | |
|   return dom;
 | |
| }
 | |
| 
 | |
| function reconcileDecorator(key: NodeKey, decorator: unknown): void {
 | |
|   let pendingDecorators = activeEditor._pendingDecorators;
 | |
|   const currentDecorators = activeEditor._decorators;
 | |
| 
 | |
|   if (pendingDecorators === null) {
 | |
|     if (currentDecorators[key] === decorator) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     pendingDecorators = cloneDecorators(activeEditor);
 | |
|   }
 | |
| 
 | |
|   pendingDecorators[key] = decorator;
 | |
| }
 | |
| 
 | |
| function getFirstChild(element: HTMLElement): Node | null {
 | |
|   return element.firstChild;
 | |
| }
 | |
| 
 | |
| function getNextSibling(element: HTMLElement): Node | null {
 | |
|   let nextSibling = element.nextSibling;
 | |
|   if (
 | |
|     nextSibling !== null &&
 | |
|     nextSibling === activeEditor._blockCursorElement
 | |
|   ) {
 | |
|     nextSibling = nextSibling.nextSibling;
 | |
|   }
 | |
|   return nextSibling;
 | |
| }
 | |
| 
 | |
| function $reconcileNodeChildren(
 | |
|   nextElement: ElementNode,
 | |
|   prevChildren: Array<NodeKey>,
 | |
|   nextChildren: Array<NodeKey>,
 | |
|   prevChildrenLength: number,
 | |
|   nextChildrenLength: number,
 | |
|   dom: HTMLElement,
 | |
| ): void {
 | |
|   const prevEndIndex = prevChildrenLength - 1;
 | |
|   const nextEndIndex = nextChildrenLength - 1;
 | |
|   let prevChildrenSet: Set<NodeKey> | undefined;
 | |
|   let nextChildrenSet: Set<NodeKey> | undefined;
 | |
|   let siblingDOM: null | Node = getFirstChild(dom);
 | |
|   let prevIndex = 0;
 | |
|   let nextIndex = 0;
 | |
| 
 | |
|   while (prevIndex <= prevEndIndex && nextIndex <= nextEndIndex) {
 | |
|     const prevKey = prevChildren[prevIndex];
 | |
|     const nextKey = nextChildren[nextIndex];
 | |
| 
 | |
|     if (prevKey === nextKey) {
 | |
|       siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
 | |
|       prevIndex++;
 | |
|       nextIndex++;
 | |
|     } else {
 | |
|       if (prevChildrenSet === undefined) {
 | |
|         prevChildrenSet = new Set(prevChildren);
 | |
|       }
 | |
| 
 | |
|       if (nextChildrenSet === undefined) {
 | |
|         nextChildrenSet = new Set(nextChildren);
 | |
|       }
 | |
| 
 | |
|       const nextHasPrevKey = nextChildrenSet.has(prevKey);
 | |
|       const prevHasNextKey = prevChildrenSet.has(nextKey);
 | |
| 
 | |
|       if (!nextHasPrevKey) {
 | |
|         // Remove prev
 | |
|         siblingDOM = getNextSibling(getPrevElementByKeyOrThrow(prevKey));
 | |
|         destroyNode(prevKey, dom);
 | |
|         prevIndex++;
 | |
|       } else if (!prevHasNextKey) {
 | |
|         // Create next
 | |
|         $createNode(nextKey, dom, siblingDOM);
 | |
|         nextIndex++;
 | |
|       } else {
 | |
|         // Move next
 | |
|         const childDOM = getElementByKeyOrThrow(activeEditor, nextKey);
 | |
| 
 | |
|         if (childDOM === siblingDOM) {
 | |
|           siblingDOM = getNextSibling($reconcileNode(nextKey, dom));
 | |
|         } else {
 | |
|           if (siblingDOM != null) {
 | |
|             dom.insertBefore(childDOM, siblingDOM);
 | |
|           } else {
 | |
|             dom.appendChild(childDOM);
 | |
|           }
 | |
| 
 | |
|           $reconcileNode(nextKey, dom);
 | |
|         }
 | |
| 
 | |
|         prevIndex++;
 | |
|         nextIndex++;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     const node = activeNextNodeMap.get(nextKey);
 | |
|     if (node !== null && $isTextNode(node)) {
 | |
|       if (subTreeTextFormat === null) {
 | |
|         subTreeTextFormat = node.getFormat();
 | |
|       }
 | |
|       if (subTreeTextStyle === '') {
 | |
|         subTreeTextStyle = node.getStyle();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const appendNewChildren = prevIndex > prevEndIndex;
 | |
|   const removeOldChildren = nextIndex > nextEndIndex;
 | |
| 
 | |
|   if (appendNewChildren && !removeOldChildren) {
 | |
|     const previousNode = nextChildren[nextEndIndex + 1];
 | |
|     const insertDOM =
 | |
|       previousNode === undefined
 | |
|         ? null
 | |
|         : activeEditor.getElementByKey(previousNode);
 | |
|     $createChildren(
 | |
|       nextChildren,
 | |
|       nextElement,
 | |
|       nextIndex,
 | |
|       nextEndIndex,
 | |
|       dom,
 | |
|       insertDOM,
 | |
|     );
 | |
|   } else if (removeOldChildren && !appendNewChildren) {
 | |
|     destroyChildren(prevChildren, prevIndex, prevEndIndex, dom);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export function $reconcileRoot(
 | |
|   prevEditorState: EditorState,
 | |
|   nextEditorState: EditorState,
 | |
|   editor: LexicalEditor,
 | |
|   dirtyType: 0 | 1 | 2,
 | |
|   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>,
 | |
|   dirtyLeaves: Set<NodeKey>,
 | |
| ): MutatedNodes {
 | |
|   // We cache text content to make retrieval more efficient.
 | |
|   // The cache must be rebuilt during reconciliation to account for any changes.
 | |
|   subTreeTextContent = '';
 | |
|   editorTextContent = '';
 | |
|   // Rather than pass around a load of arguments through the stack recursively
 | |
|   // we instead set them as bindings within the scope of the module.
 | |
|   treatAllNodesAsDirty = dirtyType === FULL_RECONCILE;
 | |
|   activeEditor = editor;
 | |
|   activeEditorConfig = editor._config;
 | |
|   activeEditorNodes = editor._nodes;
 | |
|   activeMutationListeners = activeEditor._listeners.mutation;
 | |
|   activeDirtyElements = dirtyElements;
 | |
|   activeDirtyLeaves = dirtyLeaves;
 | |
|   activePrevNodeMap = prevEditorState._nodeMap;
 | |
|   activeNextNodeMap = nextEditorState._nodeMap;
 | |
|   activeEditorStateReadOnly = nextEditorState._readOnly;
 | |
|   activePrevKeyToDOMMap = new Map(editor._keyToDOMMap);
 | |
|   // We keep track of mutated nodes so we can trigger mutation
 | |
|   // listeners later in the update cycle.
 | |
|   const currentMutatedNodes = new Map();
 | |
|   mutatedNodes = currentMutatedNodes;
 | |
|   $reconcileNode('root', null);
 | |
|   // We don't want a bunch of void checks throughout the scope
 | |
|   // so instead we make it seem that these values are always set.
 | |
|   // We also want to make sure we clear them down, otherwise we
 | |
|   // can leak memory.
 | |
|   // @ts-ignore
 | |
|   activeEditor = undefined;
 | |
|   // @ts-ignore
 | |
|   activeEditorNodes = undefined;
 | |
|   // @ts-ignore
 | |
|   activeDirtyElements = undefined;
 | |
|   // @ts-ignore
 | |
|   activeDirtyLeaves = undefined;
 | |
|   // @ts-ignore
 | |
|   activePrevNodeMap = undefined;
 | |
|   // @ts-ignore
 | |
|   activeNextNodeMap = undefined;
 | |
|   // @ts-ignore
 | |
|   activeEditorConfig = undefined;
 | |
|   // @ts-ignore
 | |
|   activePrevKeyToDOMMap = undefined;
 | |
|   // @ts-ignore
 | |
|   mutatedNodes = undefined;
 | |
| 
 | |
|   return currentMutatedNodes;
 | |
| }
 | |
| 
 | |
| export function storeDOMWithKey(
 | |
|   key: NodeKey,
 | |
|   dom: HTMLElement,
 | |
|   editor: LexicalEditor,
 | |
| ): void {
 | |
|   const keyToDOMMap = editor._keyToDOMMap;
 | |
|   // @ts-ignore We intentionally add this to the Node.
 | |
|   dom['__lexicalKey_' + editor._key] = key;
 | |
|   keyToDOMMap.set(key, dom);
 | |
| }
 | |
| 
 | |
| function getPrevElementByKeyOrThrow(key: NodeKey): HTMLElement {
 | |
|   const element = activePrevKeyToDOMMap.get(key);
 | |
| 
 | |
|   if (element === undefined) {
 | |
|     invariant(
 | |
|       false,
 | |
|       'Reconciliation: could not find DOM element for node key %s',
 | |
|       key,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   return element;
 | |
| }
 |