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.
9 import type {SerializedEditorState} from './LexicalEditorState';
10 import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';
12 import invariant from 'lexical/shared/invariant';
14 import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
15 import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
26 } from './LexicalEditor';
29 createEmptyEditorState,
31 editorStateHasDirtySelection,
32 } from './LexicalEditorState';
34 $garbageCollectDetachedDecorators,
35 $garbageCollectDetachedNodes,
37 import {initMutationObserver} from './LexicalMutations';
38 import {$normalizeTextNode} from './LexicalNormalization';
39 import {$reconcileRoot} from './LexicalReconciler';
41 $internalCreateSelection,
44 applySelectionTransforms,
46 } from './LexicalSelection';
50 getEditorPropertyFromDOMNode,
51 getEditorStateTextContent,
52 getEditorsToPropagate,
53 getRegisteredNodeOrThrow,
55 removeDOMBlockCursorElement,
57 updateDOMBlockCursorElement,
58 } from './LexicalUtils';
60 let activeEditorState: null | EditorState = null;
61 let activeEditor: null | LexicalEditor = null;
62 let isReadOnlyMode = false;
63 let isAttemptingToRecoverFromReconcilerError = false;
64 let infiniteTransformCount = 0;
66 const observerOptions = {
72 export function isCurrentlyReadOnlyMode(): boolean {
75 (activeEditorState !== null && activeEditorState._readOnly)
79 export function errorOnReadOnly(): void {
81 invariant(false, 'Cannot use method in read-only mode.');
85 export function errorOnInfiniteTransforms(): void {
86 if (infiniteTransformCount > 99) {
89 '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.',
94 export function getActiveEditorState(): EditorState {
95 if (activeEditorState === null) {
98 'Unable to find an active editor state. ' +
99 'State helpers or node methods can only be used ' +
100 'synchronously during the callback of ' +
101 'editor.update(), editor.read(), or editorState.read().%s',
102 collectBuildInformation(),
106 return activeEditorState;
109 export function getActiveEditor(): LexicalEditor {
110 if (activeEditor === null) {
113 'Unable to find an active editor. ' +
114 'This method can only be used ' +
115 'synchronously during the callback of ' +
116 'editor.update() or editor.read().%s',
117 collectBuildInformation(),
123 function collectBuildInformation(): string {
124 let compatibleEditors = 0;
125 const incompatibleEditors = new Set<string>();
126 const thisVersion = LexicalEditor.version;
127 if (typeof window !== 'undefined') {
128 for (const node of document.querySelectorAll('[contenteditable]')) {
129 const editor = getEditorPropertyFromDOMNode(node);
130 if (isLexicalEditor(editor)) {
133 let version = String(
135 editor.constructor as typeof editor['constructor'] &
136 Record<string, unknown>
137 ).version || '<0.17.1',
139 if (version === thisVersion) {
141 ' (separately built, likely a bundler configuration issue)';
143 incompatibleEditors.add(version);
147 let output = ` Detected on the page: ${compatibleEditors} compatible editor(s) with version ${thisVersion}`;
148 if (incompatibleEditors.size) {
149 output += ` and incompatible editors with versions ${Array.from(
156 export function internalGetActiveEditor(): LexicalEditor | null {
160 export function internalGetActiveEditorState(): EditorState | null {
161 return activeEditorState;
164 export function $applyTransforms(
165 editor: LexicalEditor,
167 transformsCache: Map<string, Array<Transform<LexicalNode>>>,
169 const type = node.__type;
170 const registeredNode = getRegisteredNodeOrThrow(editor, type);
171 let transformsArr = transformsCache.get(type);
173 if (transformsArr === undefined) {
174 transformsArr = Array.from(registeredNode.transforms);
175 transformsCache.set(type, transformsArr);
178 const transformsArrLength = transformsArr.length;
180 for (let i = 0; i < transformsArrLength; i++) {
181 transformsArr[i](node);
183 if (!node.isAttached()) {
189 function $isNodeValidForTransform(
191 compositionKey: null | string,
194 node !== undefined &&
195 // We don't want to transform nodes being composed
196 node.__key !== compositionKey &&
201 function $normalizeAllDirtyTextNodes(
202 editorState: EditorState,
203 editor: LexicalEditor,
205 const dirtyLeaves = editor._dirtyLeaves;
206 const nodeMap = editorState._nodeMap;
208 for (const nodeKey of dirtyLeaves) {
209 const node = nodeMap.get(nodeKey);
214 node.isSimpleText() &&
215 !node.isUnmergeable()
217 $normalizeTextNode(node);
223 * Transform heuristic:
224 * 1. We transform leaves first. If transforms generate additional dirty nodes we repeat step 1.
225 * The reasoning behind this is that marking a leaf as dirty marks all its parent elements as dirty too.
226 * 2. We transform elements. If element transforms generate additional dirty nodes we repeat step 1.
227 * If element transforms only generate additional dirty elements we only repeat step 2.
229 * Note that to keep track of newly dirty nodes and subtrees we leverage the editor._dirtyNodes and
230 * editor._subtrees which we reset in every loop.
232 function $applyAllTransforms(
233 editorState: EditorState,
234 editor: LexicalEditor,
236 const dirtyLeaves = editor._dirtyLeaves;
237 const dirtyElements = editor._dirtyElements;
238 const nodeMap = editorState._nodeMap;
239 const compositionKey = $getCompositionKey();
240 const transformsCache = new Map();
242 let untransformedDirtyLeaves = dirtyLeaves;
243 let untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
244 let untransformedDirtyElements = dirtyElements;
245 let untransformedDirtyElementsLength = untransformedDirtyElements.size;
248 untransformedDirtyLeavesLength > 0 ||
249 untransformedDirtyElementsLength > 0
251 if (untransformedDirtyLeavesLength > 0) {
252 // We leverage editor._dirtyLeaves to track the new dirty leaves after the transforms
253 editor._dirtyLeaves = new Set();
255 for (const nodeKey of untransformedDirtyLeaves) {
256 const node = nodeMap.get(nodeKey);
261 node.isSimpleText() &&
262 !node.isUnmergeable()
264 $normalizeTextNode(node);
268 node !== undefined &&
269 $isNodeValidForTransform(node, compositionKey)
271 $applyTransforms(editor, node, transformsCache);
274 dirtyLeaves.add(nodeKey);
277 untransformedDirtyLeaves = editor._dirtyLeaves;
278 untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
280 // We want to prioritize node transforms over element transforms
281 if (untransformedDirtyLeavesLength > 0) {
282 infiniteTransformCount++;
287 // All dirty leaves have been processed. Let's do elements!
288 // We have previously processed dirty leaves, so let's restart the editor leaves Set to track
289 // new ones caused by element transforms
290 editor._dirtyLeaves = new Set();
291 editor._dirtyElements = new Map();
293 for (const currentUntransformedDirtyElement of untransformedDirtyElements) {
294 const nodeKey = currentUntransformedDirtyElement[0];
295 const intentionallyMarkedAsDirty = currentUntransformedDirtyElement[1];
296 if (nodeKey !== 'root' && !intentionallyMarkedAsDirty) {
300 const node = nodeMap.get(nodeKey);
303 node !== undefined &&
304 $isNodeValidForTransform(node, compositionKey)
306 $applyTransforms(editor, node, transformsCache);
309 dirtyElements.set(nodeKey, intentionallyMarkedAsDirty);
312 untransformedDirtyLeaves = editor._dirtyLeaves;
313 untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
314 untransformedDirtyElements = editor._dirtyElements;
315 untransformedDirtyElementsLength = untransformedDirtyElements.size;
316 infiniteTransformCount++;
319 editor._dirtyLeaves = dirtyLeaves;
320 editor._dirtyElements = dirtyElements;
323 type InternalSerializedNode = {
324 children?: Array<InternalSerializedNode>;
329 export function $parseSerializedNode(
330 serializedNode: SerializedLexicalNode,
332 const internalSerializedNode: InternalSerializedNode = serializedNode;
333 return $parseSerializedNodeImpl(
334 internalSerializedNode,
335 getActiveEditor()._nodes,
339 function $parseSerializedNodeImpl<
340 SerializedNode extends InternalSerializedNode,
342 serializedNode: SerializedNode,
343 registeredNodes: RegisteredNodes,
345 const type = serializedNode.type;
346 const registeredNode = registeredNodes.get(type);
348 if (registeredNode === undefined) {
349 invariant(false, 'parseEditorState: type "%s" + not found', type);
352 const nodeClass = registeredNode.klass;
354 if (serializedNode.type !== nodeClass.getType()) {
357 'LexicalNode: Node %s does not implement .importJSON().',
362 const node = nodeClass.importJSON(serializedNode);
363 const children = serializedNode.children;
365 if ($isElementNode(node) && Array.isArray(children)) {
366 for (let i = 0; i < children.length; i++) {
367 const serializedJSONChildNode = children[i];
368 const childNode = $parseSerializedNodeImpl(
369 serializedJSONChildNode,
372 node.append(childNode);
379 export function parseEditorState(
380 serializedEditorState: SerializedEditorState,
381 editor: LexicalEditor,
382 updateFn: void | (() => void),
384 const editorState = createEmptyEditorState();
385 const previousActiveEditorState = activeEditorState;
386 const previousReadOnlyMode = isReadOnlyMode;
387 const previousActiveEditor = activeEditor;
388 const previousDirtyElements = editor._dirtyElements;
389 const previousDirtyLeaves = editor._dirtyLeaves;
390 const previousCloneNotNeeded = editor._cloneNotNeeded;
391 const previousDirtyType = editor._dirtyType;
392 editor._dirtyElements = new Map();
393 editor._dirtyLeaves = new Set();
394 editor._cloneNotNeeded = new Set();
395 editor._dirtyType = 0;
396 activeEditorState = editorState;
397 isReadOnlyMode = false;
398 activeEditor = editor;
401 const registeredNodes = editor._nodes;
402 const serializedNode = serializedEditorState.root;
403 $parseSerializedNodeImpl(serializedNode, registeredNodes);
408 // Make the editorState immutable
409 editorState._readOnly = true;
412 handleDEVOnlyPendingUpdateGuarantees(editorState);
415 if (error instanceof Error) {
416 editor._onError(error);
419 editor._dirtyElements = previousDirtyElements;
420 editor._dirtyLeaves = previousDirtyLeaves;
421 editor._cloneNotNeeded = previousCloneNotNeeded;
422 editor._dirtyType = previousDirtyType;
423 activeEditorState = previousActiveEditorState;
424 isReadOnlyMode = previousReadOnlyMode;
425 activeEditor = previousActiveEditor;
431 // This technically isn't an update but given we need
432 // exposure to the module's active bindings, we have this
435 export function readEditorState<V>(
436 editor: LexicalEditor | null,
437 editorState: EditorState,
440 const previousActiveEditorState = activeEditorState;
441 const previousReadOnlyMode = isReadOnlyMode;
442 const previousActiveEditor = activeEditor;
444 activeEditorState = editorState;
445 isReadOnlyMode = true;
446 activeEditor = editor;
451 activeEditorState = previousActiveEditorState;
452 isReadOnlyMode = previousReadOnlyMode;
453 activeEditor = previousActiveEditor;
457 function handleDEVOnlyPendingUpdateGuarantees(
458 pendingEditorState: EditorState,
460 // Given we can't Object.freeze the nodeMap as it's a Map,
461 // we instead replace its set, clear and delete methods.
462 const nodeMap = pendingEditorState._nodeMap;
464 nodeMap.set = () => {
465 throw new Error('Cannot call set() on a frozen Lexical node map');
468 nodeMap.clear = () => {
469 throw new Error('Cannot call clear() on a frozen Lexical node map');
472 nodeMap.delete = () => {
473 throw new Error('Cannot call delete() on a frozen Lexical node map');
477 export function $commitPendingUpdates(
478 editor: LexicalEditor,
479 recoveryEditorState?: EditorState,
481 const pendingEditorState = editor._pendingEditorState;
482 const rootElement = editor._rootElement;
483 const shouldSkipDOM = editor._headless || rootElement === null;
485 if (pendingEditorState === null) {
490 // Reconciliation has started.
493 const currentEditorState = editor._editorState;
494 const currentSelection = currentEditorState._selection;
495 const pendingSelection = pendingEditorState._selection;
496 const needsUpdate = editor._dirtyType !== NO_DIRTY_NODES;
497 const previousActiveEditorState = activeEditorState;
498 const previousReadOnlyMode = isReadOnlyMode;
499 const previousActiveEditor = activeEditor;
500 const previouslyUpdating = editor._updating;
501 const observer = editor._observer;
502 let mutatedNodes = null;
503 editor._pendingEditorState = null;
504 editor._editorState = pendingEditorState;
506 if (!shouldSkipDOM && needsUpdate && observer !== null) {
507 activeEditor = editor;
508 activeEditorState = pendingEditorState;
509 isReadOnlyMode = false;
510 // We don't want updates to sync block the reconciliation.
511 editor._updating = true;
513 const dirtyType = editor._dirtyType;
514 const dirtyElements = editor._dirtyElements;
515 const dirtyLeaves = editor._dirtyLeaves;
516 observer.disconnect();
518 mutatedNodes = $reconcileRoot(
528 if (error instanceof Error) {
529 editor._onError(error);
532 // Reset editor and restore incoming editor state to the DOM
533 if (!isAttemptingToRecoverFromReconcilerError) {
534 resetEditor(editor, null, rootElement, pendingEditorState);
535 initMutationObserver(editor);
536 editor._dirtyType = FULL_RECONCILE;
537 isAttemptingToRecoverFromReconcilerError = true;
538 $commitPendingUpdates(editor, currentEditorState);
539 isAttemptingToRecoverFromReconcilerError = false;
541 // To avoid a possible situation of infinite loops, lets throw
547 observer.observe(rootElement as Node, observerOptions);
548 editor._updating = previouslyUpdating;
549 activeEditorState = previousActiveEditorState;
550 isReadOnlyMode = previousReadOnlyMode;
551 activeEditor = previousActiveEditor;
555 if (!pendingEditorState._readOnly) {
556 pendingEditorState._readOnly = true;
558 handleDEVOnlyPendingUpdateGuarantees(pendingEditorState);
559 if ($isRangeSelection(pendingSelection)) {
560 Object.freeze(pendingSelection.anchor);
561 Object.freeze(pendingSelection.focus);
563 Object.freeze(pendingSelection);
567 const dirtyLeaves = editor._dirtyLeaves;
568 const dirtyElements = editor._dirtyElements;
569 const normalizedNodes = editor._normalizedNodes;
570 const tags = editor._updateTags;
571 const deferred = editor._deferred;
572 const nodeCount = pendingEditorState._nodeMap.size;
575 editor._dirtyType = NO_DIRTY_NODES;
576 editor._cloneNotNeeded.clear();
577 editor._dirtyLeaves = new Set();
578 editor._dirtyElements = new Map();
579 editor._normalizedNodes = new Set();
580 editor._updateTags = new Set();
582 $garbageCollectDetachedDecorators(editor, pendingEditorState);
585 // Reconciliation has finished. Now update selection and trigger listeners.
588 const domSelection = shouldSkipDOM ? null : getDOMSelection(editor._window);
590 // Attempt to update the DOM selection, including focusing of the root element,
591 // and scroll into view if needed.
594 // domSelection will be null in headless
595 domSelection !== null &&
596 (needsUpdate || pendingSelection === null || pendingSelection.dirty)
598 activeEditor = editor;
599 activeEditorState = pendingEditorState;
601 if (observer !== null) {
602 observer.disconnect();
604 if (needsUpdate || pendingSelection === null || pendingSelection.dirty) {
605 const blockCursorElement = editor._blockCursorElement;
606 if (blockCursorElement !== null) {
607 removeDOMBlockCursorElement(
610 rootElement as HTMLElement,
619 rootElement as HTMLElement,
623 updateDOMBlockCursorElement(
625 rootElement as HTMLElement,
628 if (observer !== null) {
629 observer.observe(rootElement as Node, observerOptions);
632 activeEditor = previousActiveEditor;
633 activeEditorState = previousActiveEditorState;
637 if (mutatedNodes !== null) {
638 triggerMutationListeners(
647 !$isRangeSelection(pendingSelection) &&
648 pendingSelection !== null &&
649 (currentSelection === null || !currentSelection.is(pendingSelection))
651 editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
654 * Capture pendingDecorators after garbage collecting detached decorators
656 const pendingDecorators = editor._pendingDecorators;
657 if (pendingDecorators !== null) {
658 editor._decorators = pendingDecorators;
659 editor._pendingDecorators = null;
660 triggerListeners('decorator', editor, true, pendingDecorators);
663 // If reconciler fails, we reset whole editor (so current editor state becomes empty)
664 // and attempt to re-render pendingEditorState. If that goes through we trigger
665 // listeners, but instead use recoverEditorState which is current editor state before reset
666 // This specifically important for collab that relies on prevEditorState from update
667 // listener to calculate delta of changed nodes/properties
668 triggerTextContentListeners(
670 recoveryEditorState || currentEditorState,
673 triggerListeners('update', editor, true, {
676 editorState: pendingEditorState,
678 prevEditorState: recoveryEditorState || currentEditorState,
681 triggerDeferredUpdateCallbacks(editor, deferred);
682 $triggerEnqueuedUpdates(editor);
685 function triggerTextContentListeners(
686 editor: LexicalEditor,
687 currentEditorState: EditorState,
688 pendingEditorState: EditorState,
690 const currentTextContent = getEditorStateTextContent(currentEditorState);
691 const latestTextContent = getEditorStateTextContent(pendingEditorState);
693 if (currentTextContent !== latestTextContent) {
694 triggerListeners('textcontent', editor, true, latestTextContent);
698 function triggerMutationListeners(
699 editor: LexicalEditor,
700 mutatedNodes: MutatedNodes,
701 updateTags: Set<string>,
702 dirtyLeaves: Set<string>,
703 prevEditorState: EditorState,
705 const listeners = Array.from(editor._listeners.mutation);
706 const listenersLength = listeners.length;
708 for (let i = 0; i < listenersLength; i++) {
709 const [listener, klass] = listeners[i];
710 const mutatedNodesByType = mutatedNodes.get(klass);
711 if (mutatedNodesByType !== undefined) {
712 listener(mutatedNodesByType, {
721 export function triggerListeners(
722 type: 'update' | 'root' | 'decorator' | 'textcontent' | 'editable',
723 editor: LexicalEditor,
724 isCurrentlyEnqueuingUpdates: boolean,
725 ...payload: unknown[]
727 const previouslyUpdating = editor._updating;
728 editor._updating = isCurrentlyEnqueuingUpdates;
731 const listeners = Array.from<Listener>(editor._listeners[type]);
732 for (let i = 0; i < listeners.length; i++) {
734 listeners[i].apply(null, payload);
737 editor._updating = previouslyUpdating;
741 export function triggerCommandListeners<
742 TCommand extends LexicalCommand<unknown>,
744 editor: LexicalEditor,
746 payload: CommandPayloadType<TCommand>,
748 if (editor._updating === false || activeEditor !== editor) {
749 let returnVal = false;
750 editor.update(() => {
751 returnVal = triggerCommandListeners(editor, type, payload);
756 const editors = getEditorsToPropagate(editor);
758 for (let i = 4; i >= 0; i--) {
759 for (let e = 0; e < editors.length; e++) {
760 const currentEditor = editors[e];
761 const commandListeners = currentEditor._commands;
762 const listenerInPriorityOrder = commandListeners.get(type);
764 if (listenerInPriorityOrder !== undefined) {
765 const listenersSet = listenerInPriorityOrder[i];
767 if (listenersSet !== undefined) {
768 const listeners = Array.from(listenersSet);
769 const listenersLength = listeners.length;
771 for (let j = 0; j < listenersLength; j++) {
772 if (listeners[j](payload, editor) === true) {
784 function $triggerEnqueuedUpdates(editor: LexicalEditor): void {
785 const queuedUpdates = editor._updates;
787 if (queuedUpdates.length !== 0) {
788 const queuedUpdate = queuedUpdates.shift();
790 const [updateFn, options] = queuedUpdate;
791 $beginUpdate(editor, updateFn, options);
796 function triggerDeferredUpdateCallbacks(
797 editor: LexicalEditor,
798 deferred: Array<() => void>,
800 editor._deferred = [];
802 if (deferred.length !== 0) {
803 const previouslyUpdating = editor._updating;
804 editor._updating = true;
807 for (let i = 0; i < deferred.length; i++) {
811 editor._updating = previouslyUpdating;
816 function processNestedUpdates(
817 editor: LexicalEditor,
818 initialSkipTransforms?: boolean,
820 const queuedUpdates = editor._updates;
821 let skipTransforms = initialSkipTransforms || false;
823 // Updates might grow as we process them, we so we'll need
824 // to handle each update as we go until the updates array is
826 while (queuedUpdates.length !== 0) {
827 const queuedUpdate = queuedUpdates.shift();
829 const [nextUpdateFn, options] = queuedUpdate;
834 if (options !== undefined) {
835 onUpdate = options.onUpdate;
838 if (options.skipTransforms) {
839 skipTransforms = true;
841 if (options.discrete) {
842 const pendingEditorState = editor._pendingEditorState;
844 pendingEditorState !== null,
845 'Unexpected empty pending editor state on discrete nested update',
847 pendingEditorState._flushSync = true;
851 editor._deferred.push(onUpdate);
855 editor._updateTags.add(tag);
863 return skipTransforms;
866 function $beginUpdate(
867 editor: LexicalEditor,
868 updateFn: () => void,
869 options?: EditorUpdateOptions,
871 const updateTags = editor._updateTags;
874 let skipTransforms = false;
875 let discrete = false;
877 if (options !== undefined) {
878 onUpdate = options.onUpdate;
885 skipTransforms = options.skipTransforms || false;
886 discrete = options.discrete || false;
890 editor._deferred.push(onUpdate);
893 const currentEditorState = editor._editorState;
894 let pendingEditorState = editor._pendingEditorState;
895 let editorStateWasCloned = false;
897 if (pendingEditorState === null || pendingEditorState._readOnly) {
898 pendingEditorState = editor._pendingEditorState = cloneEditorState(
899 pendingEditorState || currentEditorState,
901 editorStateWasCloned = true;
903 pendingEditorState._flushSync = discrete;
905 const previousActiveEditorState = activeEditorState;
906 const previousReadOnlyMode = isReadOnlyMode;
907 const previousActiveEditor = activeEditor;
908 const previouslyUpdating = editor._updating;
909 activeEditorState = pendingEditorState;
910 isReadOnlyMode = false;
911 editor._updating = true;
912 activeEditor = editor;
915 if (editorStateWasCloned) {
916 if (editor._headless) {
917 if (currentEditorState._selection !== null) {
918 pendingEditorState._selection = currentEditorState._selection.clone();
921 pendingEditorState._selection = $internalCreateSelection(editor);
925 const startingCompositionKey = editor._compositionKey;
927 skipTransforms = processNestedUpdates(editor, skipTransforms);
928 applySelectionTransforms(pendingEditorState, editor);
930 if (editor._dirtyType !== NO_DIRTY_NODES) {
931 if (skipTransforms) {
932 $normalizeAllDirtyTextNodes(pendingEditorState, editor);
934 $applyAllTransforms(pendingEditorState, editor);
937 processNestedUpdates(editor);
938 $garbageCollectDetachedNodes(
942 editor._dirtyElements,
946 const endingCompositionKey = editor._compositionKey;
948 if (startingCompositionKey !== endingCompositionKey) {
949 pendingEditorState._flushSync = true;
952 const pendingSelection = pendingEditorState._selection;
954 if ($isRangeSelection(pendingSelection)) {
955 const pendingNodeMap = pendingEditorState._nodeMap;
956 const anchorKey = pendingSelection.anchor.key;
957 const focusKey = pendingSelection.focus.key;
960 pendingNodeMap.get(anchorKey) === undefined ||
961 pendingNodeMap.get(focusKey) === undefined
965 'updateEditor: selection has been lost because the previously selected nodes have been removed and ' +
966 "selection wasn't moved to another node. Ensure selection changes after removing/replacing a selected node.",
969 } else if ($isNodeSelection(pendingSelection)) {
970 // TODO: we should also validate node selection?
971 if (pendingSelection._nodes.size === 0) {
972 pendingEditorState._selection = null;
977 if (error instanceof Error) {
978 editor._onError(error);
981 // Restore existing editor state to the DOM
982 editor._pendingEditorState = currentEditorState;
983 editor._dirtyType = FULL_RECONCILE;
985 editor._cloneNotNeeded.clear();
987 editor._dirtyLeaves = new Set();
989 editor._dirtyElements.clear();
991 $commitPendingUpdates(editor);
994 activeEditorState = previousActiveEditorState;
995 isReadOnlyMode = previousReadOnlyMode;
996 activeEditor = previousActiveEditor;
997 editor._updating = previouslyUpdating;
998 infiniteTransformCount = 0;
1001 const shouldUpdate =
1002 editor._dirtyType !== NO_DIRTY_NODES ||
1003 editorStateHasDirtySelection(pendingEditorState, editor);
1006 if (pendingEditorState._flushSync) {
1007 pendingEditorState._flushSync = false;
1008 $commitPendingUpdates(editor);
1009 } else if (editorStateWasCloned) {
1010 scheduleMicroTask(() => {
1011 $commitPendingUpdates(editor);
1015 pendingEditorState._flushSync = false;
1017 if (editorStateWasCloned) {
1019 editor._deferred = [];
1020 editor._pendingEditorState = null;
1025 export function updateEditor(
1026 editor: LexicalEditor,
1027 updateFn: () => void,
1028 options?: EditorUpdateOptions,
1030 if (editor._updating) {
1031 editor._updates.push([updateFn, options]);
1033 $beginUpdate(editor, updateFn, options);