mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-04 13:31:45 +03:00 
			
		
		
		
	Imported at 0.17.1, Modified to work in-app. Added & configured test dependancies. Tests need to be altered to avoid using non-included deps including react dependancies.
		
			
				
	
	
		
			1036 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1036 lines
		
	
	
		
			29 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 {SerializedEditorState} from './LexicalEditorState';
 | 
						|
import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';
 | 
						|
 | 
						|
import invariant from 'lexical/shared/invariant';
 | 
						|
 | 
						|
import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
 | 
						|
import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
 | 
						|
import {
 | 
						|
  CommandPayloadType,
 | 
						|
  EditorUpdateOptions,
 | 
						|
  LexicalCommand,
 | 
						|
  LexicalEditor,
 | 
						|
  Listener,
 | 
						|
  MutatedNodes,
 | 
						|
  RegisteredNodes,
 | 
						|
  resetEditor,
 | 
						|
  Transform,
 | 
						|
} from './LexicalEditor';
 | 
						|
import {
 | 
						|
  cloneEditorState,
 | 
						|
  createEmptyEditorState,
 | 
						|
  EditorState,
 | 
						|
  editorStateHasDirtySelection,
 | 
						|
} from './LexicalEditorState';
 | 
						|
import {
 | 
						|
  $garbageCollectDetachedDecorators,
 | 
						|
  $garbageCollectDetachedNodes,
 | 
						|
} from './LexicalGC';
 | 
						|
import {initMutationObserver} from './LexicalMutations';
 | 
						|
import {$normalizeTextNode} from './LexicalNormalization';
 | 
						|
import {$reconcileRoot} from './LexicalReconciler';
 | 
						|
import {
 | 
						|
  $internalCreateSelection,
 | 
						|
  $isNodeSelection,
 | 
						|
  $isRangeSelection,
 | 
						|
  applySelectionTransforms,
 | 
						|
  updateDOMSelection,
 | 
						|
} from './LexicalSelection';
 | 
						|
import {
 | 
						|
  $getCompositionKey,
 | 
						|
  getDOMSelection,
 | 
						|
  getEditorPropertyFromDOMNode,
 | 
						|
  getEditorStateTextContent,
 | 
						|
  getEditorsToPropagate,
 | 
						|
  getRegisteredNodeOrThrow,
 | 
						|
  isLexicalEditor,
 | 
						|
  removeDOMBlockCursorElement,
 | 
						|
  scheduleMicroTask,
 | 
						|
  updateDOMBlockCursorElement,
 | 
						|
} from './LexicalUtils';
 | 
						|
 | 
						|
let activeEditorState: null | EditorState = null;
 | 
						|
let activeEditor: null | LexicalEditor = null;
 | 
						|
let isReadOnlyMode = false;
 | 
						|
let isAttemptingToRecoverFromReconcilerError = false;
 | 
						|
let infiniteTransformCount = 0;
 | 
						|
 | 
						|
const observerOptions = {
 | 
						|
  characterData: true,
 | 
						|
  childList: true,
 | 
						|
  subtree: true,
 | 
						|
};
 | 
						|
 | 
						|
