]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalUpdates.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalUpdates.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {SerializedEditorState} from './LexicalEditorState';
10 import type {LexicalNode, SerializedLexicalNode} from './LexicalNode';
11
12 import invariant from 'lexical/shared/invariant';
13
14 import {$isElementNode, $isTextNode, SELECTION_CHANGE_COMMAND} from '.';
15 import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
16 import {
17   CommandPayloadType,
18   EditorUpdateOptions,
19   LexicalCommand,
20   LexicalEditor,
21   Listener,
22   MutatedNodes,
23   RegisteredNodes,
24   resetEditor,
25   Transform,
26 } from './LexicalEditor';
27 import {
28   cloneEditorState,
29   createEmptyEditorState,
30   EditorState,
31   editorStateHasDirtySelection,
32 } from './LexicalEditorState';
33 import {
34   $garbageCollectDetachedDecorators,
35   $garbageCollectDetachedNodes,
36 } from './LexicalGC';
37 import {initMutationObserver} from './LexicalMutations';
38 import {$normalizeTextNode} from './LexicalNormalization';
39 import {$reconcileRoot} from './LexicalReconciler';
40 import {
41   $internalCreateSelection,
42   $isNodeSelection,
43   $isRangeSelection,
44   applySelectionTransforms,
45   updateDOMSelection,
46 } from './LexicalSelection';
47 import {
48   $getCompositionKey,
49   getDOMSelection,
50   getEditorPropertyFromDOMNode,
51   getEditorStateTextContent,
52   getEditorsToPropagate,
53   getRegisteredNodeOrThrow,
54   isLexicalEditor,
55   removeDOMBlockCursorElement,
56   scheduleMicroTask,
57   updateDOMBlockCursorElement,
58 } from './LexicalUtils';
59
60 let activeEditorState: null | EditorState = null;
61 let activeEditor: null | LexicalEditor = null;
62 let isReadOnlyMode = false;
63 let isAttemptingToRecoverFromReconcilerError = false;
64 let infiniteTransformCount = 0;
65
66 const observerOptions = {
67   characterData: true,
68   childList: true,
69   subtree: true,
70 };
71
72 export function isCurrentlyReadOnlyMode(): boolean {
73   return (
74     isReadOnlyMode ||
75     (activeEditorState !== null && activeEditorState._readOnly)
76   );
77 }
78
79 export function errorOnReadOnly(): void {
80   if (isReadOnlyMode) {
81     invariant(false, 'Cannot use method in read-only mode.');
82   }
83 }
84
85 export function errorOnInfiniteTransforms(): void {
86   if (infiniteTransformCount > 99) {
87     invariant(
88       false,
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.',
90     );
91   }
92 }
93
94 export function getActiveEditorState(): EditorState {
95   if (activeEditorState === null) {
96     invariant(
97       false,
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(),
103     );
104   }
105
106   return activeEditorState;
107 }
108
109 export function getActiveEditor(): LexicalEditor {
110   if (activeEditor === null) {
111     invariant(
112       false,
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(),
118     );
119   }
120   return activeEditor;
121 }
122
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)) {
131         compatibleEditors++;
132       } else if (editor) {
133         let version = String(
134           (
135             editor.constructor as typeof editor['constructor'] &
136               Record<string, unknown>
137           ).version || '<0.17.1',
138         );
139         if (version === thisVersion) {
140           version +=
141             ' (separately built, likely a bundler configuration issue)';
142         }
143         incompatibleEditors.add(version);
144       }
145     }
146   }
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(
150       incompatibleEditors,
151     ).join(', ')}`;
152   }
153   return output;
154 }
155
156 export function internalGetActiveEditor(): LexicalEditor | null {
157   return activeEditor;
158 }
159
160 export function internalGetActiveEditorState(): EditorState | null {
161   return activeEditorState;
162 }
163
164 export function $applyTransforms(
165   editor: LexicalEditor,
166   node: LexicalNode,
167   transformsCache: Map<string, Array<Transform<LexicalNode>>>,
168 ) {
169   const type = node.__type;
170   const registeredNode = getRegisteredNodeOrThrow(editor, type);
171   let transformsArr = transformsCache.get(type);
172
173   if (transformsArr === undefined) {
174     transformsArr = Array.from(registeredNode.transforms);
175     transformsCache.set(type, transformsArr);
176   }
177
178   const transformsArrLength = transformsArr.length;
179
180   for (let i = 0; i < transformsArrLength; i++) {
181     transformsArr[i](node);
182
183     if (!node.isAttached()) {
184       break;
185     }
186   }
187 }
188
189 function $isNodeValidForTransform(
190   node: LexicalNode,
191   compositionKey: null | string,
192 ): boolean {
193   return (
194     node !== undefined &&
195     // We don't want to transform nodes being composed
196     node.__key !== compositionKey &&
197     node.isAttached()
198   );
199 }
200
201 function $normalizeAllDirtyTextNodes(
202   editorState: EditorState,
203   editor: LexicalEditor,
204 ): void {
205   const dirtyLeaves = editor._dirtyLeaves;
206   const nodeMap = editorState._nodeMap;
207
208   for (const nodeKey of dirtyLeaves) {
209     const node = nodeMap.get(nodeKey);
210
211     if (
212       $isTextNode(node) &&
213       node.isAttached() &&
214       node.isSimpleText() &&
215       !node.isUnmergeable()
216     ) {
217       $normalizeTextNode(node);
218     }
219   }
220 }
221
222 /**
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.
228  *
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.
231  */
232 function $applyAllTransforms(
233   editorState: EditorState,
234   editor: LexicalEditor,
235 ): void {
236   const dirtyLeaves = editor._dirtyLeaves;
237   const dirtyElements = editor._dirtyElements;
238   const nodeMap = editorState._nodeMap;
239   const compositionKey = $getCompositionKey();
240   const transformsCache = new Map();
241
242   let untransformedDirtyLeaves = dirtyLeaves;
243   let untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
244   let untransformedDirtyElements = dirtyElements;
245   let untransformedDirtyElementsLength = untransformedDirtyElements.size;
246
247   while (
248     untransformedDirtyLeavesLength > 0 ||
249     untransformedDirtyElementsLength > 0
250   ) {
251     if (untransformedDirtyLeavesLength > 0) {
252       // We leverage editor._dirtyLeaves to track the new dirty leaves after the transforms
253       editor._dirtyLeaves = new Set();
254
255       for (const nodeKey of untransformedDirtyLeaves) {
256         const node = nodeMap.get(nodeKey);
257
258         if (
259           $isTextNode(node) &&
260           node.isAttached() &&
261           node.isSimpleText() &&
262           !node.isUnmergeable()
263         ) {
264           $normalizeTextNode(node);
265         }
266
267         if (
268           node !== undefined &&
269           $isNodeValidForTransform(node, compositionKey)
270         ) {
271           $applyTransforms(editor, node, transformsCache);
272         }
273
274         dirtyLeaves.add(nodeKey);
275       }
276
277       untransformedDirtyLeaves = editor._dirtyLeaves;
278       untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
279
280       // We want to prioritize node transforms over element transforms
281       if (untransformedDirtyLeavesLength > 0) {
282         infiniteTransformCount++;
283         continue;
284       }
285     }
286
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();
292
293     for (const currentUntransformedDirtyElement of untransformedDirtyElements) {
294       const nodeKey = currentUntransformedDirtyElement[0];
295       const intentionallyMarkedAsDirty = currentUntransformedDirtyElement[1];
296       if (nodeKey !== 'root' && !intentionallyMarkedAsDirty) {
297         continue;
298       }
299
300       const node = nodeMap.get(nodeKey);
301
302       if (
303         node !== undefined &&
304         $isNodeValidForTransform(node, compositionKey)
305       ) {
306         $applyTransforms(editor, node, transformsCache);
307       }
308
309       dirtyElements.set(nodeKey, intentionallyMarkedAsDirty);
310     }
311
312     untransformedDirtyLeaves = editor._dirtyLeaves;
313     untransformedDirtyLeavesLength = untransformedDirtyLeaves.size;
314     untransformedDirtyElements = editor._dirtyElements;
315     untransformedDirtyElementsLength = untransformedDirtyElements.size;
316     infiniteTransformCount++;
317   }
318
319   editor._dirtyLeaves = dirtyLeaves;
320   editor._dirtyElements = dirtyElements;
321 }
322
323 type InternalSerializedNode = {
324   children?: Array<InternalSerializedNode>;
325   type: string;
326   version: number;
327 };
328
329 export function $parseSerializedNode(
330   serializedNode: SerializedLexicalNode,
331 ): LexicalNode {
332   const internalSerializedNode: InternalSerializedNode = serializedNode;
333   return $parseSerializedNodeImpl(
334     internalSerializedNode,
335     getActiveEditor()._nodes,
336   );
337 }
338
339 function $parseSerializedNodeImpl<
340   SerializedNode extends InternalSerializedNode,
341 >(
342   serializedNode: SerializedNode,
343   registeredNodes: RegisteredNodes,
344 ): LexicalNode {
345   const type = serializedNode.type;
346   const registeredNode = registeredNodes.get(type);
347
348   if (registeredNode === undefined) {
349     invariant(false, 'parseEditorState: type "%s" + not found', type);
350   }
351
352   const nodeClass = registeredNode.klass;
353
354   if (serializedNode.type !== nodeClass.getType()) {
355     invariant(
356       false,
357       'LexicalNode: Node %s does not implement .importJSON().',
358       nodeClass.name,
359     );
360   }
361
362   const node = nodeClass.importJSON(serializedNode);
363   const children = serializedNode.children;
364
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,
370         registeredNodes,
371       );
372       node.append(childNode);
373     }
374   }
375
376   return node;
377 }
378
379 export function parseEditorState(
380   serializedEditorState: SerializedEditorState,
381   editor: LexicalEditor,
382   updateFn: void | (() => void),
383 ): EditorState {
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;
399
400   try {
401     const registeredNodes = editor._nodes;
402     const serializedNode = serializedEditorState.root;
403     $parseSerializedNodeImpl(serializedNode, registeredNodes);
404     if (updateFn) {
405       updateFn();
406     }
407
408     // Make the editorState immutable
409     editorState._readOnly = true;
410
411     if (__DEV__) {
412       handleDEVOnlyPendingUpdateGuarantees(editorState);
413     }
414   } catch (error) {
415     if (error instanceof Error) {
416       editor._onError(error);
417     }
418   } finally {
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;
426   }
427
428   return editorState;
429 }
430
431 // This technically isn't an update but given we need
432 // exposure to the module's active bindings, we have this
433 // function here
434
435 export function readEditorState<V>(
436   editor: LexicalEditor | null,
437   editorState: EditorState,
438   callbackFn: () => V,
439 ): V {
440   const previousActiveEditorState = activeEditorState;
441   const previousReadOnlyMode = isReadOnlyMode;
442   const previousActiveEditor = activeEditor;
443
444   activeEditorState = editorState;
445   isReadOnlyMode = true;
446   activeEditor = editor;
447
448   try {
449     return callbackFn();
450   } finally {
451     activeEditorState = previousActiveEditorState;
452     isReadOnlyMode = previousReadOnlyMode;
453     activeEditor = previousActiveEditor;
454   }
455 }
456
457 function handleDEVOnlyPendingUpdateGuarantees(
458   pendingEditorState: EditorState,
459 ): void {
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;
463
464   nodeMap.set = () => {
465     throw new Error('Cannot call set() on a frozen Lexical node map');
466   };
467
468   nodeMap.clear = () => {
469     throw new Error('Cannot call clear() on a frozen Lexical node map');
470   };
471
472   nodeMap.delete = () => {
473     throw new Error('Cannot call delete() on a frozen Lexical node map');
474   };
475 }
476
477 export function $commitPendingUpdates(
478   editor: LexicalEditor,
479   recoveryEditorState?: EditorState,
480 ): void {
481   const pendingEditorState = editor._pendingEditorState;
482   const rootElement = editor._rootElement;
483   const shouldSkipDOM = editor._headless || rootElement === null;
484
485   if (pendingEditorState === null) {
486     return;
487   }
488
489   // ======
490   // Reconciliation has started.
491   // ======
492
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;
505
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;
512     try {
513       const dirtyType = editor._dirtyType;
514       const dirtyElements = editor._dirtyElements;
515       const dirtyLeaves = editor._dirtyLeaves;
516       observer.disconnect();
517
518       mutatedNodes = $reconcileRoot(
519         currentEditorState,
520         pendingEditorState,
521         editor,
522         dirtyType,
523         dirtyElements,
524         dirtyLeaves,
525       );
526     } catch (error) {
527       // Report errors
528       if (error instanceof Error) {
529         editor._onError(error);
530       }
531
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;
540       } else {
541         // To avoid a possible situation of infinite loops, lets throw
542         throw error;
543       }
544
545       return;
546     } finally {
547       observer.observe(rootElement as Node, observerOptions);
548       editor._updating = previouslyUpdating;
549       activeEditorState = previousActiveEditorState;
550       isReadOnlyMode = previousReadOnlyMode;
551       activeEditor = previousActiveEditor;
552     }
553   }
554
555   if (!pendingEditorState._readOnly) {
556     pendingEditorState._readOnly = true;
557     if (__DEV__) {
558       handleDEVOnlyPendingUpdateGuarantees(pendingEditorState);
559       if ($isRangeSelection(pendingSelection)) {
560         Object.freeze(pendingSelection.anchor);
561         Object.freeze(pendingSelection.focus);
562       }
563       Object.freeze(pendingSelection);
564     }
565   }
566
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;
573
574   if (needsUpdate) {
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();
581   }
582   $garbageCollectDetachedDecorators(editor, pendingEditorState);
583
584   // ======
585   // Reconciliation has finished. Now update selection and trigger listeners.
586   // ======
587
588   const domSelection = shouldSkipDOM ? null : getDOMSelection(editor._window);
589
590   // Attempt to update the DOM selection, including focusing of the root element,
591   // and scroll into view if needed.
592   if (
593     editor._editable &&
594     // domSelection will be null in headless
595     domSelection !== null &&
596     (needsUpdate || pendingSelection === null || pendingSelection.dirty)
597   ) {
598     activeEditor = editor;
599     activeEditorState = pendingEditorState;
600     try {
601       if (observer !== null) {
602         observer.disconnect();
603       }
604       if (needsUpdate || pendingSelection === null || pendingSelection.dirty) {
605         const blockCursorElement = editor._blockCursorElement;
606         if (blockCursorElement !== null) {
607           removeDOMBlockCursorElement(
608             blockCursorElement,
609             editor,
610             rootElement as HTMLElement,
611           );
612         }
613         updateDOMSelection(
614           currentSelection,
615           pendingSelection,
616           editor,
617           domSelection,
618           tags,
619           rootElement as HTMLElement,
620           nodeCount,
621         );
622       }
623       updateDOMBlockCursorElement(
624         editor,
625         rootElement as HTMLElement,
626         pendingSelection,
627       );
628       if (observer !== null) {
629         observer.observe(rootElement as Node, observerOptions);
630       }
631     } finally {
632       activeEditor = previousActiveEditor;
633       activeEditorState = previousActiveEditorState;
634     }
635   }
636
637   if (mutatedNodes !== null) {
638     triggerMutationListeners(
639       editor,
640       mutatedNodes,
641       tags,
642       dirtyLeaves,
643       currentEditorState,
644     );
645   }
646   if (
647     !$isRangeSelection(pendingSelection) &&
648     pendingSelection !== null &&
649     (currentSelection === null || !currentSelection.is(pendingSelection))
650   ) {
651     editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
652   }
653   /**
654    * Capture pendingDecorators after garbage collecting detached decorators
655    */
656   const pendingDecorators = editor._pendingDecorators;
657   if (pendingDecorators !== null) {
658     editor._decorators = pendingDecorators;
659     editor._pendingDecorators = null;
660     triggerListeners('decorator', editor, true, pendingDecorators);
661   }
662
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(
669     editor,
670     recoveryEditorState || currentEditorState,
671     pendingEditorState,
672   );
673   triggerListeners('update', editor, true, {
674     dirtyElements,
675     dirtyLeaves,
676     editorState: pendingEditorState,
677     normalizedNodes,
678     prevEditorState: recoveryEditorState || currentEditorState,
679     tags,
680   });
681   triggerDeferredUpdateCallbacks(editor, deferred);
682   $triggerEnqueuedUpdates(editor);
683 }
684
685 function triggerTextContentListeners(
686   editor: LexicalEditor,
687   currentEditorState: EditorState,
688   pendingEditorState: EditorState,
689 ): void {
690   const currentTextContent = getEditorStateTextContent(currentEditorState);
691   const latestTextContent = getEditorStateTextContent(pendingEditorState);
692
693   if (currentTextContent !== latestTextContent) {
694     triggerListeners('textcontent', editor, true, latestTextContent);
695   }
696 }
697
698 function triggerMutationListeners(
699   editor: LexicalEditor,
700   mutatedNodes: MutatedNodes,
701   updateTags: Set<string>,
702   dirtyLeaves: Set<string>,
703   prevEditorState: EditorState,
704 ): void {
705   const listeners = Array.from(editor._listeners.mutation);
706   const listenersLength = listeners.length;
707
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, {
713         dirtyLeaves,
714         prevEditorState,
715         updateTags,
716       });
717     }
718   }
719 }
720
721 export function triggerListeners(
722   type: 'update' | 'root' | 'decorator' | 'textcontent' | 'editable',
723   editor: LexicalEditor,
724   isCurrentlyEnqueuingUpdates: boolean,
725   ...payload: unknown[]
726 ): void {
727   const previouslyUpdating = editor._updating;
728   editor._updating = isCurrentlyEnqueuingUpdates;
729
730   try {
731     const listeners = Array.from<Listener>(editor._listeners[type]);
732     for (let i = 0; i < listeners.length; i++) {
733       // @ts-ignore
734       listeners[i].apply(null, payload);
735     }
736   } finally {
737     editor._updating = previouslyUpdating;
738   }
739 }
740
741 export function triggerCommandListeners<
742   TCommand extends LexicalCommand<unknown>,
743 >(
744   editor: LexicalEditor,
745   type: TCommand,
746   payload: CommandPayloadType<TCommand>,
747 ): boolean {
748   if (editor._updating === false || activeEditor !== editor) {
749     let returnVal = false;
750     editor.update(() => {
751       returnVal = triggerCommandListeners(editor, type, payload);
752     });
753     return returnVal;
754   }
755
756   const editors = getEditorsToPropagate(editor);
757
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);
763
764       if (listenerInPriorityOrder !== undefined) {
765         const listenersSet = listenerInPriorityOrder[i];
766
767         if (listenersSet !== undefined) {
768           const listeners = Array.from(listenersSet);
769           const listenersLength = listeners.length;
770
771           for (let j = 0; j < listenersLength; j++) {
772             if (listeners[j](payload, editor) === true) {
773               return true;
774             }
775           }
776         }
777       }
778     }
779   }
780
781   return false;
782 }
783
784 function $triggerEnqueuedUpdates(editor: LexicalEditor): void {
785   const queuedUpdates = editor._updates;
786
787   if (queuedUpdates.length !== 0) {
788     const queuedUpdate = queuedUpdates.shift();
789     if (queuedUpdate) {
790       const [updateFn, options] = queuedUpdate;
791       $beginUpdate(editor, updateFn, options);
792     }
793   }
794 }
795
796 function triggerDeferredUpdateCallbacks(
797   editor: LexicalEditor,
798   deferred: Array<() => void>,
799 ): void {
800   editor._deferred = [];
801
802   if (deferred.length !== 0) {
803     const previouslyUpdating = editor._updating;
804     editor._updating = true;
805
806     try {
807       for (let i = 0; i < deferred.length; i++) {
808         deferred[i]();
809       }
810     } finally {
811       editor._updating = previouslyUpdating;
812     }
813   }
814 }
815
816 function processNestedUpdates(
817   editor: LexicalEditor,
818   initialSkipTransforms?: boolean,
819 ): boolean {
820   const queuedUpdates = editor._updates;
821   let skipTransforms = initialSkipTransforms || false;
822
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
825   // empty.
826   while (queuedUpdates.length !== 0) {
827     const queuedUpdate = queuedUpdates.shift();
828     if (queuedUpdate) {
829       const [nextUpdateFn, options] = queuedUpdate;
830
831       let onUpdate;
832       let tag;
833
834       if (options !== undefined) {
835         onUpdate = options.onUpdate;
836         tag = options.tag;
837
838         if (options.skipTransforms) {
839           skipTransforms = true;
840         }
841         if (options.discrete) {
842           const pendingEditorState = editor._pendingEditorState;
843           invariant(
844             pendingEditorState !== null,
845             'Unexpected empty pending editor state on discrete nested update',
846           );
847           pendingEditorState._flushSync = true;
848         }
849
850         if (onUpdate) {
851           editor._deferred.push(onUpdate);
852         }
853
854         if (tag) {
855           editor._updateTags.add(tag);
856         }
857       }
858
859       nextUpdateFn();
860     }
861   }
862
863   return skipTransforms;
864 }
865
866 function $beginUpdate(
867   editor: LexicalEditor,
868   updateFn: () => void,
869   options?: EditorUpdateOptions,
870 ): void {
871   const updateTags = editor._updateTags;
872   let onUpdate;
873   let tag;
874   let skipTransforms = false;
875   let discrete = false;
876
877   if (options !== undefined) {
878     onUpdate = options.onUpdate;
879     tag = options.tag;
880
881     if (tag != null) {
882       updateTags.add(tag);
883     }
884
885     skipTransforms = options.skipTransforms || false;
886     discrete = options.discrete || false;
887   }
888
889   if (onUpdate) {
890     editor._deferred.push(onUpdate);
891   }
892
893   const currentEditorState = editor._editorState;
894   let pendingEditorState = editor._pendingEditorState;
895   let editorStateWasCloned = false;
896
897   if (pendingEditorState === null || pendingEditorState._readOnly) {
898     pendingEditorState = editor._pendingEditorState = cloneEditorState(
899       pendingEditorState || currentEditorState,
900     );
901     editorStateWasCloned = true;
902   }
903   pendingEditorState._flushSync = discrete;
904
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;
913
914   try {
915     if (editorStateWasCloned) {
916       if (editor._headless) {
917         if (currentEditorState._selection !== null) {
918           pendingEditorState._selection = currentEditorState._selection.clone();
919         }
920       } else {
921         pendingEditorState._selection = $internalCreateSelection(editor);
922       }
923     }
924
925     const startingCompositionKey = editor._compositionKey;
926     updateFn();
927     skipTransforms = processNestedUpdates(editor, skipTransforms);
928     applySelectionTransforms(pendingEditorState, editor);
929
930     if (editor._dirtyType !== NO_DIRTY_NODES) {
931       if (skipTransforms) {
932         $normalizeAllDirtyTextNodes(pendingEditorState, editor);
933       } else {
934         $applyAllTransforms(pendingEditorState, editor);
935       }
936
937       processNestedUpdates(editor);
938       $garbageCollectDetachedNodes(
939         currentEditorState,
940         pendingEditorState,
941         editor._dirtyLeaves,
942         editor._dirtyElements,
943       );
944     }
945
946     const endingCompositionKey = editor._compositionKey;
947
948     if (startingCompositionKey !== endingCompositionKey) {
949       pendingEditorState._flushSync = true;
950     }
951
952     const pendingSelection = pendingEditorState._selection;
953
954     if ($isRangeSelection(pendingSelection)) {
955       const pendingNodeMap = pendingEditorState._nodeMap;
956       const anchorKey = pendingSelection.anchor.key;
957       const focusKey = pendingSelection.focus.key;
958
959       if (
960         pendingNodeMap.get(anchorKey) === undefined ||
961         pendingNodeMap.get(focusKey) === undefined
962       ) {
963         invariant(
964           false,
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.",
967         );
968       }
969     } else if ($isNodeSelection(pendingSelection)) {
970       // TODO: we should also validate node selection?
971       if (pendingSelection._nodes.size === 0) {
972         pendingEditorState._selection = null;
973       }
974     }
975   } catch (error) {
976     // Report errors
977     if (error instanceof Error) {
978       editor._onError(error);
979     }
980
981     // Restore existing editor state to the DOM
982     editor._pendingEditorState = currentEditorState;
983     editor._dirtyType = FULL_RECONCILE;
984
985     editor._cloneNotNeeded.clear();
986
987     editor._dirtyLeaves = new Set();
988
989     editor._dirtyElements.clear();
990
991     $commitPendingUpdates(editor);
992     return;
993   } finally {
994     activeEditorState = previousActiveEditorState;
995     isReadOnlyMode = previousReadOnlyMode;
996     activeEditor = previousActiveEditor;
997     editor._updating = previouslyUpdating;
998     infiniteTransformCount = 0;
999   }
1000
1001   const shouldUpdate =
1002     editor._dirtyType !== NO_DIRTY_NODES ||
1003     editorStateHasDirtySelection(pendingEditorState, editor);
1004
1005   if (shouldUpdate) {
1006     if (pendingEditorState._flushSync) {
1007       pendingEditorState._flushSync = false;
1008       $commitPendingUpdates(editor);
1009     } else if (editorStateWasCloned) {
1010       scheduleMicroTask(() => {
1011         $commitPendingUpdates(editor);
1012       });
1013     }
1014   } else {
1015     pendingEditorState._flushSync = false;
1016
1017     if (editorStateWasCloned) {
1018       updateTags.clear();
1019       editor._deferred = [];
1020       editor._pendingEditorState = null;
1021     }
1022   }
1023 }
1024
1025 export function updateEditor(
1026   editor: LexicalEditor,
1027   updateFn: () => void,
1028   options?: EditorUpdateOptions,
1029 ): void {
1030   if (editor._updating) {
1031     editor._updates.push([updateFn, options]);
1032   } else {
1033     $beginUpdate(editor, updateFn, options);
1034   }
1035 }