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 /* eslint-disable no-constant-condition */
10 import type {EditorConfig, LexicalEditor} from './LexicalEditor';
11 import type {BaseSelection, RangeSelection} from './LexicalSelection';
12 import type {Klass, KlassConstructor} from 'lexical';
14 import invariant from 'lexical/shared/invariant';
29 $moveSelectionPointToEnd,
30 $updateElementSelectionOnCreateDeleteNode,
31 moveSelectionPointToSibling,
32 } from './LexicalSelection';
37 } from './LexicalUpdates';
43 $maybeMoveChildrenSelectionToParent,
47 errorOnInsertTextNodeOnRoot,
48 internalMarkNodeAsDirty,
50 } from './LexicalUtils';
52 export type NodeMap = Map<NodeKey, LexicalNode>;
54 export type SerializedLexicalNode = {
59 export function $removeNode(
60 nodeToRemove: LexicalNode,
61 restoreSelection: boolean,
62 preserveEmptyParent?: boolean,
65 const key = nodeToRemove.__key;
66 const parent = nodeToRemove.getParent();
67 if (parent === null) {
70 const selection = $maybeMoveChildrenSelectionToParent(nodeToRemove);
71 let selectionMoved = false;
72 if ($isRangeSelection(selection) && restoreSelection) {
73 const anchor = selection.anchor;
74 const focus = selection.focus;
75 if (anchor.key === key) {
76 moveSelectionPointToSibling(
80 nodeToRemove.getPreviousSibling(),
81 nodeToRemove.getNextSibling(),
83 selectionMoved = true;
85 if (focus.key === key) {
86 moveSelectionPointToSibling(
90 nodeToRemove.getPreviousSibling(),
91 nodeToRemove.getNextSibling(),
93 selectionMoved = true;
96 $isNodeSelection(selection) &&
98 nodeToRemove.isSelected()
100 nodeToRemove.selectPrevious();
103 if ($isRangeSelection(selection) && restoreSelection && !selectionMoved) {
104 // Doing this is O(n) so lets avoid it unless we need to do it
105 const index = nodeToRemove.getIndexWithinParent();
106 removeFromParent(nodeToRemove);
107 $updateElementSelectionOnCreateDeleteNode(selection, parent, index, -1);
109 removeFromParent(nodeToRemove);
113 !preserveEmptyParent &&
114 !$isRootOrShadowRoot(parent) &&
115 !parent.canBeEmpty() &&
118 $removeNode(parent, restoreSelection);
120 if (restoreSelection && $isRootNode(parent) && parent.isEmpty()) {
125 export type DOMConversion<T extends HTMLElement = HTMLElement> = {
126 conversion: DOMConversionFn<T>;
127 priority?: 0 | 1 | 2 | 3 | 4;
130 export type DOMConversionFn<T extends HTMLElement = HTMLElement> = (
132 ) => DOMConversionOutput | null;
134 export type DOMChildConversion = (
135 lexicalNode: LexicalNode,
136 parentLexicalNode: LexicalNode | null | undefined,
137 ) => LexicalNode | null | undefined;
139 export type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<
141 (node: T) => DOMConversion<T> | null
143 type NodeName = string;
145 export type DOMConversionOutput = {
146 after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
147 forChild?: DOMChildConversion;
148 node: null | LexicalNode | Array<LexicalNode>;
151 export type DOMExportOutputMap = Map<
153 (editor: LexicalEditor, target: LexicalNode) => DOMExportOutput
156 export type DOMExportOutput = {
158 generatedElement: HTMLElement | Text | null | undefined,
159 ) => HTMLElement | Text | null | undefined;
160 element: HTMLElement | Text | null;
163 export type NodeKey = string;
165 export class LexicalNode {
166 // Allow us to look up the type including static props
167 ['constructor']!: KlassConstructor<typeof LexicalNode>;
171 //@ts-ignore We set the key in the constructor.
174 __parent: null | NodeKey;
176 __prev: null | NodeKey;
178 __next: null | NodeKey;
180 // Flow doesn't support abstract classes unfortunately, so we can't _force_
181 // subclasses of Node to implement statics. All subclasses of Node should have
182 // a static getType and clone method though. We define getType and clone here so we can call it
183 // on any Node, and we throw this error by default since the subclass should provide
184 // their own implementation.
186 * Returns the string type of this node. Every node must
187 * implement this and it MUST BE UNIQUE amongst nodes registered
191 static getType(): string {
194 'LexicalNode: Node %s does not implement .getType().',
200 * Clones this node, creating a new node with a different key
201 * and adding it to the EditorState (but not attaching it anywhere!). All nodes must
202 * implement this method.
205 static clone(_data: unknown): LexicalNode {
208 'LexicalNode: Node %s does not implement .clone().',
214 * Perform any state updates on the clone of prevNode that are not already
215 * handled by the constructor call in the static clone method. If you have
216 * state to update in your clone that is not handled directly by the
217 * constructor, it is advisable to override this method but it is required
218 * to include a call to `super.afterCloneFrom(prevNode)` in your
219 * implementation. This is only intended to be called by
220 * {@link $cloneWithProperties} function or via a super call.
224 * class ClassesTextNode extends TextNode {
225 * // Not shown: static getType, static importJSON, exportJSON, createDOM, updateDOM
226 * __classes = new Set<string>();
227 * static clone(node: ClassesTextNode): ClassesTextNode {
228 * // The inherited TextNode constructor is used here, so
229 * // classes is not set by this method.
230 * return new ClassesTextNode(node.__text, node.__key);
232 * afterCloneFrom(node: this): void {
233 * // This calls TextNode.afterCloneFrom and LexicalNode.afterCloneFrom
234 * // for necessary state updates
235 * super.afterCloneFrom(node);
236 * this.__addClasses(node.__classes);
238 * // This method is a private implementation detail, it is not
239 * // suitable for the public API because it does not call getWritable
240 * __addClasses(classNames: Iterable<string>): this {
241 * for (const className of classNames) {
242 * this.__classes.add(className);
246 * addClass(...classNames: string[]): this {
247 * return this.getWritable().__addClasses(classNames);
249 * removeClass(...classNames: string[]): this {
250 * const node = this.getWritable();
251 * for (const className of classNames) {
252 * this.__classes.delete(className);
256 * getClasses(): Set<string> {
257 * return this.getLatest().__classes;
263 afterCloneFrom(prevNode: this) {
264 this.__parent = prevNode.__parent;
265 this.__next = prevNode.__next;
266 this.__prev = prevNode.__prev;
269 // eslint-disable-next-line @typescript-eslint/no-explicit-any
270 static importDOM?: () => DOMConversionMap<any> | null;
272 constructor(key?: NodeKey) {
273 this.__type = this.constructor.getType();
274 this.__parent = null;
277 $setNodeKey(this, key);
280 if (this.__type !== 'root') {
282 errorOnTypeKlassMismatch(this.__type, this.constructor);
286 // Getters and Traversers
289 * Returns the string type of this node.
295 isInline(): boolean {
298 'LexicalNode: Node %s does not implement .isInline().',
299 this.constructor.name,
304 * Returns true if there is a path between this node and the RootNode, false otherwise.
305 * This is a way of determining if the node is "attached" EditorState. Unattached nodes
306 * won't be reconciled and will ultimatelt be cleaned up by the Lexical GC.
308 isAttached(): boolean {
309 let nodeKey: string | null = this.__key;
310 while (nodeKey !== null) {
311 if (nodeKey === 'root') {
315 const node: LexicalNode | null = $getNodeByKey(nodeKey);
320 nodeKey = node.__parent;
326 * Returns true if this node is contained within the provided Selection., false otherwise.
327 * Relies on the algorithms implemented in {@link BaseSelection.getNodes} to determine
330 * @param selection - The selection that we want to determine if the node is in.
332 isSelected(selection?: null | BaseSelection): boolean {
333 const targetSelection = selection || $getSelection();
334 if (targetSelection == null) {
338 const isSelected = targetSelection
340 .some((n) => n.__key === this.__key);
342 if ($isTextNode(this)) {
345 // For inline images inside of element nodes.
346 // Without this change the image will be selected if the cursor is before or after it.
347 const isElementRangeSelection =
348 $isRangeSelection(targetSelection) &&
349 targetSelection.anchor.type === 'element' &&
350 targetSelection.focus.type === 'element';
352 if (isElementRangeSelection) {
353 if (targetSelection.isCollapsed()) {
357 const parentNode = this.getParent();
358 if ($isDecoratorNode(this) && this.isInline() && parentNode) {
359 const firstPoint = targetSelection.isBackward()
360 ? targetSelection.focus
361 : targetSelection.anchor;
362 const firstElement = firstPoint.getNode() as ElementNode;
364 firstPoint.offset === firstElement.getChildrenSize() &&
365 firstElement.is(parentNode) &&
366 firstElement.getLastChildOrThrow().is(this)
376 * Returns this nodes key.
379 // Key is stable between copies
384 * Returns the zero-based index of this node within the parent.
386 getIndexWithinParent(): number {
387 const parent = this.getParent();
388 if (parent === null) {
391 let node = parent.getFirstChild();
393 while (node !== null) {
398 node = node.getNextSibling();
404 * Returns the parent of this node, or null if none is found.
406 getParent<T extends ElementNode>(): T | null {
407 const parent = this.getLatest().__parent;
408 if (parent === null) {
411 return $getNodeByKey<T>(parent);
415 * Returns the parent of this node, or throws if none is found.
417 getParentOrThrow<T extends ElementNode>(): T {
418 const parent = this.getParent<T>();
419 if (parent === null) {
420 invariant(false, 'Expected node %s to have a parent.', this.__key);
426 * Returns the highest (in the EditorState tree)
427 * non-root ancestor of this node, or null if none is found. See {@link lexical!$isRootOrShadowRoot}
428 * for more information on which Elements comprise "roots".
430 getTopLevelElement(): ElementNode | DecoratorNode<unknown> | null {
431 let node: ElementNode | this | null = this;
432 while (node !== null) {
433 const parent: ElementNode | null = node.getParent();
434 if ($isRootOrShadowRoot(parent)) {
436 $isElementNode(node) || (node === this && $isDecoratorNode(node)),
437 'Children of root nodes must be elements or decorators',
447 * Returns the highest (in the EditorState tree)
448 * non-root ancestor of this node, or throws if none is found. See {@link lexical!$isRootOrShadowRoot}
449 * for more information on which Elements comprise "roots".
451 getTopLevelElementOrThrow(): ElementNode | DecoratorNode<unknown> {
452 const parent = this.getTopLevelElement();
453 if (parent === null) {
456 'Expected node %s to have a top parent element.',
464 * Returns a list of the every ancestor of this node,
465 * all the way up to the RootNode.
468 getParents(): Array<ElementNode> {
469 const parents: Array<ElementNode> = [];
470 let node = this.getParent();
471 while (node !== null) {
473 node = node.getParent();
479 * Returns a list of the keys of every ancestor of this node,
480 * all the way up to the RootNode.
483 getParentKeys(): Array<NodeKey> {
485 let node = this.getParent();
486 while (node !== null) {
487 parents.push(node.__key);
488 node = node.getParent();
494 * Returns the "previous" siblings - that is, the node that comes
495 * before this one in the same parent.
498 getPreviousSibling<T extends LexicalNode>(): T | null {
499 const self = this.getLatest();
500 const prevKey = self.__prev;
501 return prevKey === null ? null : $getNodeByKey<T>(prevKey);
505 * Returns the "previous" siblings - that is, the nodes that come between
506 * this one and the first child of it's parent, inclusive.
509 getPreviousSiblings<T extends LexicalNode>(): Array<T> {
510 const siblings: Array<T> = [];
511 const parent = this.getParent();
512 if (parent === null) {
515 let node: null | T = parent.getFirstChild();
516 while (node !== null) {
521 node = node.getNextSibling();
527 * Returns the "next" siblings - that is, the node that comes
528 * after this one in the same parent
531 getNextSibling<T extends LexicalNode>(): T | null {
532 const self = this.getLatest();
533 const nextKey = self.__next;
534 return nextKey === null ? null : $getNodeByKey<T>(nextKey);
538 * Returns all "next" siblings - that is, the nodes that come between this
539 * one and the last child of it's parent, inclusive.
542 getNextSiblings<T extends LexicalNode>(): Array<T> {
543 const siblings: Array<T> = [];
544 let node: null | T = this.getNextSibling();
545 while (node !== null) {
547 node = node.getNextSibling();
553 * Returns the closest common ancestor of this node and the provided one or null
554 * if one cannot be found.
556 * @param node - the other node to find the common ancestor of.
558 getCommonAncestor<T extends ElementNode = ElementNode>(
561 const a = this.getParents();
562 const b = node.getParents();
563 if ($isElementNode(this)) {
566 if ($isElementNode(node)) {
569 const aLength = a.length;
570 const bLength = b.length;
571 if (aLength === 0 || bLength === 0 || a[aLength - 1] !== b[bLength - 1]) {
574 const bSet = new Set(b);
575 for (let i = 0; i < aLength; i++) {
576 const ancestor = a[i] as T;
577 if (bSet.has(ancestor)) {
585 * Returns true if the provided node is the exact same one as this node, from Lexical's perspective.
586 * Always use this instead of referential equality.
588 * @param object - the node to perform the equality comparison on.
590 is(object: LexicalNode | null | undefined): boolean {
591 if (object == null) {
594 return this.__key === object.__key;
598 * Returns true if this node logical precedes the target node in the editor state.
600 * @param targetNode - the node we're testing to see if it's after this one.
602 isBefore(targetNode: LexicalNode): boolean {
603 if (this === targetNode) {
606 if (targetNode.isParentOf(this)) {
609 if (this.isParentOf(targetNode)) {
612 const commonAncestor = this.getCommonAncestor(targetNode);
615 let node: this | ElementNode | LexicalNode = this;
617 const parent: ElementNode = node.getParentOrThrow();
618 if (parent === commonAncestor) {
619 indexA = node.getIndexWithinParent();
626 const parent: ElementNode = node.getParentOrThrow();
627 if (parent === commonAncestor) {
628 indexB = node.getIndexWithinParent();
633 return indexA < indexB;
637 * Returns true if this node is the parent of the target node, false otherwise.
639 * @param targetNode - the would-be child node.
641 isParentOf(targetNode: LexicalNode): boolean {
642 const key = this.__key;
643 if (key === targetNode.__key) {
646 let node: ElementNode | LexicalNode | null = targetNode;
647 while (node !== null) {
648 if (node.__key === key) {
651 node = node.getParent();
656 // TO-DO: this function can be simplified a lot
658 * Returns a list of nodes that are between this node and
659 * the target node in the EditorState.
661 * @param targetNode - the node that marks the other end of the range of nodes to be returned.
663 getNodesBetween(targetNode: LexicalNode): Array<LexicalNode> {
664 const isBefore = this.isBefore(targetNode);
666 const visited = new Set();
667 let node: LexicalNode | this | null = this;
672 const key = node.__key;
673 if (!visited.has(key)) {
677 if (node === targetNode) {
680 const child: LexicalNode | null = $isElementNode(node)
682 ? node.getFirstChild()
683 : node.getLastChild()
685 if (child !== null) {
689 const nextSibling: LexicalNode | null = isBefore
690 ? node.getNextSibling()
691 : node.getPreviousSibling();
692 if (nextSibling !== null) {
696 const parent: LexicalNode | null = node.getParentOrThrow();
697 if (!visited.has(parent.__key)) {
700 if (parent === targetNode) {
703 let parentSibling = null;
704 let ancestor: LexicalNode | null = parent;
706 if (ancestor === null) {
707 invariant(false, 'getNodesBetween: ancestor is null');
709 parentSibling = isBefore
710 ? ancestor.getNextSibling()
711 : ancestor.getPreviousSibling();
712 ancestor = ancestor.getParent();
713 if (ancestor !== null) {
714 if (parentSibling === null && !visited.has(ancestor.__key)) {
715 nodes.push(ancestor);
720 } while (parentSibling === null);
721 node = parentSibling;
730 * Returns true if this node has been marked dirty during this update cycle.
734 const editor = getActiveEditor();
735 const dirtyLeaves = editor._dirtyLeaves;
736 return dirtyLeaves !== null && dirtyLeaves.has(this.__key);
740 * Returns the latest version of the node from the active EditorState.
741 * This is used to avoid getting values from stale node references.
745 const latest = $getNodeByKey<this>(this.__key);
746 if (latest === null) {
749 'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.',
756 * Returns a mutable version of the node using {@link $cloneWithProperties}
757 * if necessary. Will throw an error if called outside of a Lexical Editor
758 * {@link LexicalEditor.update} callback.
761 getWritable(): this {
763 const editorState = getActiveEditorState();
764 const editor = getActiveEditor();
765 const nodeMap = editorState._nodeMap;
766 const key = this.__key;
767 // Ensure we get the latest node from pending state
768 const latestNode = this.getLatest();
769 const cloneNotNeeded = editor._cloneNotNeeded;
770 const selection = $getSelection();
771 if (selection !== null) {
772 selection.setCachedNodes(null);
774 if (cloneNotNeeded.has(key)) {
775 // Transforms clear the dirty node set on each iteration to keep track on newly dirty nodes
776 internalMarkNodeAsDirty(latestNode);
779 const mutableNode = $cloneWithProperties(latestNode);
780 cloneNotNeeded.add(key);
781 internalMarkNodeAsDirty(mutableNode);
782 // Update reference in node map
783 nodeMap.set(key, mutableNode);
789 * Returns the text content of the node. Override this for
790 * custom nodes that should have a representation in plain text
791 * format (for copy + paste, for example)
794 getTextContent(): string {
799 * Returns the length of the string produced by calling getTextContent on this node.
802 getTextContentSize(): number {
803 return this.getTextContent().length;
809 * Called during the reconciliation process to determine which nodes
810 * to insert into the DOM for this Lexical Node.
812 * This method must return exactly one HTMLElement. Nested elements are not supported.
814 * Do not attempt to update the Lexical EditorState during this phase of the update lifecyle.
816 * @param _config - allows access to things like the EditorTheme (to apply classes) during reconciliation.
817 * @param _editor - allows access to the editor for context during reconciliation.
820 createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
821 invariant(false, 'createDOM: base method not extended');
825 * Called when a node changes and should update the DOM
826 * in whatever way is necessary to make it align with any changes that might
827 * have happened during the update.
829 * Returning "true" here will cause lexical to unmount and recreate the DOM node
830 * (by calling createDOM). You would need to do this if the element tag changes,
837 _config: EditorConfig,
839 invariant(false, 'updateDOM: base method not extended');
843 * Controls how the this node is serialized to HTML. This is important for
844 * copy and paste between Lexical and non-Lexical editors, or Lexical editors with different namespaces,
845 * in which case the primary transfer format is HTML. It's also important if you're serializing
846 * to HTML for any other reason via {@link @lexical/html!$generateHtmlFromNodes}. You could
847 * also use this method to build your own HTML renderer.
850 exportDOM(editor: LexicalEditor): DOMExportOutput {
851 const element = this.createDOM(editor._config, editor);
856 * Controls how the this node is serialized to JSON. This is important for
857 * copy and paste between Lexical editors sharing the same namespace. It's also important
858 * if you're serializing to JSON for persistent storage somewhere.
859 * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).
862 exportJSON(): SerializedLexicalNode {
863 invariant(false, 'exportJSON: base method not extended');
867 * Controls how the this node is deserialized from JSON. This is usually boilerplate,
868 * but provides an abstraction between the node implementation and serialized interface that can
869 * be important if you ever make breaking changes to a node schema (by adding or removing properties).
870 * See [Serialization & Deserialization](https://lexical.dev/docs/concepts/serialization#lexical---html).
873 static importJSON(_serializedNode: SerializedLexicalNode): LexicalNode {
876 'LexicalNode: Node %s does not implement .importJSON().',
883 * Registers the returned function as a transform on the node during
884 * Editor initialization. Most such use cases should be addressed via
885 * the {@link LexicalEditor.registerNodeTransform} API.
887 * Experimental - use at your own risk.
889 static transform(): ((node: LexicalNode) => void) | null {
893 // Setters and mutators
896 * Removes this LexicalNode from the EditorState. If the node isn't re-inserted
897 * somewhere, the Lexical garbage collector will eventually clean it up.
899 * @param preserveEmptyParent - If falsy, the node's parent will be removed if
900 * it's empty after the removal operation. This is the default behavior, subject to
901 * other node heuristics such as {@link ElementNode#canBeEmpty}
903 remove(preserveEmptyParent?: boolean): void {
904 $removeNode(this, true, preserveEmptyParent);
908 * Replaces this LexicalNode with the provided node, optionally transferring the children
909 * of the replaced node to the replacing node.
911 * @param replaceWith - The node to replace this one with.
912 * @param includeChildren - Whether or not to transfer the children of this node to the replacing node.
914 replace<N extends LexicalNode>(replaceWith: N, includeChildren?: boolean): N {
916 let selection = $getSelection();
917 if (selection !== null) {
918 selection = selection.clone();
920 errorOnInsertTextNodeOnRoot(this, replaceWith);
921 const self = this.getLatest();
922 const toReplaceKey = this.__key;
923 const key = replaceWith.__key;
924 const writableReplaceWith = replaceWith.getWritable();
925 const writableParent = this.getParentOrThrow().getWritable();
926 const size = writableParent.__size;
927 removeFromParent(writableReplaceWith);
928 const prevSibling = self.getPreviousSibling();
929 const nextSibling = self.getNextSibling();
930 const prevKey = self.__prev;
931 const nextKey = self.__next;
932 const parentKey = self.__parent;
933 $removeNode(self, false, true);
935 if (prevSibling === null) {
936 writableParent.__first = key;
938 const writablePrevSibling = prevSibling.getWritable();
939 writablePrevSibling.__next = key;
941 writableReplaceWith.__prev = prevKey;
942 if (nextSibling === null) {
943 writableParent.__last = key;
945 const writableNextSibling = nextSibling.getWritable();
946 writableNextSibling.__prev = key;
948 writableReplaceWith.__next = nextKey;
949 writableReplaceWith.__parent = parentKey;
950 writableParent.__size = size;
951 if (includeChildren) {
953 $isElementNode(this) && $isElementNode(writableReplaceWith),
954 'includeChildren should only be true for ElementNodes',
956 this.getChildren().forEach((child: LexicalNode) => {
957 writableReplaceWith.append(child);
960 if ($isRangeSelection(selection)) {
961 $setSelection(selection);
962 const anchor = selection.anchor;
963 const focus = selection.focus;
964 if (anchor.key === toReplaceKey) {
965 $moveSelectionPointToEnd(anchor, writableReplaceWith);
967 if (focus.key === toReplaceKey) {
968 $moveSelectionPointToEnd(focus, writableReplaceWith);
971 if ($getCompositionKey() === toReplaceKey) {
972 $setCompositionKey(key);
974 return writableReplaceWith;
978 * Inserts a node after this LexicalNode (as the next sibling).
980 * @param nodeToInsert - The node to insert after this one.
981 * @param restoreSelection - Whether or not to attempt to resolve the
982 * selection to the appropriate place after the operation is complete.
984 insertAfter(nodeToInsert: LexicalNode, restoreSelection = true): LexicalNode {
986 errorOnInsertTextNodeOnRoot(this, nodeToInsert);
987 const writableSelf = this.getWritable();
988 const writableNodeToInsert = nodeToInsert.getWritable();
989 const oldParent = writableNodeToInsert.getParent();
990 const selection = $getSelection();
991 let elementAnchorSelectionOnNode = false;
992 let elementFocusSelectionOnNode = false;
993 if (oldParent !== null) {
994 // TODO: this is O(n), can we improve?
995 const oldIndex = nodeToInsert.getIndexWithinParent();
996 removeFromParent(writableNodeToInsert);
997 if ($isRangeSelection(selection)) {
998 const oldParentKey = oldParent.__key;
999 const anchor = selection.anchor;
1000 const focus = selection.focus;
1001 elementAnchorSelectionOnNode =
1002 anchor.type === 'element' &&
1003 anchor.key === oldParentKey &&
1004 anchor.offset === oldIndex + 1;
1005 elementFocusSelectionOnNode =
1006 focus.type === 'element' &&
1007 focus.key === oldParentKey &&
1008 focus.offset === oldIndex + 1;
1011 const nextSibling = this.getNextSibling();
1012 const writableParent = this.getParentOrThrow().getWritable();
1013 const insertKey = writableNodeToInsert.__key;
1014 const nextKey = writableSelf.__next;
1015 if (nextSibling === null) {
1016 writableParent.__last = insertKey;
1018 const writableNextSibling = nextSibling.getWritable();
1019 writableNextSibling.__prev = insertKey;
1021 writableParent.__size++;
1022 writableSelf.__next = insertKey;
1023 writableNodeToInsert.__next = nextKey;
1024 writableNodeToInsert.__prev = writableSelf.__key;
1025 writableNodeToInsert.__parent = writableSelf.__parent;
1026 if (restoreSelection && $isRangeSelection(selection)) {
1027 const index = this.getIndexWithinParent();
1028 $updateElementSelectionOnCreateDeleteNode(
1033 const writableParentKey = writableParent.__key;
1034 if (elementAnchorSelectionOnNode) {
1035 selection.anchor.set(writableParentKey, index + 2, 'element');
1037 if (elementFocusSelectionOnNode) {
1038 selection.focus.set(writableParentKey, index + 2, 'element');
1041 return nodeToInsert;
1045 * Inserts a node before this LexicalNode (as the previous sibling).
1047 * @param nodeToInsert - The node to insert before this one.
1048 * @param restoreSelection - Whether or not to attempt to resolve the
1049 * selection to the appropriate place after the operation is complete.
1052 nodeToInsert: LexicalNode,
1053 restoreSelection = true,
1056 errorOnInsertTextNodeOnRoot(this, nodeToInsert);
1057 const writableSelf = this.getWritable();
1058 const writableNodeToInsert = nodeToInsert.getWritable();
1059 const insertKey = writableNodeToInsert.__key;
1060 removeFromParent(writableNodeToInsert);
1061 const prevSibling = this.getPreviousSibling();
1062 const writableParent = this.getParentOrThrow().getWritable();
1063 const prevKey = writableSelf.__prev;
1064 // TODO: this is O(n), can we improve?
1065 const index = this.getIndexWithinParent();
1066 if (prevSibling === null) {
1067 writableParent.__first = insertKey;
1069 const writablePrevSibling = prevSibling.getWritable();
1070 writablePrevSibling.__next = insertKey;
1072 writableParent.__size++;
1073 writableSelf.__prev = insertKey;
1074 writableNodeToInsert.__prev = prevKey;
1075 writableNodeToInsert.__next = writableSelf.__key;
1076 writableNodeToInsert.__parent = writableSelf.__parent;
1077 const selection = $getSelection();
1078 if (restoreSelection && $isRangeSelection(selection)) {
1079 const parent = this.getParentOrThrow();
1080 $updateElementSelectionOnCreateDeleteNode(selection, parent, index);
1082 return nodeToInsert;
1086 * Whether or not this node has a required parent. Used during copy + paste operations
1087 * to normalize nodes that would otherwise be orphaned. For example, ListItemNodes without
1088 * a ListNode parent or TextNodes with a ParagraphNode parent.
1091 isParentRequired(): boolean {
1096 * The creation logic for any required parent. Should be implemented if {@link isParentRequired} returns true.
1099 createParentElementNode(): ElementNode {
1100 return $createParagraphNode();
1103 selectStart(): RangeSelection {
1104 return this.selectPrevious();
1107 selectEnd(): RangeSelection {
1108 return this.selectNext(0, 0);
1112 * Moves selection to the previous sibling of this node, at the specified offsets.
1114 * @param anchorOffset - The anchor offset for selection.
1115 * @param focusOffset - The focus offset for selection
1117 selectPrevious(anchorOffset?: number, focusOffset?: number): RangeSelection {
1119 const prevSibling = this.getPreviousSibling();
1120 const parent = this.getParentOrThrow();
1121 if (prevSibling === null) {
1122 return parent.select(0, 0);
1124 if ($isElementNode(prevSibling)) {
1125 return prevSibling.select();
1126 } else if (!$isTextNode(prevSibling)) {
1127 const index = prevSibling.getIndexWithinParent() + 1;
1128 return parent.select(index, index);
1130 return prevSibling.select(anchorOffset, focusOffset);
1134 * Moves selection to the next sibling of this node, at the specified offsets.
1136 * @param anchorOffset - The anchor offset for selection.
1137 * @param focusOffset - The focus offset for selection
1139 selectNext(anchorOffset?: number, focusOffset?: number): RangeSelection {
1141 const nextSibling = this.getNextSibling();
1142 const parent = this.getParentOrThrow();
1143 if (nextSibling === null) {
1144 return parent.select();
1146 if ($isElementNode(nextSibling)) {
1147 return nextSibling.select(0, 0);
1148 } else if (!$isTextNode(nextSibling)) {
1149 const index = nextSibling.getIndexWithinParent();
1150 return parent.select(index, index);
1152 return nextSibling.select(anchorOffset, focusOffset);
1156 * Marks a node dirty, triggering transforms and
1157 * forcing it to be reconciled during the update cycle.
1165 function errorOnTypeKlassMismatch(
1167 klass: Klass<LexicalNode>,
1169 const registeredNode = getActiveEditor()._nodes.get(type);
1170 // Common error - split in its own invariant
1171 if (registeredNode === undefined) {
1174 'Create node: Attempted to create node %s that was not configured to be used on the editor.',
1178 const editorKlass = registeredNode.klass;
1179 if (editorKlass !== klass) {
1182 'Create node: Type %s in node %s does not match registered node %s with the same type',
1191 * Insert a series of nodes after this LexicalNode (as next siblings)
1193 * @param firstToInsert - The first node to insert after this one.
1194 * @param lastToInsert - The last node to insert after this one. Must be a
1195 * later sibling of FirstNode. If not provided, it will be its last sibling.
1197 export function insertRangeAfter(
1199 firstToInsert: LexicalNode,
1200 lastToInsert?: LexicalNode,
1202 const lastToInsert2 =
1203 lastToInsert || firstToInsert.getParentOrThrow().getLastChild()!;
1204 let current = firstToInsert;
1205 const nodesToInsert = [firstToInsert];
1206 while (current !== lastToInsert2) {
1207 if (!current.getNextSibling()) {
1210 'insertRangeAfter: lastToInsert must be a later sibling of firstToInsert',
1213 current = current.getNextSibling()!;
1214 nodesToInsert.push(current);
1217 let currentNode: LexicalNode = node;
1218 for (const nodeToInsert of nodesToInsert) {
1219 currentNode = currentNode.insertAfter(nodeToInsert);