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 {EditorState, SerializedEditorState} from './LexicalEditorState';
16 } from './LexicalNode';
18 import invariant from 'lexical/shared/invariant';
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';
27 $commitPendingUpdates,
28 internalGetActiveEditor,
32 } from './LexicalUpdates';
36 getCachedClassNameArray,
37 getCachedTypeToNodeMap,
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';
49 export type Spread<T1, T2> = Omit<T2, keyof T1> & T1;
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;
58 export type Klass<T extends LexicalNode> = InstanceType<
62 : GenericConstructor<T> & T['constructor'];
64 export type EditorThemeClassName = string;
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;
80 export type EditorUpdateOptions = {
81 onUpdate?: () => void;
82 skipTransforms?: true;
87 export type EditorSetOptions = {
91 export type EditorFocusOptions = {
92 defaultSelection?: 'rootStart' | 'rootEnd';
95 export type EditorThemeClasses = {
96 blockCursor?: EditorThemeClassName;
97 characterLimit?: EditorThemeClassName;
98 code?: EditorThemeClassName;
99 codeHighlight?: Record<string, EditorThemeClassName>;
100 hashtag?: EditorThemeClassName;
102 h1?: EditorThemeClassName;
103 h2?: EditorThemeClassName;
104 h3?: EditorThemeClassName;
105 h4?: EditorThemeClassName;
106 h5?: EditorThemeClassName;
107 h6?: EditorThemeClassName;
109 hr?: EditorThemeClassName;
110 image?: EditorThemeClassName;
111 link?: EditorThemeClassName;
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;
122 list?: EditorThemeClassName;
123 listitem?: EditorThemeClassName;
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;
150 base?: EditorThemeClassName;
151 focus?: EditorThemeClassName;
153 indent?: EditorThemeClassName;
154 // eslint-disable-next-line @typescript-eslint/no-explicit-any
158 export type EditorConfig = {
159 disableEvents?: boolean;
161 theme: EditorThemeClasses;
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>,
170 withKlass?: Klass<LexicalNode>;
173 export type HTMLConfig = {
174 export?: DOMExportOutputMap;
175 import?: DOMConversionMap;
178 export type CreateEditorArgs = {
179 disableEvents?: boolean;
180 editorState?: EditorState;
182 nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
183 onError?: ErrorHandler;
184 parentEditor?: LexicalEditor;
186 theme?: EditorThemeClasses;
190 export type RegisteredNodes = Map<string, RegisteredNode>;
192 export type RegisteredNode = {
193 klass: Klass<LexicalNode>;
194 transforms: Set<Transform<LexicalNode>>;
195 replace: null | ((node: LexicalNode) => LexicalNode);
196 replaceWithKlass: null | Klass<LexicalNode>;
198 editor: LexicalEditor,
199 targetNode: LexicalNode,
200 ) => DOMExportOutput;
203 export type Transform<T extends LexicalNode> = (node: T) => void;
205 export type ErrorHandler = (error: Error) => void;
207 export type MutationListeners = Map<MutationListener, Klass<LexicalNode>>;
209 export type MutatedNodes = Map<Klass<LexicalNode>, Map<NodeKey, NodeMutation>>;
211 export type NodeMutation = 'created' | 'updated' | 'destroyed';
213 export interface MutationListenerOptions {
215 * Skip the initial call of the listener with pre-existing DOM nodes.
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.
220 skipInitialization?: boolean;
223 const DEFAULT_SKIP_INITIALIZATION = true;
225 export type UpdateListener = (arg0: {
226 dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
227 dirtyLeaves: Set<NodeKey>;
228 editorState: EditorState;
229 normalizedNodes: Set<NodeKey>;
230 prevEditorState: EditorState;
234 export type DecoratorListener<T = never> = (
235 decorator: Record<NodeKey, T>,
238 export type RootListener = (
239 rootElement: null | HTMLElement,
240 prevRootElement: null | HTMLElement,
243 export type TextContentListener = (text: string) => void;
245 export type MutationListener = (
246 nodes: Map<NodeKey, NodeMutation>,
248 updateTags: Set<string>;
249 dirtyLeaves: Set<string>;
250 prevEditorState: EditorState;
254 export type CommandListener<P> = (payload: P, editor: LexicalEditor) => boolean;
256 export type EditableListener = (editable: boolean) => void;
258 export type CommandListenerPriority = 0 | 1 | 2 | 3 | 4;
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;
266 // eslint-disable-next-line @typescript-eslint/no-unused-vars
267 export type LexicalCommand<TPayload> = {
272 * Type helper for extracting the payload type from a command.
276 * const MY_COMMAND = createCommand<SomeType>();
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);
286 * function handleMyCommand(editor: LexicalEditor, payload: CommandPayloadType<typeof MY_COMMAND>) {
287 * // `payload` is of type `SomeType`, extracted from the command.
291 export type CommandPayloadType<TCommand extends LexicalCommand<unknown>> =
292 TCommand extends LexicalCommand<infer TPayload> ? TPayload : never;
295 LexicalCommand<unknown>,
296 Array<Set<CommandListener<unknown>>>
299 decorator: Set<DecoratorListener>;
300 mutation: MutationListeners;
301 editable: Set<EditableListener>;
302 root: Set<RootListener>;
303 textcontent: Set<TextContentListener>;
304 update: Set<UpdateListener>;
307 export type Listener =
312 | TextContentListener
315 export type ListenerType =
323 export type TransformerType = 'text' | 'decorator' | 'element' | 'root';
325 type IntentionallyMarkedAsDirtyElement = boolean;
327 type DOMConversionCache = Map<
329 Array<(node: Node) => DOMConversion | null>
332 export type SerializedEditor = {
333 editorState: SerializedEditorState;
336 export function resetEditor(
337 editor: LexicalEditor,
338 prevRootElement: null | HTMLElement,
339 nextRootElement: null | HTMLElement,
340 pendingEditorState: EditorState,
342 const keyNodeMap = editor._keyToDOMMap;
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;
356 const observer = editor._observer;
358 if (observer !== null) {
359 observer.disconnect();
360 editor._observer = null;
363 // Remove all the DOM nodes from the root element
364 if (prevRootElement !== null) {
365 prevRootElement.textContent = '';
368 if (nextRootElement !== null) {
369 nextRootElement.textContent = '';
370 keyNodeMap.set('root', nextRootElement);
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);
384 if (currentCache === undefined) {
386 conversionCache.set(key, currentCache);
389 currentCache.push(map[key]);
392 nodes.forEach((node) => {
393 const importDOM = node.klass.importDOM;
395 if (importDOM == null || handledConversions.has(importDOM)) {
399 handledConversions.add(importDOM);
400 const map = importDOM.call(node.klass);
403 addConversionsToCache(map);
406 if (additionalConversions) {
407 addConversionsToCache(additionalConversions);
409 return conversionCache;
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
419 export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor {
420 const config = editorConfig || {};
421 const activeEditor = internalGetActiveEditor();
422 const theme = config.theme || {};
424 editorConfig === undefined ? activeEditor : config.parentEditor || null;
425 const disableEvents = config.disableEvents || false;
426 const editorState = createEmptyEditorState();
429 (parentEditor !== null ? parentEditor._config.namespace : createUID());
430 const initialEditorState = config.editorState;
437 ArtificialNode__DO_NOT_USE,
438 ...(config.nodes || []),
440 const {onError, html} = config;
441 const isEditable = config.editable !== undefined ? config.editable : true;
442 let registeredNodes: Map<string, RegisteredNode>;
444 if (editorConfig === undefined && activeEditor !== null) {
445 registeredNodes = activeEditor._nodes;
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;
453 if (typeof klass !== 'function') {
454 const options = klass;
455 klass = options.replace;
456 replace = options.with;
457 replaceWithKlass = options.withKlass || null;
459 // Ensure custom nodes implement required methods and replaceWithKlass is instance of base klass.
461 // ArtificialNode__DO_NOT_USE can get renamed, so we use the type
463 Object.prototype.hasOwnProperty.call(klass, 'getType') &&
465 const name = klass.name;
467 if (replaceWithKlass) {
469 replaceWithKlass.prototype instanceof klass,
470 "%s doesn't extend the %s",
471 replaceWithKlass.name,
477 name !== 'RootNode' &&
478 nodeType !== 'root' &&
479 nodeType !== 'artificial'
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`);
489 // eslint-disable-next-line no-prototype-builtins
490 !klass.hasOwnProperty('importDOM') &&
491 // eslint-disable-next-line no-prototype-builtins
492 klass.hasOwnProperty('exportDOM')
495 `${name} should implement "importDOM" if using a custom "exportDOM" method to ensure HTML serialization (important for copy & paste) works as expected`,
498 if (proto instanceof DecoratorNode) {
499 // eslint-disable-next-line no-prototype-builtins
500 if (!proto.hasOwnProperty('decorate')) {
502 `${proto.constructor.name} must implement "decorate" method`,
507 // eslint-disable-next-line no-prototype-builtins
508 !klass.hasOwnProperty('importJSON')
511 `${name} should implement "importJSON" method to ensure JSON and default HTML serialization works as expected`,
515 // eslint-disable-next-line no-prototype-builtins
516 !proto.hasOwnProperty('exportJSON')
519 `${name} should implement "exportJSON" method to ensure JSON and default HTML serialization works as expected`,
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);
530 registeredNodes.set(type, {
531 exportDOM: html && html.export ? html.export.get(klass) : undefined,
539 const editor = new LexicalEditor(
548 onError ? onError : console.error,
549 initializeConversionCache(registeredNodes, html ? html.import : undefined),
553 if (initialEditorState !== undefined) {
554 editor._pendingEditorState = initialEditorState;
555 editor._dirtyType = FULL_RECONCILE;
560 export class LexicalEditor {
561 ['constructor']!: KlassConstructor<typeof LexicalEditor>;
563 /** The version with build identifiers for this editor (since 0.17.1) */
564 static version: string | undefined;
569 _parentEditor: null | LexicalEditor;
571 _rootElement: null | HTMLElement;
573 _editorState: EditorState;
575 _pendingEditorState: null | EditorState;
577 _compositionKey: null | NodeKey;
579 _deferred: Array<() => void>;
581 _keyToDOMMap: Map<NodeKey, HTMLElement>;
583 _updates: Array<[() => void, EditorUpdateOptions | undefined]>;
587 _listeners: Listeners;
591 _nodes: RegisteredNodes;
593 _decorators: Record<NodeKey, unknown>;
595 _pendingDecorators: null | Record<NodeKey, unknown>;
597 _config: EditorConfig;
599 _dirtyType: 0 | 1 | 2;
601 _cloneNotNeeded: Set<NodeKey>;
603 _dirtyLeaves: Set<NodeKey>;
605 _dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
607 _normalizedNodes: Set<NodeKey>;
609 _updateTags: Set<string>;
611 _observer: null | MutationObserver;
615 _onError: ErrorHandler;
617 _htmlConversions: DOMConversionCache;
619 _window: null | Window;
623 _blockCursorElement: null | HTMLDivElement;
627 editorState: EditorState,
628 parentEditor: null | LexicalEditor,
629 nodes: RegisteredNodes,
630 config: EditorConfig,
631 onError: ErrorHandler,
632 htmlConversions: DOMConversionCache,
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;
645 // Used during reconciliation
646 this._keyToDOMMap = new Map();
648 this._updating = false;
651 decorator: new Set(),
655 textcontent: new Set(),
659 this._commands = new Map();
660 // Editor configuration for theme/context.
661 this._config = config;
662 // Mapping of types to their 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();
679 this._onError = onError;
680 this._htmlConversions = htmlConversions;
681 this._editable = editable;
682 this._headless = parentEditor !== null && parentEditor._headless;
684 this._blockCursorElement = null;
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.
692 isComposing(): boolean {
693 return this._compositionKey != null;
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.
700 * @returns a teardown function that can be used to cleanup the listener.
702 registerUpdateListener(listener: UpdateListener): () => void {
703 const listenerSetOrMap = this._listeners.update;
704 listenerSetOrMap.add(listener);
706 listenerSetOrMap.delete(listener);
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.
714 * @returns a teardown function that can be used to cleanup the listener.
716 registerEditableListener(listener: EditableListener): () => void {
717 const listenerSetOrMap = this._listeners.editable;
718 listenerSetOrMap.add(listener);
720 listenerSetOrMap.delete(listener);
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.
727 * Will trigger the provided callback each time the editor transitions between these states until the
728 * teardown function is called.
730 * @returns a teardown function that can be used to cleanup the listener.
732 registerDecoratorListener<T>(listener: DecoratorListener<T>): () => void {
733 const listenerSetOrMap = this._listeners.decorator;
734 listenerSetOrMap.add(listener);
736 listenerSetOrMap.delete(listener);
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.
744 * Will trigger the provided callback each time the editor transitions between these states until the
745 * teardown function is called.
747 * @returns a teardown function that can be used to cleanup the listener.
749 registerTextContentListener(listener: TextContentListener): () => void {
750 const listenerSetOrMap = this._listeners.textcontent;
751 listenerSetOrMap.add(listener);
753 listenerSetOrMap.delete(listener);
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.
762 * Will trigger the provided callback each time the editor transitions between these states until the
763 * teardown function is called.
765 * @returns a teardown function that can be used to cleanup the listener.
767 registerRootListener(listener: RootListener): () => void {
768 const listenerSetOrMap = this._listeners.root;
769 listener(this._rootElement, null);
770 listenerSetOrMap.add(listener);
772 listener(null, this._rootElement);
773 listenerSetOrMap.delete(listener);
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.
781 * Listeners registered at the same priority level will run deterministically in the order of registration.
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.
789 command: LexicalCommand<P>,
790 listener: CommandListener<P>,
791 priority: CommandListenerPriority,
793 if (priority === undefined) {
794 invariant(false, 'Listener for type "command" requires a "priority".');
797 const commandsMap = this._commands;
799 if (!commandsMap.has(command)) {
800 commandsMap.set(command, [
809 const listenersInPriorityOrder = commandsMap.get(command);
811 if (listenersInPriorityOrder === undefined) {
814 'registerCommand: Command %s not found in command map',
819 const listeners = listenersInPriorityOrder[priority];
820 listeners.add(listener as CommandListener<unknown>);
822 listeners.delete(listener as CommandListener<unknown>);
825 listenersInPriorityOrder.every(
826 (listenersSet) => listenersSet.size === 0,
829 commandsMap.delete(command);
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.
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.
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).
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.
852 registerMutationListener(
853 klass: Klass<LexicalNode>,
854 listener: MutationListener,
855 options?: MutationListenerOptions,
857 const klassToMutate = this.resolveRegisteredNodeAfterReplacements(
858 this.getRegisteredNode(klass),
860 const mutations = this._listeners.mutation;
861 mutations.set(listener, klassToMutate);
862 const skipInitialization = options && options.skipInitialization;
864 !(skipInitialization === undefined
865 ? DEFAULT_SKIP_INITIALIZATION
866 : skipInitialization)
868 this.initializeMutationListener(listener, klassToMutate);
872 mutations.delete(listener);
877 private getRegisteredNode(klass: Klass<LexicalNode>): RegisteredNode {
878 const registeredNode = this._nodes.get(klass.getType());
880 if (registeredNode === undefined) {
883 'Node %s has not been registered. Ensure node has been passed to createEditor.',
888 return registeredNode;
892 private resolveRegisteredNodeAfterReplacements(
893 registeredNode: RegisteredNode,
895 while (registeredNode.replaceWithKlass) {
896 registeredNode = this.getRegisteredNode(registeredNode.replaceWithKlass);
898 return registeredNode;
902 private initializeMutationListener(
903 listener: MutationListener,
904 klass: Klass<LexicalNode>,
906 const prevEditorState = this._editorState;
907 const nodeMap = getCachedTypeToNodeMap(prevEditorState).get(
913 const nodeMutationMap = new Map<string, NodeMutation>();
914 for (const k of nodeMap.keys()) {
915 nodeMutationMap.set(k, 'created');
917 if (nodeMutationMap.size > 0) {
918 listener(nodeMutationMap, {
919 dirtyLeaves: new Set(),
921 updateTags: new Set(['registerMutationListener']),
927 private registerNodeTransformToKlass<T extends LexicalNode>(
929 listener: Transform<T>,
931 const registeredNode = this.getRegisteredNode(klass);
932 registeredNode.transforms.add(listener as Transform<LexicalNode>);
934 return registeredNode;
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!
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.
947 registerNodeTransform<T extends LexicalNode>(
949 listener: Transform<T>,
951 const registeredNode = this.registerNodeTransformToKlass(klass, listener);
952 const registeredNodes = [registeredNode];
954 const replaceWithKlass = registeredNode.replaceWithKlass;
955 if (replaceWithKlass != null) {
956 const registeredReplaceWithNode = this.registerNodeTransformToKlass(
958 listener as Transform<LexicalNode>,
960 registeredNodes.push(registeredReplaceWithNode);
963 markAllNodesAsDirty(this, klass.getType());
965 registeredNodes.forEach((node) =>
966 node.transforms.delete(listener as Transform<LexicalNode>),
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.
976 hasNode<T extends Klass<LexicalNode>>(node: T): boolean {
977 return this._nodes.has(node.getType());
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.
985 hasNodes<T extends Klass<LexicalNode>>(nodes: Array<T>): boolean {
986 return nodes.every(this.hasNode.bind(this));
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.
996 dispatchCommand<TCommand extends LexicalCommand<unknown>>(
998 payload: CommandPayloadType<TCommand>,
1000 return dispatchCommand(this, type, payload);
1004 * Gets a map of all decorators in the editor.
1005 * @returns A mapping of call decorator keys to their decorated content
1007 getDecorators<T>(): Record<NodeKey, T> {
1008 return this._decorators as Record<NodeKey, T>;
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.
1017 getRootElement(): null | HTMLElement {
1018 return this._rootElement;
1022 * Gets the key of the editor
1023 * @returns The editor key
1030 * Imperatively set the root contenteditable element that Lexical listens
1033 setRootElement(nextRootElement: null | HTMLElement): void {
1034 const prevRootElement = this._rootElement;
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);
1042 if (prevRootElement !== null) {
1043 // TODO: remove this flag once we no longer use UEv2 internally
1044 if (!this._config.disableEvents) {
1045 removeRootElementEvents(prevRootElement);
1047 if (classNames != null) {
1048 prevRootElement.classList.remove(...classNames);
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);
1063 this._updateTags.add('history-merge');
1065 $commitPendingUpdates(this);
1067 // TODO: remove this flag once we no longer use UEv2 internally
1068 if (!this._config.disableEvents) {
1069 addRootElementEvents(nextRootElement, this);
1071 if (classNames != null) {
1072 nextRootElement.classList.add(...classNames);
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;
1082 triggerListeners('root', this, false, nextRootElement, prevRootElement);
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.
1091 getElementByKey(key: NodeKey): HTMLElement | null {
1092 return this._keyToDOMMap.get(key) || null;
1096 * Gets the active editor state.
1097 * @returns The editor state
1099 getEditorState(): EditorState {
1100 return this._editorState;
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.
1108 setEditorState(editorState: EditorState, options?: EditorSetOptions): void {
1109 if (editorState.isEmpty()) {
1112 "setEditorState: the editor state is empty. Ensure the editor state's root node never becomes empty.",
1116 $flushRootMutations(this);
1117 const pendingEditorState = this._pendingEditorState;
1118 const tags = this._updateTags;
1119 const tag = options !== undefined ? options.tag : null;
1121 if (pendingEditorState !== null && !pendingEditorState.isEmpty()) {
1126 $commitPendingUpdates(this);
1129 this._pendingEditorState = editorState;
1130 this._dirtyType = FULL_RECONCILE;
1131 this._dirtyElements.set('root', false);
1132 this._compositionKey = null;
1138 $commitPendingUpdates(this);
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
1150 maybeStringifiedEditorState: string | SerializedEditorState,
1151 updateFn?: () => void,
1153 const serializedEditorState =
1154 typeof maybeStringifiedEditorState === 'string'
1155 ? JSON.parse(maybeStringifiedEditorState)
1156 : maybeStringifiedEditorState;
1157 return parseEditorState(serializedEditorState, this, updateFn);
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
1166 * @param callbackFn - A function that has access to read-only editor state.
1168 read<T>(callbackFn: () => T): T {
1169 $commitPendingUpdates(this);
1170 return this.getEditorState().read(callbackFn, {editor: this});
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.
1187 update(updateFn: () => void, options?: EditorUpdateOptions): void {
1188 updateEditor(this, updateFn, options);
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.
1198 focus(callbackFn?: () => void, options: EditorFocusOptions = {}): void {
1199 const rootElement = this._rootElement;
1201 if (rootElement !== null) {
1202 // This ensures that iOS does not trigger caps lock upon focus
1203 rootElement.setAttribute('autocapitalize', 'off');
1207 const selection = $getSelection();
1208 const root = $getRoot();
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') {
1223 rootElement.removeAttribute('autocapitalize');
1231 // In the case where onUpdate doesn't fire (due to the focus update not
1233 if (this._pendingEditorState === null) {
1234 rootElement.removeAttribute('autocapitalize');
1240 * Commits any currently pending updates scheduled for the editor.
1242 commitUpdates(): void {
1243 $commitPendingUpdates(this);
1247 * Removes focus from the editor.
1250 const rootElement = this._rootElement;
1252 if (rootElement !== null) {
1256 const domSelection = getDOMSelection(this._window);
1258 if (domSelection !== null) {
1259 domSelection.removeAllRanges();
1263 * Returns true if the editor is editable, false otherwise.
1264 * @returns True if the editor is editable, false otherwise.
1266 isEditable(): boolean {
1267 return this._editable;
1270 * Sets the editable property of the editor. When false, the
1271 * editor will not listen for user events on the underling contenteditable.
1272 * @param editable - the value to set the editable mode to.
1274 setEditable(editable: boolean): void {
1275 if (this._editable !== editable) {
1276 this._editable = editable;
1277 triggerListeners('editable', this, true, editable);
1281 * Returns a JSON-serializable javascript object NOT a JSON string.
1282 * You still must call JSON.stringify (or something else) to turn the
1283 * state into a string you can transfer over the wire and store in a database.
1285 * See {@link LexicalNode.exportJSON}
1287 * @returns A JSON-serializable javascript object
1289 toJSON(): SerializedEditor {
1291 editorState: this._editorState.toJSON(),
1296 LexicalEditor.version = '0.17.1';