export function isCurrentlyReadOnlyMode(): boolean {
 | 
						|
  return (
 | 
						|
    isReadOnlyMode ||
 | 
						|
    (activeEditorState !== null && activeEditorState._readOnly)
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
export function errorOnReadOnly(): void {
 | 
						|
  if (isReadOnlyMode) {
 | 
						|
    invariant(false, 'Cannot use method in read-only mode.');
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function errorOnInfiniteTransforms(): void {
 | 
						|
  if (infiniteTransformCount > 99) {
 | 
						|
    invariant(
 | 
						|
      false,
 | 
						|
      'One or more transforms are endlessly triggering additional transforms. May have encountered infinite recursion caused by transforms that have their preconditions too lose and/or conflict with each other.',
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function getActiveEditorState(): EditorState {
 | 
						|
  if (activeEditorState === null) {
 | 
						|
    invariant(
 | 
						|
      false,
 | 
						|
      'Unable to find an active editor state. ' +
 | 
						|
        'State helpers or node methods can only be used ' +
 | 
						|
        'synchronously during the callback of ' +
 | 
						|
        'editor.update(), editor.read(), or editorState.read().%s',
 | 
						|
      collectBuildInformation(),
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  return activeEditorState;
 | 
						|
}
 | 
						|
 | 
						|
export function getActiveEditor(): LexicalEditor {
 | 
						|
  if (activeEditor === null) {
 | 
						|
    invariant(
 | 
						|
      false,
 | 
						|
      'Unable to find an active editor. ' +
 | 
						|
        'This method can only be used ' +
 | 
						|
        'synchronously during the callback of ' +
 | 
						|
        'editor.update() or editor.read().%s',
 | 
						|
      collectBuildInformation(),
 | 
						|
    );
 | 
						|
  }
 | 
						|
  return activeEditor;
 | 
						|
}
 | 
						|
 | 
						|
function collectBuildInformation(): string {
 | 
						|
  let compatibleEditors = 0;
 | 
						|
  const incompatibleEditors = new Set<string>();
 | 
						|
  const thisVersion = LexicalEditor.version;
 | 
						|
  if (typeof window !== 'undefined') {
 | 
						|
    for (const node of document.querySelectorAll('[contenteditable]')) {
 | 
						|
      const editor = getEditorPropertyFromDOMNode(node);
 | 
						|
      if (isLexicalEditor(editor)) {
 | 
						|
        compatibleEditors++;
 | 
						|
      } else if (editor) {
 | 
						|
        let version = String(
 | 
						|
          (
 | 
						|
            editor.constructor as typeof editor['constructor'] &
 | 
						|
              Record<string, unknown>
 | 
						|
          ).version || '<0.17.1',
 | 
						|
        );
 | 
						|
        if (version === thisVersion) {
 | 
						|
          version +=
 | 
						|
            ' (separately built, likely a bundler configuration issue)';
 | 
						|
        }
 | 
						|
        incompatibleEditors.add(version);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
  let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`;
 | 
						|
  if (incompatibleEditors.size) {
 | 
						|
    output += ` and incompatible editors with versions ${Array.from(
 | 
						|
      incompatibleEditors,
 | 
						|
    ).join(', ')}`;
 | 
						|
  }
 | 
						|
  return output;
 | 
						|
}
 | 
						|
 | 
						|
export function internalGetActiveEditor(): LexicalEditor | null {
 | 
						|
  return activeEditor;
 | 
						|
}
 | 
						|
 | 
						|
export function internalGetActiveEditorState(): EditorState | null {
 | 
						|
  return activeEditorState;
 | 
						|
}
 | 
						|
 | 
						|
export function $applyTransforms(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  node: LexicalNode,
 | 
						|
  transformsCache: Map<string, Array<Transform<LexicalNode>>>,
 | 
						|
) {
 | 
						|
  const type = node.__type;
 | 
						|
  const registeredNode = getRegisteredNodeOrThrow(editor, type);
 | 
						|
  let transformsArr = transformsCache.get(type);
 | 
						|
 | 
						|
  if (transformsArr === undefined) {
 | 
						|
    transformsArr = Array.from(registeredNode.transforms);
 | 
						|
    transformsCache.set(type, transformsArr);
 | 
						|
  }
 | 
						|
 | 
						|
  const transformsArrLength = transformsArr.length;
 | 
						|
 | 
						|
  for (let i = 0; i < transformsArrLength; i++) {
 | 
						|
    transformsArr[i](node);
 | 
						|
 | 
						|
    if (!node.isAttached()) {
 | 
						|
      break;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function $isNodeValidForTransform(
 | 
						|
  node: LexicalNode,
 | 
						|
  compositionKey: null | string,
 | 
						|
): boolean {
 | 
						|
  return (
 | 
						|
    node !== undefined &&
 | 
						|
    // We don't want to transform nodes being composed
 | 
						|
    node.__key !== compositionKey &&
 | 
						|
    node.isAttached()
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function $normalizeAllDirtyTextNodes(
 | 
						|
  editorState: EditorState,
 | 
						|
  editor: LexicalEditor,
 | 
						|
): void {
 | 
						|
  const dirtyLeaves = editor._dirtyLeaves;
 | 
						|
  const nodeMap = editorState._nodeMap;
 | 
						|
 | 
						|
  for (const nodeKey of dirtyLeaves) {
 | 
						|
    const node = nodeMap.get(nodeKey);
 | 
						|
 | 
						|
    if (
 | 
						|
      $isTextNode(node) &&
 | 
						|
      node.isAttached() &&
 | 
						|
      node.isSimpleText() &&
 | 
						|
      !node.isUnmergeable()
 | 
						|
    ) {
 | 
						|
      $normalizeTextNode(node);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Transform heuristic:
 | 
						|
 * 1. We transform leaves first. If transforms generate additional dirty nodes we repeat step 1.
 | 
						|
 * The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.
 | 
						|
 * 2. We transform elements. If element transforms generate additional dirty nodes we repeat step 1.
 | 
						|
 * If element transforms only generate additional dirty elements we only repeat step 2.
 | 
						|
 *
 | 
						|
 * Note that to keep track of newly dirty nodes and subtrees we leverage the editor._dirtyNodes and
 | 
						|
 * editor._subtrees which we reset in every loop.
 | 
						|
 */
 | 
						|
function $applyAllTransforms(
 | 
						|
  editorState: EditorState,
 | 
						|
  editor: LexicalEditor,
 | 
						|
): void {
 | 
						|
  const dirtyLeaves = editor._dirtyLeaves;
 | 
						|
  const dirtyElements = editor._dirtyElements;
 | 
						|
  const nodeMap = editorState._nodeMap;
 | 
						|
  const compositionKey = $getCompositionKey();
 | 
						|
  const transformsCache = new Map();
 | 
						|
 | 
						|
  let untransformedDirtyLeaves = dirtyLeaves;
 | 
						|
  let untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
 | 
						|
  let untransformedDirtyElements = dirtyElements;
 | 
						|
  let untransformedDirtyElementsLength = untransformedDirtyElements.size;
 | 
						|
 | 
						|
  while (
 | 
						|
    untransformedDirtyLeavesLength > 0 ||
 | 
						|
    untransformedDirtyElementsLength > 0
 | 
						|
  ) {
 | 
						|
    if (untransformedDirtyLeavesLength > 0) {
 | 
						|
      // We leverage editor._dirtyLeaves to track the new dirty leaves after the transforms
 | 
						|
      editor._dirtyLeaves = new Set();
 | 
						|
 | 
						|
      for (const nodeKey of untransformedDirtyLeaves) {
 | 
						|
        const node = nodeMap.get(nodeKey);
 | 
						|
 | 
						|
        if (
 | 
						|
          $isTextNode(node) &&
 | 
						|
          node.isAttached() &&
 | 
						|
          node.isSimpleText() &&
 | 
						|
          !node.isUnmergeable()
 | 
						|
        ) {
 | 
						|
          $normalizeTextNode(node);
 | 
						|
        }
 | 
						|
 | 
						|
        if (
 | 
						|
          node !== undefined &&
 | 
						|
          $isNodeValidForTransform(node, compositionKey)
 | 
						|
        ) {
 | 
						|
          $applyTransforms(editor, node, transformsCache);
 | 
						|
        }
 | 
						|
 | 
						|
        dirtyLeaves.add(nodeKey);
 | 
						|
      }
 | 
						|
 | 
						|
      untransformedDirtyLeaves = editor._dirtyLeaves;
 | 
						|
      untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
 | 
						|
 | 
						|
      // We want to prioritize node transforms over element transforms
 | 
						|
      if (untransformedDirtyLeavesLength > 0) {
 | 
						|
        infiniteTransformCount++;
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // All dirty leaves have been processed. Let's do elements!
 | 
						|
    // We have previously processed dirty leaves, so let's restart the editor leaves Set to track
 | 
						|
    // new ones caused by element transforms
 | 
						|
    editor._dirtyLeaves = new Set();
 | 
						|
    editor._dirtyElements = new Map();
 | 
						|
 | 
						|
    for (const currentUntransformedDirtyElement of untransformedDirtyElements) {
 | 
						|
      const nodeKey = currentUntransformedDirtyElement[0];
 | 
						|
      const intentionallyMarkedAsDirty = currentUntransformedDirtyElement[1];
 | 
						|
      if (nodeKey !== 'root' && !intentionallyMarkedAsDirty) {
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      const node = nodeMap.get(nodeKey);
 | 
						|
 | 
						|
      if (
 | 
						|
        node !== undefined &&
 | 
						|
        $isNodeValidForTransform(node, compositionKey)
 | 
						|
      ) {
 | 
						|
        $applyTransforms(editor, node, transformsCache);
 | 
						|
      }
 | 
						|
 | 
						|
      dirtyElements.set(nodeKey, intentionallyMarkedAsDirty);
 | 
						|
    }
 | 
						|
 | 
						|
    untransformedDirtyLeaves = editor._dirtyLeaves;
 | 
						|
    untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
 | 
						|
    untransformedDirtyElements = editor._dirtyElements;
 | 
						|
    untransformedDirtyElementsLength = untransformedDirtyElements.size;
 | 
						|
    infiniteTransformCount++;
 | 
						|
  }
 | 
						|
 | 
						|
  editor._dirtyLeaves = dirtyLeaves;
 | 
						|
  editor._dirtyElements = dirtyElements;
 | 
						|
}
 | 
						|
 | 
						|
type InternalSerializedNode = {
 | 
						|
  children?: Array<InternalSerializedNode>;
 | 
						|
  type: string;
 | 
						|
  version: number;
 | 
						|
};
 | 
						|
 | 
						|
export function $parseSerializedNode(
 | 
						|
  serializedNode: SerializedLexicalNode,
 | 
						|
): LexicalNode {
 | 
						|
  const internalSerializedNode: InternalSerializedNode = serializedNode;
 | 
						|
  return $parseSerializedNodeImpl(
 | 
						|
    internalSerializedNode,
 | 
						|
    getActiveEditor()._nodes,
 | 
						|
  );
 | 
						|
}
 | 
						|
 | 
						|
function $parseSerializedNodeImpl<
 | 
						|
  SerializedNode extends InternalSerializedNode,
 | 
						|
>(
 | 
						|
  serializedNode: SerializedNode,
 | 
						|
  registeredNodes: RegisteredNodes,
 | 
						|
): LexicalNode {
 | 
						|
  const type = serializedNode.type;
 | 
						|
  const registeredNode = registeredNodes.get(type);
 | 
						|
 | 
						|
  if (registeredNode === undefined) {
 | 
						|
    invariant(false, 'parseEditorState: type "%s" + not found', type);
 | 
						|
  }
 | 
						|
 | 
						|
  const nodeClass = registeredNode.klass;
 | 
						|
 | 
						|
  if (serializedNode.type !== nodeClass.getType()) {
 | 
						|
    invariant(
 | 
						|
      false,
 | 
						|
      'LexicalNode: Node %s does not implement .importJSON().',
 | 
						|
      nodeClass.name,
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  const node = nodeClass.importJSON(serializedNode);
 | 
						|
  const children = serializedNode.children;
 | 
						|
 | 
						|
  if ($isElementNode(node) && Array.isArray(children)) {
 | 
						|
    for (let i = 0; i < children.length; i++) {
 | 
						|
      const serializedJSONChildNode = children[i];
 | 
						|
      const childNode = $parseSerializedNodeImpl(
 | 
						|
        serializedJSONChildNode,
 | 
						|
        registeredNodes,
 | 
						|
      );
 | 
						|
      node.append(childNode);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return node;
 | 
						|
}
 | 
						|
 | 
						|
export function parseEditorState(
 | 
						|
  serializedEditorState: SerializedEditorState,
 | 
						|
  editor: LexicalEditor,
 | 
						|
  updateFn: void | (() => void),
 | 
						|
): EditorState {
 | 
						|
  const editorState = createEmptyEditorState();
 | 
						|
  const previousActiveEditorState = activeEditorState;
 | 
						|
  const previousReadOnlyMode = isReadOnlyMode;
 | 
						|
  const previousActiveEditor = activeEditor;
 | 
						|
  const previousDirtyElements = editor._dirtyElements;
 | 
						|
  const previousDirtyLeaves = editor._dirtyLeaves;
 | 
						|
  const previousCloneNotNeeded = editor._cloneNotNeeded;
 | 
						|
  const previousDirtyType = editor._dirtyType;
 | 
						|
  editor._dirtyElements = new Map();
 | 
						|
  editor._dirtyLeaves = new Set();
 | 
						|
  editor._cloneNotNeeded = new Set();
 | 
						|
  editor._dirtyType = 0;
 | 
						|
  activeEditorState = editorState;
 | 
						|
  isReadOnlyMode = false;
 | 
						|
  activeEditor = editor;
 | 
						|
 | 
						|
  try {
 | 
						|
    const registeredNodes = editor._nodes;
 | 
						|
    const serializedNode = serializedEditorState.root;
 | 
						|
    $parseSerializedNodeImpl(serializedNode, registeredNodes);
 | 
						|
    if (updateFn) {
 | 
						|
      updateFn();
 | 
						|
    }
 | 
						|
 | 
						|
    // Make the editorState immutable
 | 
						|
    editorState._readOnly = true;
 | 
						|
 | 
						|
    if (__DEV__) {
 | 
						|
      handleDEVOnlyPendingUpdateGuarantees(editorState);
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    if (error instanceof Error) {
 | 
						|
      editor._onError(error);
 | 
						|
    }
 | 
						|
  } finally {
 | 
						|
    editor._dirtyElements = previousDirtyElements;
 | 
						|
    editor._dirtyLeaves = previousDirtyLeaves;
 | 
						|
    editor._cloneNotNeeded = previousCloneNotNeeded;
 | 
						|
    editor._dirtyType = previousDirtyType;
 | 
						|
    activeEditorState = previousActiveEditorState;
 | 
						|
    isReadOnlyMode = previousReadOnlyMode;
 | 
						|
    activeEditor = previousActiveEditor;
 | 
						|
  }
 | 
						|
 | 
						|
  return editorState;
 | 
						|
}
 | 
						|
 | 
						|
// This technically isn't an update but given we need
 | 
						|
// exposure to the module's active bindings, we have this
 | 
						|
// function here
 | 
						|
 | 
						|
export function readEditorState<V>(
 | 
						|
  editor: LexicalEditor | null,
 | 
						|
  editorState: EditorState,
 | 
						|
  callbackFn: () => V,
 | 
						|
): V {
 | 
						|
  const previousActiveEditorState = activeEditorState;
 | 
						|
  const previousReadOnlyMode = isReadOnlyMode;
 | 
						|
  const previousActiveEditor = activeEditor;
 | 
						|
 | 
						|
  activeEditorState = editorState;
 | 
						|
  isReadOnlyMode = true;
 | 
						|
  activeEditor = editor;
 | 
						|
 | 
						|
  try {
 | 
						|
    return callbackFn();
 | 
						|
  } finally {
 | 
						|
    activeEditorState = previousActiveEditorState;
 | 
						|
    isReadOnlyMode = previousReadOnlyMode;
 | 
						|
    activeEditor = previousActiveEditor;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function handleDEVOnlyPendingUpdateGuarantees(
 | 
						|
  pendingEditorState: EditorState,
 | 
						|
): void {
 | 
						|
  // Given we can't Object.freeze the nodeMap as it's a Map,
 | 
						|
  // we instead replace its set, clear and delete methods.
 | 
						|
  const nodeMap = pendingEditorState._nodeMap;
 | 
						|
 | 
						|
  nodeMap.set = () => {
 | 
						|
    throw new Error('Cannot call set() on a frozen Lexical node map');
 | 
						|
  };
 | 
						|
 | 
						|
  nodeMap.clear = () => {
 | 
						|
    throw new Error('Cannot call clear() on a frozen Lexical node map');
 | 
						|
  };
 | 
						|
 | 
						|
  nodeMap.delete = () => {
 | 
						|
    throw new Error('Cannot call delete() on a frozen Lexical node map');
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export function $commitPendingUpdates(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  recoveryEditorState?: EditorState,
 | 
						|
): void {
 | 
						|
  const pendingEditorState = editor._pendingEditorState;
 | 
						|
  const rootElement = editor._rootElement;
 | 
						|
  const shouldSkipDOM = editor._headless || rootElement === null;
 | 
						|
 | 
						|
  if (pendingEditorState === null) {
 | 
						|
    return;
 | 
						|
  }
 | 
						|
 | 
						|
  // ======
 | 
						|
  // Reconciliation has started.
 | 
						|
  // ======
 | 
						|
 | 
						|
  const currentEditorState = editor._editorState;
 | 
						|
  const currentSelection = currentEditorState._selection;
 | 
						|
  const pendingSelection = pendingEditorState._selection;
 | 
						|
  const needsUpdate = editor._dirtyType !== NO_DIRTY_NODES;
 | 
						|
  const previousActiveEditorState = activeEditorState;
 | 
						|
  const previousReadOnlyMode = isReadOnlyMode;
 | 
						|
  const previousActiveEditor = activeEditor;
 | 
						|
  const previouslyUpdating = editor._updating;
 | 
						|
  const observer = editor._observer;
 | 
						|
  let mutatedNodes = null;
 | 
						|
  editor._pendingEditorState = null;
 | 
						|
  editor._editorState = pendingEditorState;
 | 
						|
 | 
						|
  if (!shouldSkipDOM && needsUpdate && observer !== null) {
 | 
						|
    activeEditor = editor;
 | 
						|
    activeEditorState = pendingEditorState;
 | 
						|
    isReadOnlyMode = false;
 | 
						|
    // We don't want updates to sync block the reconciliation.
 | 
						|
    editor._updating = true;
 | 
						|
    try {
 | 
						|
      const dirtyType = editor._dirtyType;
 | 
						|
      const dirtyElements = editor._dirtyElements;
 | 
						|
      const dirtyLeaves = editor._dirtyLeaves;
 | 
						|
      observer.disconnect();
 | 
						|
 | 
						|
      mutatedNodes = $reconcileRoot(
 | 
						|
        currentEditorState,
 | 
						|
        pendingEditorState,
 | 
						|
        editor,
 | 
						|
        dirtyType,
 | 
						|
        dirtyElements,
 | 
						|
        dirtyLeaves,
 | 
						|
      );
 | 
						|
    } catch (error) {
 | 
						|
      // Report errors
 | 
						|
      if (error instanceof Error) {
 | 
						|
        editor._onError(error);
 | 
						|
      }
 | 
						|
 | 
						|
      // Reset editor and restore incoming editor state to the DOM
 | 
						|
      if (!isAttemptingToRecoverFromReconcilerError) {
 | 
						|
        resetEditor(editor, null, rootElement, pendingEditorState);
 | 
						|
        initMutationObserver(editor);
 | 
						|
        editor._dirtyType = FULL_RECONCILE;
 | 
						|
        isAttemptingToRecoverFromReconcilerError = true;
 | 
						|
        $commitPendingUpdates(editor, currentEditorState);
 | 
						|
        isAttemptingToRecoverFromReconcilerError = false;
 | 
						|
      } else {
 | 
						|
        // To avoid a possible situation of infinite loops, lets throw
 | 
						|
        throw error;
 | 
						|
      }
 | 
						|
 | 
						|
      return;
 | 
						|
    } finally {
 | 
						|
      observer.observe(rootElement as Node, observerOptions);
 | 
						|
      editor._updating = previouslyUpdating;
 | 
						|
      activeEditorState = previousActiveEditorState;
 | 
						|
      isReadOnlyMode = previousReadOnlyMode;
 | 
						|
      activeEditor = previousActiveEditor;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (!pendingEditorState._readOnly) {
 | 
						|
    pendingEditorState._readOnly = true;
 | 
						|
    if (__DEV__) {
 | 
						|
      handleDEVOnlyPendingUpdateGuarantees(pendingEditorState);
 | 
						|
      if ($isRangeSelection(pendingSelection)) {
 | 
						|
        Object.freeze(pendingSelection.anchor);
 | 
						|
        Object.freeze(pendingSelection.focus);
 | 
						|
      }
 | 
						|
      Object.freeze(pendingSelection);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  const dirtyLeaves = editor._dirtyLeaves;
 | 
						|
  const dirtyElements = editor._dirtyElements;
 | 
						|
  const normalizedNodes = editor._normalizedNodes;
 | 
						|
  const tags = editor._updateTags;
 | 
						|
  const deferred = editor._deferred;
 | 
						|
  const nodeCount = pendingEditorState._nodeMap.size;
 | 
						|
 | 
						|
  if (needsUpdate) {
 | 
						|
    editor._dirtyType = NO_DIRTY_NODES;
 | 
						|
    editor._cloneNotNeeded.clear();
 | 
						|
    editor._dirtyLeaves = new Set();
 | 
						|
    editor._dirtyElements = new Map();
 | 
						|
    editor._normalizedNodes = new Set();
 | 
						|
    editor._updateTags = new Set();
 | 
						|
  }
 | 
						|
  $garbageCollectDetachedDecorators(editor, pendingEditorState);
 | 
						|
 | 
						|
  // ======
 | 
						|
  // Reconciliation has finished. Now update selection and trigger listeners.
 | 
						|
  // ======
 | 
						|
 | 
						|
  const domSelection = shouldSkipDOM ? null : getDOMSelection(editor._window);
 | 
						|
 | 
						|
  // Attempt to update the DOM selection, including focusing of the root element,
 | 
						|
  // and scroll into view if needed.
 | 
						|
  if (
 | 
						|
    editor._editable &&
 | 
						|
    // domSelection will be null in headless
 | 
						|
    domSelection !== null &&
 | 
						|
    (needsUpdate || pendingSelection === null || pendingSelection.dirty)
 | 
						|
  ) {
 | 
						|
    activeEditor = editor;
 | 
						|
    activeEditorState = pendingEditorState;
 | 
						|
    try {
 | 
						|
      if (observer !== null) {
 | 
						|
        observer.disconnect();
 | 
						|
      }
 | 
						|
      if (needsUpdate || pendingSelection === null || pendingSelection.dirty) {
 | 
						|
        const blockCursorElement = editor._blockCursorElement;
 | 
						|
        if (blockCursorElement !== null) {
 | 
						|
          removeDOMBlockCursorElement(
 | 
						|
            blockCursorElement,
 | 
						|
            editor,
 | 
						|
            rootElement as HTMLElement,
 | 
						|
          );
 | 
						|
        }
 | 
						|
        updateDOMSelection(
 | 
						|
          currentSelection,
 | 
						|
          pendingSelection,
 | 
						|
          editor,
 | 
						|
          domSelection,
 | 
						|
          tags,
 | 
						|
          rootElement as HTMLElement,
 | 
						|
          nodeCount,
 | 
						|
        );
 | 
						|
      }
 | 
						|
      updateDOMBlockCursorElement(
 | 
						|
        editor,
 | 
						|
        rootElement as HTMLElement,
 | 
						|
        pendingSelection,
 | 
						|
      );
 | 
						|
      if (observer !== null) {
 | 
						|
        observer.observe(rootElement as Node, observerOptions);
 | 
						|
      }
 | 
						|
    } finally {
 | 
						|
      activeEditor = previousActiveEditor;
 | 
						|
      activeEditorState = previousActiveEditorState;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (mutatedNodes !== null) {
 | 
						|
    triggerMutationListeners(
 | 
						|
      editor,
 | 
						|
      mutatedNodes,
 | 
						|
      tags,
 | 
						|
      dirtyLeaves,
 | 
						|
      currentEditorState,
 | 
						|
    );
 | 
						|
  }
 | 
						|
  if (
 | 
						|
    !$isRangeSelection(pendingSelection) &&
 | 
						|
    pendingSelection !== null &&
 | 
						|
    (currentSelection === null || !currentSelection.is(pendingSelection))
 | 
						|
  ) {
 | 
						|
    editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
 | 
						|
  }
 | 
						|
  /**
 | 
						|
   * Capture pendingDecorators after garbage collecting detached decorators
 | 
						|
   */
 | 
						|
  const pendingDecorators = editor._pendingDecorators;
 | 
						|
  if (pendingDecorators !== null) {
 | 
						|
    editor._decorators = pendingDecorators;
 | 
						|
    editor._pendingDecorators = null;
 | 
						|
    triggerListeners('decorator', editor, true, pendingDecorators);
 | 
						|
  }
 | 
						|
 | 
						|
  // If reconciler fails, we reset whole editor (so current editor state becomes empty)
 | 
						|
  // and attempt to re-render pendingEditorState. If that goes through we trigger
 | 
						|
  // listeners, but instead use recoverEditorState which is current editor state before reset
 | 
						|
  // This specifically important for collab that relies on prevEditorState from update
 | 
						|
  // listener to calculate delta of changed nodes/properties
 | 
						|
  triggerTextContentListeners(
 | 
						|
    editor,
 | 
						|
    recoveryEditorState || currentEditorState,
 | 
						|
    pendingEditorState,
 | 
						|
  );
 | 
						|
  triggerListeners('update', editor, true, {
 | 
						|
    dirtyElements,
 | 
						|
    dirtyLeaves,
 | 
						|
    editorState: pendingEditorState,
 | 
						|
    normalizedNodes,
 | 
						|
    prevEditorState: recoveryEditorState || currentEditorState,
 | 
						|
    tags,
 | 
						|
  });
 | 
						|
  triggerDeferredUpdateCallbacks(editor, deferred);
 | 
						|
  $triggerEnqueuedUpdates(editor);
 | 
						|
}
 | 
						|
 | 
						|
function triggerTextContentListeners(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  currentEditorState: EditorState,
 | 
						|
  pendingEditorState: EditorState,
 | 
						|
): void {
 | 
						|
  const currentTextContent = getEditorStateTextContent(currentEditorState);
 | 
						|
  const latestTextContent = getEditorStateTextContent(pendingEditorState);
 | 
						|
 | 
						|
  if (currentTextContent !== latestTextContent) {
 | 
						|
    triggerListeners('textcontent', editor, true, latestTextContent);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function triggerMutationListeners(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  mutatedNodes: MutatedNodes,
 | 
						|
  updateTags: Set<string>,
 | 
						|
  dirtyLeaves: Set<string>,
 | 
						|
  prevEditorState: EditorState,
 | 
						|
): void {
 | 
						|
  const listeners = Array.from(editor._listeners.mutation);
 | 
						|
  const listenersLength = listeners.length;
 | 
						|
 | 
						|
  for (let i = 0; i < listenersLength; i++) {
 | 
						|
    const [listener, klass] = listeners[i];
 | 
						|
    const mutatedNodesByType = mutatedNodes.get(klass);
 | 
						|
    if (mutatedNodesByType !== undefined) {
 | 
						|
      listener(mutatedNodesByType, {
 | 
						|
        dirtyLeaves,
 | 
						|
        prevEditorState,
 | 
						|
        updateTags,
 | 
						|
      });
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function triggerListeners(
 | 
						|
  type: 'update' | 'root' | 'decorator' | 'textcontent' | 'editable',
 | 
						|
  editor: LexicalEditor,
 | 
						|
  isCurrentlyEnqueuingUpdates: boolean,
 | 
						|
  ...payload: unknown[]
 | 
						|
): void {
 | 
						|
  const previouslyUpdating = editor._updating;
 | 
						|
  editor._updating = isCurrentlyEnqueuingUpdates;
 | 
						|
 | 
						|
  try {
 | 
						|
    const listeners = Array.from<Listener>(editor._listeners[type]);
 | 
						|
    for (let i = 0; i < listeners.length; i++) {
 | 
						|
      // @ts-ignore
 | 
						|
      listeners[i].apply(null, payload);
 | 
						|
    }
 | 
						|
  } finally {
 | 
						|
    editor._updating = previouslyUpdating;
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function triggerCommandListeners<
 | 
						|
  TCommand extends LexicalCommand<unknown>,
 | 
						|
>(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  type: TCommand,
 | 
						|
  payload: CommandPayloadType<TCommand>,
 | 
						|
): boolean {
 | 
						|
  if (editor._updating === false || activeEditor !== editor) {
 | 
						|
    let returnVal = false;
 | 
						|
    editor.update(() => {
 | 
						|
      returnVal = triggerCommandListeners(editor, type, payload);
 | 
						|
    });
 | 
						|
    return returnVal;
 | 
						|
  }
 | 
						|
 | 
						|
  const editors = getEditorsToPropagate(editor);
 | 
						|
 | 
						|
  for (let i = 4; i >= 0; i--) {
 | 
						|
    for (let e = 0; e < editors.length; e++) {
 | 
						|
      const currentEditor = editors[e];
 | 
						|
      const commandListeners = currentEditor._commands;
 | 
						|
      const listenerInPriorityOrder = commandListeners.get(type);
 | 
						|
 | 
						|
      if (listenerInPriorityOrder !== undefined) {
 | 
						|
        const listenersSet = listenerInPriorityOrder[i];
 | 
						|
 | 
						|
        if (listenersSet !== undefined) {
 | 
						|
          const listeners = Array.from(listenersSet);
 | 
						|
          const listenersLength = listeners.length;
 | 
						|
 | 
						|
          for (let j = 0; j < listenersLength; j++) {
 | 
						|
            if (listeners[j](payload, editor) === true) {
 | 
						|
              return true;
 | 
						|
            }
 | 
						|
          }
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return false;
 | 
						|
}
 | 
						|
 | 
						|
function $triggerEnqueuedUpdates(editor: LexicalEditor): void {
 | 
						|
  const queuedUpdates = editor._updates;
 | 
						|
 | 
						|
  if (queuedUpdates.length !== 0) {
 | 
						|
    const queuedUpdate = queuedUpdates.shift();
 | 
						|
    if (queuedUpdate) {
 | 
						|
      const [updateFn, options] = queuedUpdate;
 | 
						|
      $beginUpdate(editor, updateFn, options);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function triggerDeferredUpdateCallbacks(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  deferred: Array<() => void>,
 | 
						|
): void {
 | 
						|
  editor._deferred = [];
 | 
						|
 | 
						|
  if (deferred.length !== 0) {
 | 
						|
    const previouslyUpdating = editor._updating;
 | 
						|
    editor._updating = true;
 | 
						|
 | 
						|
    try {
 | 
						|
      for (let i = 0; i < deferred.length; i++) {
 | 
						|
        deferred[i]();
 | 
						|
      }
 | 
						|
    } finally {
 | 
						|
      editor._updating = previouslyUpdating;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
function processNestedUpdates(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  initialSkipTransforms?: boolean,
 | 
						|
): boolean {
 | 
						|
  const queuedUpdates = editor._updates;
 | 
						|
  let skipTransforms = initialSkipTransforms || false;
 | 
						|
 | 
						|
  // Updates might grow as we process them, we so we'll need
 | 
						|
  // to handle each update as we go until the updates array is
 | 
						|
  // empty.
 | 
						|
  while (queuedUpdates.length !== 0) {
 | 
						|
    const queuedUpdate = queuedUpdates.shift();
 | 
						|
    if (queuedUpdate) {
 | 
						|
      const [nextUpdateFn, options] = queuedUpdate;
 | 
						|
 | 
						|
      let onUpdate;
 | 
						|
      let tag;
 | 
						|
 | 
						|
      if (options !== undefined) {
 | 
						|
        onUpdate = options.onUpdate;
 | 
						|
        tag = options.tag;
 | 
						|
 | 
						|
        if (options.skipTransforms) {
 | 
						|
          skipTransforms = true;
 | 
						|
        }
 | 
						|
        if (options.discrete) {
 | 
						|
          const pendingEditorState = editor._pendingEditorState;
 | 
						|
          invariant(
 | 
						|
            pendingEditorState !== null,
 | 
						|
            'Unexpected empty pending editor state on discrete nested update',
 | 
						|
          );
 | 
						|
          pendingEditorState._flushSync = true;
 | 
						|
        }
 | 
						|
 | 
						|
        if (onUpdate) {
 | 
						|
          editor._deferred.push(onUpdate);
 | 
						|
        }
 | 
						|
 | 
						|
        if (tag) {
 | 
						|
          editor._updateTags.add(tag);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      nextUpdateFn();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return skipTransforms;
 | 
						|
}
 | 
						|
 | 
						|
function $beginUpdate(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  updateFn: () => void,
 | 
						|
  options?: EditorUpdateOptions,
 | 
						|
): void {
 | 
						|
  const updateTags = editor._updateTags;
 | 
						|
  let onUpdate;
 | 
						|
  let tag;
 | 
						|
  let skipTransforms = false;
 | 
						|
  let discrete = false;
 | 
						|
 | 
						|
  if (options !== undefined) {
 | 
						|
    onUpdate = options.onUpdate;
 | 
						|
    tag = options.tag;
 | 
						|
 | 
						|
    if (tag != null) {
 | 
						|
      updateTags.add(tag);
 | 
						|
    }
 | 
						|
 | 
						|
    skipTransforms = options.skipTransforms || false;
 | 
						|
    discrete = options.discrete || false;
 | 
						|
  }
 | 
						|
 | 
						|
  if (onUpdate) {
 | 
						|
    editor._deferred.push(onUpdate);
 | 
						|
  }
 | 
						|
 | 
						|
  const currentEditorState = editor._editorState;
 | 
						|
  let pendingEditorState = editor._pendingEditorState;
 | 
						|
  let editorStateWasCloned = false;
 | 
						|
 | 
						|
  if (pendingEditorState === null || pendingEditorState._readOnly) {
 | 
						|
    pendingEditorState = editor._pendingEditorState = cloneEditorState(
 | 
						|
      pendingEditorState || currentEditorState,
 | 
						|
    );
 | 
						|
    editorStateWasCloned = true;
 | 
						|
  }
 | 
						|
  pendingEditorState._flushSync = discrete;
 | 
						|
 | 
						|
  const previousActiveEditorState = activeEditorState;
 | 
						|
  const previousReadOnlyMode = isReadOnlyMode;
 | 
						|
  const previousActiveEditor = activeEditor;
 | 
						|
  const previouslyUpdating = editor._updating;
 | 
						|
  activeEditorState = pendingEditorState;
 | 
						|
  isReadOnlyMode = false;
 | 
						|
  editor._updating = true;
 | 
						|
  activeEditor = editor;
 | 
						|
 | 
						|
  try {
 | 
						|
    if (editorStateWasCloned) {
 | 
						|
      if (editor._headless) {
 | 
						|
        if (currentEditorState._selection !== null) {
 | 
						|
          pendingEditorState._selection = currentEditorState._selection.clone();
 | 
						|
        }
 | 
						|
      } else {
 | 
						|
        pendingEditorState._selection = $internalCreateSelection(editor);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    const startingCompositionKey = editor._compositionKey;
 | 
						|
    updateFn();
 | 
						|
    skipTransforms = processNestedUpdates(editor, skipTransforms);
 | 
						|
    applySelectionTransforms(pendingEditorState, editor);
 | 
						|
 | 
						|
    if (editor._dirtyType !== NO_DIRTY_NODES) {
 | 
						|
      if (skipTransforms) {
 | 
						|
        $normalizeAllDirtyTextNodes(pendingEditorState, editor);
 | 
						|
      } else {
 | 
						|
        $applyAllTransforms(pendingEditorState, editor);
 | 
						|
      }
 | 
						|
 | 
						|
      processNestedUpdates(editor);
 | 
						|
      $garbageCollectDetachedNodes(
 | 
						|
        currentEditorState,
 | 
						|
        pendingEditorState,
 | 
						|
        editor._dirtyLeaves,
 | 
						|
        editor._dirtyElements,
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const endingCompositionKey = editor._compositionKey;
 | 
						|
 | 
						|
    if (startingCompositionKey !== endingCompositionKey) {
 | 
						|
      pendingEditorState._flushSync = true;
 | 
						|
    }
 | 
						|
 | 
						|
    const pendingSelection = pendingEditorState._selection;
 | 
						|
 | 
						|
    if ($isRangeSelection(pendingSelection)) {
 | 
						|
      const pendingNodeMap = pendingEditorState._nodeMap;
 | 
						|
      const anchorKey = pendingSelection.anchor.key;
 | 
						|
      const focusKey = pendingSelection.focus.key;
 | 
						|
 | 
						|
      if (
 | 
						|
        pendingNodeMap.get(anchorKey) === undefined ||
 | 
						|
        pendingNodeMap.get(focusKey) === undefined
 | 
						|
      ) {
 | 
						|
        invariant(
 | 
						|
          false,
 | 
						|
          'updateEditor: selection has been lost because the previously selected nodes have been removed and ' +
 | 
						|
            "selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.",
 | 
						|
        );
 | 
						|
      }
 | 
						|
    } else if ($isNodeSelection(pendingSelection)) {
 | 
						|
      // TODO: we should also validate node selection?
 | 
						|
      if (pendingSelection._nodes.size === 0) {
 | 
						|
        pendingEditorState._selection = null;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  } catch (error) {
 | 
						|
    // Report errors
 | 
						|
    if (error instanceof Error) {
 | 
						|
      editor._onError(error);
 | 
						|
    }
 | 
						|
 | 
						|
    // Restore existing editor state to the DOM
 | 
						|
    editor._pendingEditorState = currentEditorState;
 | 
						|
    editor._dirtyType = FULL_RECONCILE;
 | 
						|
 | 
						|
    editor._cloneNotNeeded.clear();
 | 
						|
 | 
						|
    editor._dirtyLeaves = new Set();
 | 
						|
 | 
						|
    editor._dirtyElements.clear();
 | 
						|
 | 
						|
    $commitPendingUpdates(editor);
 | 
						|
    return;
 | 
						|
  } finally {
 | 
						|
    activeEditorState = previousActiveEditorState;
 | 
						|
    isReadOnlyMode = previousReadOnlyMode;
 | 
						|
    activeEditor = previousActiveEditor;
 | 
						|
    editor._updating = previouslyUpdating;
 | 
						|
    infiniteTransformCount = 0;
 | 
						|
  }
 | 
						|
 | 
						|
  const shouldUpdate =
 | 
						|
    editor._dirtyType !== NO_DIRTY_NODES ||
 | 
						|
    editorStateHasDirtySelection(pendingEditorState, editor);
 | 
						|
 | 
						|
  if (shouldUpdate) {
 | 
						|
    if (pendingEditorState._flushSync) {
 | 
						|
      pendingEditorState._flushSync = false;
 | 
						|
      $commitPendingUpdates(editor);
 | 
						|
    } else if (editorStateWasCloned) {
 | 
						|
      scheduleMicroTask(() => {
 | 
						|
        $commitPendingUpdates(editor);
 | 
						|
      });
 | 
						|
    }
 | 
						|
  } else {
 | 
						|
    pendingEditorState._flushSync = false;
 | 
						|
 | 
						|
    if (editorStateWasCloned) {
 | 
						|
      updateTags.clear();
 | 
						|
      editor._deferred = [];
 | 
						|
      editor._pendingEditorState = null;
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
export function updateEditor(
 | 
						|
  editor: LexicalEditor,
 | 
						|
  updateFn: () => void,
 | 
						|
  options?: EditorUpdateOptions,
 | 
						|
): void {
 | 
						|
  if (editor._updating) {
 | 
						|
    editor._updates.push([updateFn, options]);
 | 
						|
  } else {
 | 
						|
    $beginUpdate(editor, updateFn, options);
 | 
						|
  }
 | 
						|
}
 |