]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/core/LexicalEditor.ts
b0b90002eaadfe9d9761beaf45bd96dc2cb5ec28
[bookstack] / resources / js / wysiwyg / lexical / core / LexicalEditor.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 {EditorState, SerializedEditorState} from './LexicalEditorState';
10 import type {
11   DOMConversion,
12   DOMConversionMap,
13   DOMExportOutput,
14   DOMExportOutputMap,
15   NodeKey,
16 } from './LexicalNode';
17
18 import invariant from 'lexical/shared/invariant';
19
20 import {$getRoot, $getSelection, TextNode} from '.';
21 import {FULL_RECONCILE, NO_DIRTY_NODES} from './LexicalConstants';
22 import {createEmptyEditorState} from './LexicalEditorState';
23 import {addRootElementEvents, removeRootElementEvents} from './LexicalEvents';
24 import {$flushRootMutations, initMutationObserver} from './LexicalMutations';
25 import {LexicalNode} from './LexicalNode';
26 import {
27   $commitPendingUpdates,
28   internalGetActiveEditor,
29   parseEditorState,
30   triggerListeners,
31   updateEditor,
32 } from './LexicalUpdates';
33 import {
34   createUID,
35   dispatchCommand,
36   getCachedClassNameArray,
37   getCachedTypeToNodeMap,
38   getDefaultView,
39   getDOMSelection,
40   markAllNodesAsDirty,
41 } from './LexicalUtils';
42 import {ArtificialNode__DO_NOT_USE} from './nodes/ArtificialNode';
43 import {DecoratorNode} from './nodes/LexicalDecoratorNode';
44 import {LineBreakNode} from './nodes/LexicalLineBreakNode';
45 import {ParagraphNode} from './nodes/LexicalParagraphNode';
46 import {RootNode} from './nodes/LexicalRootNode';
47 import {TabNode} from './nodes/LexicalTabNode';
48
49 export type Spread<T1, T2> = Omit<T2, keyof T1> & T1;
50
51 // https://github.com/microsoft/TypeScript/issues/3841
52 // eslint-disable-next-line @typescript-eslint/no-explicit-any
53 export type KlassConstructor<Cls extends GenericConstructor<any>> =
54   GenericConstructor<InstanceType<Cls>> & {[k in keyof Cls]: Cls[k]};
55 // eslint-disable-next-line @typescript-eslint/no-explicit-any
56 type GenericConstructor<T> = new (...args: any[]) => T;
57
58 export type Klass<T extends LexicalNode> = InstanceType<
59   T['constructor']
60 > extends T
61   ? T['constructor']
62   : GenericConstructor<T> & T['constructor'];
63
64 export type EditorThemeClassName = string;
65
66 export type TextNodeThemeClasses = {
67   base?: EditorThemeClassName;
68   bold?: EditorThemeClassName;
69   code?: EditorThemeClassName;
70   highlight?: EditorThemeClassName;
71   italic?: EditorThemeClassName;
72   strikethrough?: EditorThemeClassName;
73   subscript?: EditorThemeClassName;
74   superscript?: EditorThemeClassName;
75   underline?: EditorThemeClassName;
76   underlineStrikethrough?: EditorThemeClassName;
77   [key: string]: EditorThemeClassName | undefined;
78 };
79
80 export type EditorUpdateOptions = {
81   onUpdate?: () => void;
82   skipTransforms?: true;
83   tag?: string;
84   discrete?: true;
85 };
86
87 export type EditorSetOptions = {
88   tag?: string;
89 };
90
91 export type EditorFocusOptions = {
92   defaultSelection?: 'rootStart' | 'rootEnd';
93 };
94
95 export type EditorThemeClasses = {
96   blockCursor?: EditorThemeClassName;
97   characterLimit?: EditorThemeClassName;
98   code?: EditorThemeClassName;
99   codeHighlight?: Record<string, EditorThemeClassName>;
100   hashtag?: EditorThemeClassName;
101   heading?: {
102     h1?: EditorThemeClassName;
103     h2?: EditorThemeClassName;
104     h3?: EditorThemeClassName;
105     h4?: EditorThemeClassName;
106     h5?: EditorThemeClassName;
107     h6?: EditorThemeClassName;
108   };
109   hr?: EditorThemeClassName;
110   image?: EditorThemeClassName;
111   link?: EditorThemeClassName;
112   list?: {
113     ul?: EditorThemeClassName;
114     ulDepth?: Array<EditorThemeClassName>;
115     ol?: EditorThemeClassName;
116     olDepth?: Array<EditorThemeClassName>;
117     checklist?: EditorThemeClassName;
118     listitem?: EditorThemeClassName;
119     listitemChecked?: EditorThemeClassName;
120     listitemUnchecked?: EditorThemeClassName;
121     nested?: {
122       list?: EditorThemeClassName;
123       listitem?: EditorThemeClassName;
124     };
125   };
126   ltr?: EditorThemeClassName;
127   mark?: EditorThemeClassName;
128   markOverlap?: EditorThemeClassName;
129   paragraph?: EditorThemeClassName;
130   quote?: EditorThemeClassName;
131   root?: EditorThemeClassName;
132   rtl?: EditorThemeClassName;
133   table?: EditorThemeClassName;
134   tableAddColumns?: EditorThemeClassName;
135   tableAddRows?: EditorThemeClassName;
136   tableCellActionButton?: EditorThemeClassName;
137   tableCellActionButtonContainer?: EditorThemeClassName;
138   tableCellPrimarySelected?: EditorThemeClassName;
139   tableCellSelected?: EditorThemeClassName;
140   tableCell?: EditorThemeClassName;
141   tableCellEditing?: EditorThemeClassName;
142   tableCellHeader?: EditorThemeClassName;
143   tableCellResizer?: EditorThemeClassName;
144   tableCellSortedIndicator?: EditorThemeClassName;
145   tableResizeRuler?: EditorThemeClassName;
146   tableRow?: EditorThemeClassName;
147   tableSelected?: EditorThemeClassName;
148   text?: TextNodeThemeClasses;
149   embedBlock?: {
150     base?: EditorThemeClassName;
151     focus?: EditorThemeClassName;
152   };
153   indent?: EditorThemeClassName;
154   // eslint-disable-next-line @typescript-eslint/no-explicit-any
155   [key: string]: any;
156 };
157
158 export type EditorConfig = {
159   disableEvents?: boolean;
160   namespace: string;
161   theme: EditorThemeClasses;
162 };
163
164 export type LexicalNodeReplacement = {
165   replace: Klass<LexicalNode>;
166   // eslint-disable-next-line @typescript-eslint/no-explicit-any
167   with: <T extends {new (...args: any): any}>(
168     node: InstanceType<T>,
169   ) => LexicalNode;
170   withKlass?: Klass<LexicalNode>;
171 };
172
173 export type HTMLConfig = {
174   export?: DOMExportOutputMap;
175   import?: DOMConversionMap;
176 };
177
178 export type CreateEditorArgs = {
179   disableEvents?: boolean;
180   editorState?: EditorState;
181   namespace?: string;
182   nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
183   onError?: ErrorHandler;
184   parentEditor?: LexicalEditor;
185   editable?: boolean;
186   theme?: EditorThemeClasses;
187   html?: HTMLConfig;
188 };
189
190 export type RegisteredNodes = Map<string, RegisteredNode>;
191
192 export type RegisteredNode = {
193   klass: Klass<LexicalNode>;
194   transforms: Set<Transform<LexicalNode>>;
195   replace: null | ((node: LexicalNode) => LexicalNode);
196   replaceWithKlass: null | Klass<LexicalNode>;
197   exportDOM?: (
198     editor: LexicalEditor,
199     targetNode: LexicalNode,
200   ) => DOMExportOutput;
201 };
202
203 export type Transform<T extends LexicalNode> = (node: T) => void;
204
205 export type ErrorHandler = (error: Error) => void;
206
207 export type MutationListeners = Map<MutationListener, Klass<LexicalNode>>;
208
209 export type MutatedNodes = Map<Klass<LexicalNode>, Map<NodeKey, NodeMutation>>;
210
211 export type NodeMutation = 'created' | 'updated' | 'destroyed';
212
213 export interface MutationListenerOptions {
214   /**
215    * Skip the initial call of the listener with pre-existing DOM nodes.
216    *
217    * The default is currently true for backwards compatibility with <= 0.16.1
218    * but this default is expected to change to false in 0.17.0.
219    */
220   skipInitialization?: boolean;
221 }
222
223 const DEFAULT_SKIP_INITIALIZATION = true;
224
225 export type UpdateListener = (arg0: {
226   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
227   dirtyLeaves: Set<NodeKey>;
228   editorState: EditorState;
229   normalizedNodes: Set<NodeKey>;
230   prevEditorState: EditorState;
231   tags: Set<string>;
232 }) => void;
233
234 export type DecoratorListener<T = never> = (
235   decorator: Record<NodeKey, T>,
236 ) => void;
237
238 export type RootListener = (
239   rootElement: null | HTMLElement,
240   prevRootElement: null | HTMLElement,
241 ) => void;
242
243 export type TextContentListener = (text: string) => void;
244
245 export type MutationListener = (
246   nodes: Map<NodeKey, NodeMutation>,
247   payload: {
248     updateTags: Set<string>;
249     dirtyLeaves: Set<string>;
250     prevEditorState: EditorState;
251   },
252 ) => void;
253
254 export type CommandListener<P> = (payload: P, editor: LexicalEditor) => boolean;
255
256 export type EditableListener = (editable: boolean) => void;
257
258 export type CommandListenerPriority = 0 | 1 | 2 | 3 | 4;
259
260 export const COMMAND_PRIORITY_EDITOR = 0;
261 export const COMMAND_PRIORITY_LOW = 1;
262 export const COMMAND_PRIORITY_NORMAL = 2;
263 export const COMMAND_PRIORITY_HIGH = 3;
264 export const COMMAND_PRIORITY_CRITICAL = 4;
265
266 // eslint-disable-next-line @typescript-eslint/no-unused-vars
267 export type LexicalCommand<TPayload> = {
268   type?: string;
269 };
270
271 /**
272  * Type helper for extracting the payload type from a command.
273  *
274  * @example
275  * ```ts
276  * const MY_COMMAND = createCommand<SomeType>();
277  *
278  * // ...
279  *
280  * editor.registerCommand(MY_COMMAND, payload => {
281  *   // Type of `payload` is inferred here. But lets say we want to extract a function to delegate to
282  *   handleMyCommand(editor, payload);
283  *   return true;
284  * });
285  *
286  * function handleMyCommand(editor: LexicalEditor, payload: CommandPayloadType<typeof MY_COMMAND>) {
287  *   // `payload` is of type `SomeType`, extracted from the command.
288  * }
289  * ```
290  */
291 export type CommandPayloadType<TCommand extends LexicalCommand<unknown>> =
292   TCommand extends LexicalCommand<infer TPayload> ? TPayload : never;
293
294 type Commands = Map<
295   LexicalCommand<unknown>,
296   Array<Set<CommandListener<unknown>>>
297 >;
298 type Listeners = {
299   decorator: Set<DecoratorListener>;
300   mutation: MutationListeners;
301   editable: Set<EditableListener>;
302   root: Set<RootListener>;
303   textcontent: Set<TextContentListener>;
304   update: Set<UpdateListener>;
305 };
306
307 export type Listener =
308   | DecoratorListener
309   | EditableListener
310   | MutationListener
311   | RootListener
312   | TextContentListener
313   | UpdateListener;
314
315 export type ListenerType =
316   | 'update'
317   | 'root'
318   | 'decorator'
319   | 'textcontent'
320   | 'mutation'
321   | 'editable';
322
323 export type TransformerType = 'text' | 'decorator' | 'element' | 'root';
324
325 type IntentionallyMarkedAsDirtyElement = boolean;
326
327 type DOMConversionCache = Map<
328   string,
329   Array<(node: Node) => DOMConversion | null>
330 >;
331
332 export type SerializedEditor = {
333   editorState: SerializedEditorState;
334 };
335
336 export function resetEditor(
337   editor: LexicalEditor,
338   prevRootElement: null | HTMLElement,
339   nextRootElement: null | HTMLElement,
340   pendingEditorState: EditorState,
341 ): void {
342   const keyNodeMap = editor._keyToDOMMap;
343   keyNodeMap.clear();
344   editor._editorState = createEmptyEditorState();
345   editor._pendingEditorState = pendingEditorState;
346   editor._compositionKey = null;
347   editor._dirtyType = NO_DIRTY_NODES;
348   editor._cloneNotNeeded.clear();
349   editor._dirtyLeaves = new Set();
350   editor._dirtyElements.clear();
351   editor._normalizedNodes = new Set();
352   editor._updateTags = new Set();
353   editor._updates = [];
354   editor._blockCursorElement = null;
355
356   const observer = editor._observer;
357
358   if (observer !== null) {
359     observer.disconnect();
360     editor._observer = null;
361   }
362
363   // Remove all the DOM nodes from the root element
364   if (prevRootElement !== null) {
365     prevRootElement.textContent = '';
366   }
367
368   if (nextRootElement !== null) {
369     nextRootElement.textContent = '';
370     keyNodeMap.set('root', nextRootElement);
371   }
372 }
373
374 function initializeConversionCache(
375   nodes: RegisteredNodes,
376   additionalConversions?: DOMConversionMap,
377 ): DOMConversionCache {
378   const conversionCache = new Map();
379   const handledConversions = new Set();
380   const addConversionsToCache = (map: DOMConversionMap) => {
381     Object.keys(map).forEach((key) => {
382       let currentCache = conversionCache.get(key);
383
384       if (currentCache === undefined) {
385         currentCache = [];
386         conversionCache.set(key, currentCache);
387       }
388
389       currentCache.push(map[key]);
390     });
391   };
392   nodes.forEach((node) => {
393     const importDOM = node.klass.importDOM;
394
395     if (importDOM == null || handledConversions.has(importDOM)) {
396       return;
397     }
398
399     handledConversions.add(importDOM);
400     const map = importDOM.call(node.klass);
401
402     if (map !== null) {
403       addConversionsToCache(map);
404     }
405   });
406   if (additionalConversions) {
407     addConversionsToCache(additionalConversions);
408   }
409   return conversionCache;
410 }
411
412 /**
413  * Creates a new LexicalEditor attached to a single contentEditable (provided in the config). This is
414  * the lowest-level initialization API for a LexicalEditor. If you're using React or another framework,
415  * consider using the appropriate abstractions, such as LexicalComposer
416  * @param editorConfig - the editor configuration.
417  * @returns a LexicalEditor instance
418  */
419 export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
420   const config = editorConfig || {};
421   const activeEditor = internalGetActiveEditor();
422   const theme = config.theme || {};
423   const parentEditor =
424     editorConfig === undefined ? activeEditor : config.parentEditor || null;
425   const disableEvents = config.disableEvents || false;
426   const editorState = createEmptyEditorState();
427   const namespace =
428     config.namespace ||
429     (parentEditor !== null ? parentEditor._config.namespace : createUID());
430   const initialEditorState = config.editorState;
431   const nodes = [
432     RootNode,
433     TextNode,
434     LineBreakNode,
435     TabNode,
436     ParagraphNode,
437     ArtificialNode__DO_NOT_USE,
438     ...(config.nodes || []),
439   ];
440   const {onError, html} = config;
441   const isEditable = config.editable !== undefined ? config.editable : true;
442   let registeredNodes: Map<string, RegisteredNode>;
443
444   if (editorConfig === undefined && activeEditor !== null) {
445     registeredNodes = activeEditor._nodes;
446   } else {
447     registeredNodes = new Map();
448     for (let i = 0; i < nodes.length; i++) {
449       let klass = nodes[i];
450       let replace: RegisteredNode['replace'] = null;
451       let replaceWithKlass: RegisteredNode['replaceWithKlass'] = null;
452
453       if (typeof klass !== 'function') {
454         const options = klass;
455         klass = options.replace;
456         replace = options.with;
457         replaceWithKlass = options.withKlass || null;
458       }
459       // Ensure custom nodes implement required methods and replaceWithKlass is instance of base klass.
460       if (__DEV__) {
461         // ArtificialNode__DO_NOT_USE can get renamed, so we use the type
462         const nodeType =
463           Object.prototype.hasOwnProperty.call(klass, 'getType') &&
464           klass.getType();
465         const name = klass.name;
466
467         if (replaceWithKlass) {
468           invariant(
469             replaceWithKlass.prototype instanceof klass,
470             "%s doesn't extend the %s",
471             replaceWithKlass.name,
472             name,
473           );
474         }
475
476         if (
477           name !== 'RootNode' &&
478           nodeType !== 'root' &&
479           nodeType !== 'artificial'
480         ) {
481           const proto = klass.prototype;
482           ['getType', 'clone'].forEach((method) => {
483             // eslint-disable-next-line no-prototype-builtins
484             if (!klass.hasOwnProperty(method)) {
485               console.warn(`${name} must implement static "${method}" method`);
486             }
487           });
488           if (
489             // eslint-disable-next-line no-prototype-builtins
490             !klass.hasOwnProperty('importDOM') &&
491             // eslint-disable-next-line no-prototype-builtins
492             klass.hasOwnProperty('exportDOM')
493           ) {
494             console.warn(
495               `${name} should implement "importDOM" if using a custom "exportDOM" method to ensure HTML serialization (important for copy & paste) works as expected`,
496             );
497           }
498           if (proto instanceof DecoratorNode) {
499             // eslint-disable-next-line no-prototype-builtins
500             if (!proto.hasOwnProperty('decorate')) {
501               console.warn(
502                 `${proto.constructor.name} must implement "decorate" method`,
503               );
504             }
505           }
506           if (
507             // eslint-disable-next-line no-prototype-builtins
508             !klass.hasOwnProperty('importJSON')
509           ) {
510             console.warn(
511               `${name} should implement "importJSON" method to ensure JSON and default HTML serialization works as expected`,
512             );
513           }
514           if (
515             // eslint-disable-next-line no-prototype-builtins
516             !proto.hasOwnProperty('exportJSON')
517           ) {
518             console.warn(
519               `${name} should implement "exportJSON" method to ensure JSON and default HTML serialization works as expected`,
520             );
521           }
522         }
523       }
524       const type = klass.getType();
525       const transform = klass.transform();
526       const transforms = new Set<Transform<LexicalNode>>();
527       if (transform !== null) {
528         transforms.add(transform);
529       }
530       registeredNodes.set(type, {
531         exportDOM: html && html.export ? html.export.get(klass) : undefined,
532         klass,
533         replace,
534         replaceWithKlass,
535         transforms,
536       });
537     }
538   }
539   const editor = new LexicalEditor(
540     editorState,
541     parentEditor,
542     registeredNodes,
543     {
544       disableEvents,
545       namespace,
546       theme,
547     },
548     onError ? onError : console.error,
549     initializeConversionCache(registeredNodes, html ? html.import : undefined),
550     isEditable,
551   );
552
553   if (initialEditorState !== undefined) {
554     editor._pendingEditorState = initialEditorState;
555     editor._dirtyType = FULL_RECONCILE;
556   }
557
558   return editor;
559 }
560 export class LexicalEditor {
561   ['constructor']!: KlassConstructor<typeof LexicalEditor>;
562
563   /** The version with build identifiers for this editor (since 0.17.1) */
564   static version: string | undefined;
565
566   /** @internal */
567   _headless: boolean;
568   /** @internal */
569   _parentEditor: null | LexicalEditor;
570   /** @internal */
571   _rootElement: null | HTMLElement;
572   /** @internal */
573   _editorState: EditorState;
574   /** @internal */
575   _pendingEditorState: null | EditorState;
576   /** @internal */
577   _compositionKey: null | NodeKey;
578   /** @internal */
579   _deferred: Array<() => void>;
580   /** @internal */
581   _keyToDOMMap: Map<NodeKey, HTMLElement>;
582   /** @internal */
583   _updates: Array<[() => void, EditorUpdateOptions | undefined]>;
584   /** @internal */
585   _updating: boolean;
586   /** @internal */
587   _listeners: Listeners;
588   /** @internal */
589   _commands: Commands;
590   /** @internal */
591   _nodes: RegisteredNodes;
592   /** @internal */
593   _decorators: Record<NodeKey, unknown>;
594   /** @internal */
595   _pendingDecorators: null | Record<NodeKey, unknown>;
596   /** @internal */
597   _config: EditorConfig;
598   /** @internal */
599   _dirtyType: 0 | 1 | 2;
600   /** @internal */
601   _cloneNotNeeded: Set<NodeKey>;
602   /** @internal */
603   _dirtyLeaves: Set<NodeKey>;
604   /** @internal */
605   _dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
606   /** @internal */
607   _normalizedNodes: Set<NodeKey>;
608   /** @internal */
609   _updateTags: Set<string>;
610   /** @internal */
611   _observer: null | MutationObserver;
612   /** @internal */
613   _key: string;
614   /** @internal */
615   _onError: ErrorHandler;
616   /** @internal */
617   _htmlConversions: DOMConversionCache;
618   /** @internal */
619   _window: null | Window;
620   /** @internal */
621   _editable: boolean;
622   /** @internal */
623   _blockCursorElement: null | HTMLDivElement;
624
625   /** @internal */
626   constructor(
627     editorState: EditorState,
628     parentEditor: null | LexicalEditor,
629     nodes: RegisteredNodes,
630     config: EditorConfig,
631     onError: ErrorHandler,
632     htmlConversions: DOMConversionCache,
633     editable: boolean,
634   ) {
635     this._parentEditor = parentEditor;
636     // The root element associated with this editor
637     this._rootElement = null;
638     // The current editor state
639     this._editorState = editorState;
640     // Handling of drafts and updates
641     this._pendingEditorState = null;
642     // Used to help co-ordinate selection and events
643     this._compositionKey = null;
644     this._deferred = [];
645     // Used during reconciliation
646     this._keyToDOMMap = new Map();
647     this._updates = [];
648     this._updating = false;
649     // Listeners
650     this._listeners = {
651       decorator: new Set(),
652       editable: new Set(),
653       mutation: new Map(),
654       root: new Set(),
655       textcontent: new Set(),
656       update: new Set(),
657     };
658     // Commands
659     this._commands = new Map();
660     // Editor configuration for theme/context.
661     this._config = config;
662     // Mapping of types to their nodes
663     this._nodes = nodes;
664     // React node decorators for portals
665     this._decorators = {};
666     this._pendingDecorators = null;
667     // Used to optimize reconciliation
668     this._dirtyType = NO_DIRTY_NODES;
669     this._cloneNotNeeded = new Set();
670     this._dirtyLeaves = new Set();
671     this._dirtyElements = new Map();
672     this._normalizedNodes = new Set();
673     this._updateTags = new Set();
674     // Handling of DOM mutations
675     this._observer = null;
676     // Used for identifying owning editors
677     this._key = createUID();
678
679     this._onError = onError;
680     this._htmlConversions = htmlConversions;
681     this._editable = editable;
682     this._headless = parentEditor !== null && parentEditor._headless;
683     this._window = null;
684     this._blockCursorElement = null;
685   }
686
687   /**
688    *
689    * @returns true if the editor is currently in "composition" mode due to receiving input
690    * through an IME, or 3P extension, for example. Returns false otherwise.
691    */
692   isComposing(): boolean {
693     return this._compositionKey != null;
694   }
695   /**
696    * Registers a listener for Editor update event. Will trigger the provided callback
697    * each time the editor goes through an update (via {@link LexicalEditor.update}) until the
698    * teardown function is called.
699    *
700    * @returns a teardown function that can be used to cleanup the listener.
701    */
702   registerUpdateListener(listener: UpdateListener): () => void {
703     const listenerSetOrMap = this._listeners.update;
704     listenerSetOrMap.add(listener);
705     return () => {
706       listenerSetOrMap.delete(listener);
707     };
708   }
709   /**
710    * Registers a listener for for when the editor changes between editable and non-editable states.
711    * Will trigger the provided callback each time the editor transitions between these states until the
712    * teardown function is called.
713    *
714    * @returns a teardown function that can be used to cleanup the listener.
715    */
716   registerEditableListener(listener: EditableListener): () => void {
717     const listenerSetOrMap = this._listeners.editable;
718     listenerSetOrMap.add(listener);
719     return () => {
720       listenerSetOrMap.delete(listener);
721     };
722   }
723   /**
724    * Registers a listener for when the editor's decorator object changes. The decorator object contains
725    * all DecoratorNode keys -> their decorated value. This is primarily used with external UI frameworks.
726    *
727    * Will trigger the provided callback each time the editor transitions between these states until the
728    * teardown function is called.
729    *
730    * @returns a teardown function that can be used to cleanup the listener.
731    */
732   registerDecoratorListener<T>(listener: DecoratorListener<T>): () => void {
733     const listenerSetOrMap = this._listeners.decorator;
734     listenerSetOrMap.add(listener);
735     return () => {
736       listenerSetOrMap.delete(listener);
737     };
738   }
739   /**
740    * Registers a listener for when Lexical commits an update to the DOM and the text content of
741    * the editor changes from the previous state of the editor. If the text content is the
742    * same between updates, no notifications to the listeners will happen.
743    *
744    * Will trigger the provided callback each time the editor transitions between these states until the
745    * teardown function is called.
746    *
747    * @returns a teardown function that can be used to cleanup the listener.
748    */
749   registerTextContentListener(listener: TextContentListener): () => void {
750     const listenerSetOrMap = this._listeners.textcontent;
751     listenerSetOrMap.add(listener);
752     return () => {
753       listenerSetOrMap.delete(listener);
754     };
755   }
756   /**
757    * Registers a listener for when the editor's root DOM element (the content editable
758    * Lexical attaches to) changes. This is primarily used to attach event listeners to the root
759    *  element. The root listener function is executed directly upon registration and then on
760    * any subsequent update.
761    *
762    * Will trigger the provided callback each time the editor transitions between these states until the
763    * teardown function is called.
764    *
765    * @returns a teardown function that can be used to cleanup the listener.
766    */
767   registerRootListener(listener: RootListener): () => void {
768     const listenerSetOrMap = this._listeners.root;
769     listener(this._rootElement, null);
770     listenerSetOrMap.add(listener);
771     return () => {
772       listener(null, this._rootElement);
773       listenerSetOrMap.delete(listener);
774     };
775   }
776   /**
777    * Registers a listener that will trigger anytime the provided command
778    * is dispatched, subject to priority. Listeners that run at a higher priority can "intercept"
779    * commands and prevent them from propagating to other handlers by returning true.
780    *
781    * Listeners registered at the same priority level will run deterministically in the order of registration.
782    *
783    * @param command - the command that will trigger the callback.
784    * @param listener - the function that will execute when the command is dispatched.
785    * @param priority - the relative priority of the listener. 0 | 1 | 2 | 3 | 4
786    * @returns a teardown function that can be used to cleanup the listener.
787    */
788   registerCommand<P>(
789     command: LexicalCommand<P>,
790     listener: CommandListener<P>,
791     priority: CommandListenerPriority,
792   ): () => void {
793     if (priority === undefined) {
794       invariant(false, 'Listener for type "command" requires a "priority".');
795     }
796
797     const commandsMap = this._commands;
798
799     if (!commandsMap.has(command)) {
800       commandsMap.set(command, [
801         new Set(),
802         new Set(),
803         new Set(),
804         new Set(),
805         new Set(),
806       ]);
807     }
808
809     const listenersInPriorityOrder = commandsMap.get(command);
810
811     if (listenersInPriorityOrder === undefined) {
812       invariant(
813         false,
814         'registerCommand: Command %s not found in command map',
815         String(command),
816       );
817     }
818
819     const listeners = listenersInPriorityOrder[priority];
820     listeners.add(listener as CommandListener<unknown>);
821     return () => {
822       listeners.delete(listener as CommandListener<unknown>);
823
824       if (
825         listenersInPriorityOrder.every(
826           (listenersSet) => listenersSet.size === 0,
827         )
828       ) {
829         commandsMap.delete(command);
830       }
831     };
832   }
833
834   /**
835    * Registers a listener that will run when a Lexical node of the provided class is
836    * mutated. The listener will receive a list of nodes along with the type of mutation
837    * that was performed on each: created, destroyed, or updated.
838    *
839    * One common use case for this is to attach DOM event listeners to the underlying DOM nodes as Lexical nodes are created.
840    * {@link LexicalEditor.getElementByKey} can be used for this.
841    *
842    * If any existing nodes are in the DOM, and skipInitialization is not true, the listener
843    * will be called immediately with an updateTag of 'registerMutationListener' where all
844    * nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option
845    * (default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).
846    *
847    * @param klass - The class of the node that you want to listen to mutations on.
848    * @param listener - The logic you want to run when the node is mutated.
849    * @param options - see {@link MutationListenerOptions}
850    * @returns a teardown function that can be used to cleanup the listener.
851    */
852   registerMutationListener(
853     klass: Klass<LexicalNode>,
854     listener: MutationListener,
855     options?: MutationListenerOptions,
856   ): () => void {
857     const klassToMutate = this.resolveRegisteredNodeAfterReplacements(
858       this.getRegisteredNode(klass),
859     ).klass;
860     const mutations = this._listeners.mutation;
861     mutations.set(listener, klassToMutate);
862     const skipInitialization = options && options.skipInitialization;
863     if (
864       !(skipInitialization === undefined
865         ? DEFAULT_SKIP_INITIALIZATION
866         : skipInitialization)
867     ) {
868       this.initializeMutationListener(listener, klassToMutate);
869     }
870
871     return () => {
872       mutations.delete(listener);
873     };
874   }
875
876   /** @internal */
877   private getRegisteredNode(klass: Klass<LexicalNode>): RegisteredNode {
878     const registeredNode = this._nodes.get(klass.getType());
879
880     if (registeredNode === undefined) {
881       invariant(
882         false,
883         'Node %s has not been registered. Ensure node has been passed to createEditor.',
884         klass.name,
885       );
886     }
887
888     return registeredNode;
889   }
890
891   /** @internal */
892   private resolveRegisteredNodeAfterReplacements(
893     registeredNode: RegisteredNode,
894   ): RegisteredNode {
895     while (registeredNode.replaceWithKlass) {
896       registeredNode = this.getRegisteredNode(registeredNode.replaceWithKlass);
897     }
898     return registeredNode;
899   }
900
901   /** @internal */
902   private initializeMutationListener(
903     listener: MutationListener,
904     klass: Klass<LexicalNode>,
905   ): void {
906     const prevEditorState = this._editorState;
907     const nodeMap = getCachedTypeToNodeMap(prevEditorState).get(
908       klass.getType(),
909     );
910     if (!nodeMap) {
911       return;
912     }
913     const nodeMutationMap = new Map<string, NodeMutation>();
914     for (const k of nodeMap.keys()) {
915       nodeMutationMap.set(k, 'created');
916     }
917     if (nodeMutationMap.size > 0) {
918       listener(nodeMutationMap, {
919         dirtyLeaves: new Set(),
920         prevEditorState,
921         updateTags: new Set(['registerMutationListener']),
922       });
923     }
924   }
925
926   /** @internal */
927   private registerNodeTransformToKlass<T extends LexicalNode>(
928     klass: Klass<T>,
929     listener: Transform<T>,
930   ): RegisteredNode {
931     const registeredNode = this.getRegisteredNode(klass);
932     registeredNode.transforms.add(listener as Transform<LexicalNode>);
933
934     return registeredNode;
935   }
936
937   /**
938    * Registers a listener that will run when a Lexical node of the provided class is
939    * marked dirty during an update. The listener will continue to run as long as the node
940    * is marked dirty. There are no guarantees around the order of transform execution!
941    *
942    * Watch out for infinite loops. See [Node Transforms](https://lexical.dev/docs/concepts/transforms)
943    * @param klass - The class of the node that you want to run transforms on.
944    * @param listener - The logic you want to run when the node is updated.
945    * @returns a teardown function that can be used to cleanup the listener.
946    */
947   registerNodeTransform<T extends LexicalNode>(
948     klass: Klass<T>,
949     listener: Transform<T>,
950   ): () => void {
951     const registeredNode = this.registerNodeTransformToKlass(klass, listener);
952     const registeredNodes = [registeredNode];
953
954     const replaceWithKlass = registeredNode.replaceWithKlass;
955     if (replaceWithKlass != null) {
956       const registeredReplaceWithNode = this.registerNodeTransformToKlass(
957         replaceWithKlass,
958         listener as Transform<LexicalNode>,
959       );
960       registeredNodes.push(registeredReplaceWithNode);
961     }
962
963     markAllNodesAsDirty(this, klass.getType());
964     return () => {
965       registeredNodes.forEach((node) =>
966         node.transforms.delete(listener as Transform<LexicalNode>),
967       );
968     };
969   }
970
971   /**
972    * Used to assert that a certain node is registered, usually by plugins to ensure nodes that they
973    * depend on have been registered.
974    * @returns True if the editor has registered the provided node type, false otherwise.
975    */
976   hasNode<T extends Klass<LexicalNode>>(node: T): boolean {
977     return this._nodes.has(node.getType());
978   }
979
980   /**
981    * Used to assert that certain nodes are registered, usually by plugins to ensure nodes that they
982    * depend on have been registered.
983    * @returns True if the editor has registered all of the provided node types, false otherwise.
984    */
985   hasNodes<T extends Klass<LexicalNode>>(nodes: Array<T>): boolean {
986     return nodes.every(this.hasNode.bind(this));
987   }
988
989   /**
990    * Dispatches a command of the specified type with the specified payload.
991    * This triggers all command listeners (set by {@link LexicalEditor.registerCommand})
992    * for this type, passing them the provided payload.
993    * @param type - the type of command listeners to trigger.
994    * @param payload - the data to pass as an argument to the command listeners.
995    */
996   dispatchCommand<TCommand extends LexicalCommand<unknown>>(
997     type: TCommand,
998     payload: CommandPayloadType<TCommand>,
999   ): boolean {
1000     return dispatchCommand(this, type, payload);
1001   }
1002
1003   /**
1004    * Gets a map of all decorators in the editor.
1005    * @returns A mapping of call decorator keys to their decorated content
1006    */
1007   getDecorators<T>(): Record<NodeKey, T> {
1008     return this._decorators as Record<NodeKey, T>;
1009   }
1010
1011   /**
1012    *
1013    * @returns the current root element of the editor. If you want to register
1014    * an event listener, do it via {@link LexicalEditor.registerRootListener}, since
1015    * this reference may not be stable.
1016    */
1017   getRootElement(): null | HTMLElement {
1018     return this._rootElement;
1019   }
1020
1021   /**
1022    * Gets the key of the editor
1023    * @returns The editor key
1024    */
1025   getKey(): string {
1026     return this._key;
1027   }
1028
1029   /**
1030    * Imperatively set the root contenteditable element that Lexical listens
1031    * for events on.
1032    */
1033   setRootElement(nextRootElement: null | HTMLElement): void {
1034     const prevRootElement = this._rootElement;
1035
1036     if (nextRootElement !== prevRootElement) {
1037       const classNames = getCachedClassNameArray(this._config.theme, 'root');
1038       const pendingEditorState = this._pendingEditorState || this._editorState;
1039       this._rootElement = nextRootElement;
1040       resetEditor(this, prevRootElement, nextRootElement, pendingEditorState);
1041
1042       if (prevRootElement !== null) {
1043         // TODO: remove this flag once we no longer use UEv2 internally
1044         if (!this._config.disableEvents) {
1045           removeRootElementEvents(prevRootElement);
1046         }
1047         if (classNames != null) {
1048           prevRootElement.classList.remove(...classNames);
1049         }
1050       }
1051
1052       if (nextRootElement !== null) {
1053         const windowObj = getDefaultView(nextRootElement);
1054         const style = nextRootElement.style;
1055         style.userSelect = 'text';
1056         style.whiteSpace = 'pre-wrap';
1057         style.wordBreak = 'break-word';
1058         nextRootElement.setAttribute('data-lexical-editor', 'true');
1059         this._window = windowObj;
1060         this._dirtyType = FULL_RECONCILE;
1061         initMutationObserver(this);
1062
1063         this._updateTags.add('history-merge');
1064
1065         $commitPendingUpdates(this);
1066
1067         // TODO: remove this flag once we no longer use UEv2 internally
1068         if (!this._config.disableEvents) {
1069           addRootElementEvents(nextRootElement, this);
1070         }
1071         if (classNames != null) {
1072           nextRootElement.classList.add(...classNames);
1073         }
1074       } else {
1075         // If content editable is unmounted we'll reset editor state back to original
1076         // (or pending) editor state since there will be no reconciliation
1077         this._editorState = pendingEditorState;
1078         this._pendingEditorState = null;
1079         this._window = null;
1080       }
1081
1082       triggerListeners('root', this, false, nextRootElement, prevRootElement);
1083     }
1084   }
1085
1086   /**
1087    * Gets the underlying HTMLElement associated with the LexicalNode for the given key.
1088    * @returns the HTMLElement rendered by the LexicalNode associated with the key.
1089    * @param key - the key of the LexicalNode.
1090    */
1091   getElementByKey(key: NodeKey): HTMLElement | null {
1092     return this._keyToDOMMap.get(key) || null;
1093   }
1094
1095   /**
1096    * Gets the active editor state.
1097    * @returns The editor state
1098    */
1099   getEditorState(): EditorState {
1100     return this._editorState;
1101   }
1102
1103   /**
1104    * Imperatively set the EditorState. Triggers reconciliation like an update.
1105    * @param editorState - the state to set the editor
1106    * @param options - options for the update.
1107    */
1108   setEditorState(editorState: EditorState, options?: EditorSetOptions): void {
1109     if (editorState.isEmpty()) {
1110       invariant(
1111         false,
1112         "setEditorState: the editor state is empty. Ensure the editor state's root node never becomes empty.",
1113       );
1114     }
1115
1116     $flushRootMutations(this);
1117     const pendingEditorState = this._pendingEditorState;
1118     const tags = this._updateTags;
1119     const tag = options !== undefined ? options.tag : null;
1120
1121     if (pendingEditorState !== null && !pendingEditorState.isEmpty()) {
1122       if (tag != null) {
1123         tags.add(tag);
1124       }
1125
1126       $commitPendingUpdates(this);
1127     }
1128
1129     this._pendingEditorState = editorState;
1130     this._dirtyType = FULL_RECONCILE;
1131     this._dirtyElements.set('root', false);
1132     this._compositionKey = null;
1133
1134     if (tag != null) {
1135       tags.add(tag);
1136     }
1137
1138     $commitPendingUpdates(this);
1139   }
1140
1141   /**
1142    * Parses a SerializedEditorState (usually produced by {@link EditorState.toJSON}) and returns
1143    * and EditorState object that can be, for example, passed to {@link LexicalEditor.setEditorState}. Typically,
1144    * deserialization from JSON stored in a database uses this method.
1145    * @param maybeStringifiedEditorState
1146    * @param updateFn
1147    * @returns
1148    */
1149   parseEditorState(
1150     maybeStringifiedEditorState: string | SerializedEditorState,
1151     updateFn?: () => void,
1152   ): EditorState {
1153     const serializedEditorState =
1154       typeof maybeStringifiedEditorState === 'string'
1155         ? JSON.parse(maybeStringifiedEditorState)
1156         : maybeStringifiedEditorState;
1157     return parseEditorState(serializedEditorState, this, updateFn);
1158   }
1159
1160   /**
1161    * Executes a read of the editor's state, with the
1162    * editor context available (useful for exporting and read-only DOM
1163    * operations). Much like update, but prevents any mutation of the
1164    * editor's state. Any pending updates will be flushed immediately before
1165    * the read.
1166    * @param callbackFn - A function that has access to read-only editor state.
1167    */
1168   read<T>(callbackFn: () => T): T {
1169     $commitPendingUpdates(this);
1170     return this.getEditorState().read(callbackFn, {editor: this});
1171   }
1172
1173   /**
1174    * Executes an update to the editor state. The updateFn callback is the ONLY place
1175    * where Lexical editor state can be safely mutated.
1176    * @param updateFn - A function that has access to writable editor state.
1177    * @param options - A bag of options to control the behavior of the update.
1178    * @param options.onUpdate - A function to run once the update is complete.
1179    * Useful for synchronizing updates in some cases.
1180    * @param options.skipTransforms - Setting this to true will suppress all node
1181    * transforms for this update cycle.
1182    * @param options.tag - A tag to identify this update, in an update listener, for instance.
1183    * Some tags are reserved by the core and control update behavior in different ways.
1184    * @param options.discrete - If true, prevents this update from being batched, forcing it to
1185    * run synchronously.
1186    */
1187   update(updateFn: () => void, options?: EditorUpdateOptions): void {
1188     updateEditor(this, updateFn, options);
1189   }
1190
1191   /**
1192    * Focuses the editor
1193    * @param callbackFn - A function to run after the editor is focused.
1194    * @param options - A bag of options
1195    * @param options.defaultSelection - Where to move selection when the editor is
1196    * focused. Can be rootStart, rootEnd, or undefined. Defaults to rootEnd.
1197    */
1198   focus(callbackFn?: () => void, options: EditorFocusOptions = {}): void {
1199     const rootElement = this._rootElement;
1200
1201     if (rootElement !== null) {
1202       // This ensures that iOS does not trigger caps lock upon focus
1203       rootElement.setAttribute('autocapitalize', 'off');
1204       updateEditor(
1205         this,
1206         () => {
1207           const selection = $getSelection();
1208           const root = $getRoot();
1209
1210           if (selection !== null) {
1211             // Marking the selection dirty will force the selection back to it
1212             selection.dirty = true;
1213           } else if (root.getChildrenSize() !== 0) {
1214             if (options.defaultSelection === 'rootStart') {
1215               root.selectStart();
1216             } else {
1217               root.selectEnd();
1218             }
1219           }
1220         },
1221         {
1222           onUpdate: () => {
1223             rootElement.removeAttribute('autocapitalize');
1224             if (callbackFn) {
1225               callbackFn();
1226             }
1227           },
1228           tag: 'focus',
1229         },
1230       );
1231       // In the case where onUpdate doesn't fire (due to the focus update not
1232       // occuring).
1233       if (this._pendingEditorState === null) {
1234         rootElement.removeAttribute('autocapitalize');
1235       }
1236     }
1237   }
1238
1239   /**
1240    * Removes focus from the editor.
1241    */
1242   blur(): void {
1243     const rootElement = this._rootElement;
1244
1245     if (rootElement !== null) {
1246       rootElement.blur();
1247     }
1248
1249     const domSelection = getDOMSelection(this._window);
1250
1251     if (domSelection !== null) {
1252       domSelection.removeAllRanges();
1253     }
1254   }
1255   /**
1256    * Returns true if the editor is editable, false otherwise.
1257    * @returns True if the editor is editable, false otherwise.
1258    */
1259   isEditable(): boolean {
1260     return this._editable;
1261   }
1262   /**
1263    * Sets the editable property of the editor. When false, the
1264    * editor will not listen for user events on the underling contenteditable.
1265    * @param editable - the value to set the editable mode to.
1266    */
1267   setEditable(editable: boolean): void {
1268     if (this._editable !== editable) {
1269       this._editable = editable;
1270       triggerListeners('editable', this, true, editable);
1271     }
1272   }
1273   /**
1274    * Returns a JSON-serializable javascript object NOT a JSON string.
1275    * You still must call JSON.stringify (or something else) to turn the
1276    * state into a string you can transfer over the wire and store in a database.
1277    *
1278    * See {@link LexicalNode.exportJSON}
1279    *
1280    * @returns A JSON-serializable javascript object
1281    */
1282   toJSON(): SerializedEditor {
1283     return {
1284       editorState: this._editorState.toJSON(),
1285     };
1286   }
1287 }
1288
1289 LexicalEditor.version = '0.17.1';