mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-04-19 18:22:16 +03:00
769 lines
21 KiB
TypeScript
769 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) {
|
|
|
|
const inserted = node?.insertDOMIntoParent(dom, parentDOM);
|
|
|
|
if (!inserted) {
|
|
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;
|
|
}
|