This package provides essential utility functions for the Lexical rich text editor framework, offering a comprehensive set of DOM manipulation helpers, tree traversal algorithms, and editor state management tools.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Advanced editor state manipulation, node insertion, and state restoration utilities for complex editor operations. These functions provide sophisticated tools for managing the Lexical editor's state, handling nested elements, and performing complex node manipulations.
Inserts a node at the nearest root position with automatic paragraph wrapping and intelligent selection management.
/**
* If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
* the node will be appended there, otherwise, it will be inserted before the insertion area.
* If there is no selection where the node is to be inserted, it will be appended after any current nodes
* within the tree, as a child of the root node. A paragraph will then be added after the inserted node and selected.
* @param node - The node to be inserted
* @returns The node after its insertion
*/
function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T;Usage Examples:
import { $insertNodeToNearestRoot } from "@lexical/utils";
import { $createHeadingNode, $createImageNode, $createQuoteNode } from "lexical";
// Insert heading at current selection or root
const headingNode = $createHeadingNode('h1');
headingNode.append($createTextNode('New Heading'));
$insertNodeToNearestRoot(headingNode);
// Insert image node
const imageNode = $createImageNode({
src: 'image.jpg',
alt: 'Description'
});
$insertNodeToNearestRoot(imageNode);
// Insert quote block
const quoteNode = $createQuoteNode();
quoteNode.append($createTextNode('Quoted text here'));
$insertNodeToNearestRoot(quoteNode);
// Chain multiple insertions
editor.update(() => {
const title = $createHeadingNode('h1');
title.append($createTextNode('Document Title'));
$insertNodeToNearestRoot(title);
const content = $createParagraphNode();
content.append($createTextNode('Document content starts here.'));
$insertNodeToNearestRoot(content);
});More precise node insertion with caret-based positioning and splitting options.
/**
* If the insertion caret is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
* the node will be inserted there, otherwise the parent nodes will be split according to the
* given options.
* @param node - The node to be inserted
* @param caret - The location to insert or split from
* @param options - Options for splitting behavior
* @returns The node after its insertion
*/
function $insertNodeToNearestRootAtCaret<
T extends LexicalNode,
D extends CaretDirection
>(
node: T,
caret: PointCaret<D>,
options?: SplitAtPointCaretNextOptions
): NodeCaret<D>;Inserts a node as the first child of a parent element.
/**
* Appends the node before the first child of the parent node
* @param parent - A parent node
* @param node - Node that needs to be appended
*/
function $insertFirst(parent: ElementNode, node: LexicalNode): void;Usage Examples:
import { $insertFirst } from "@lexical/utils";
const listNode = $createListNode('bullet');
const firstItem = $createListItemNode();
firstItem.append($createTextNode('First item'));
// Insert as first child
$insertFirst(listNode, firstItem);
// Insert multiple nodes in order
const items = ['First', 'Second', 'Third'];
items.reverse().forEach(text => {
const item = $createListItemNode();
item.append($createTextNode(text));
$insertFirst(listNode, item);
});Wraps a node within a newly created element node.
/**
* Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
* @param node - Node to be wrapped.
* @param createElementNode - Creates a new lexical element to wrap the to-be-wrapped node and returns it.
* @returns A new lexical element with the previous node appended within (as a child, including its children).
*/
function $wrapNodeInElement(
node: LexicalNode,
createElementNode: () => ElementNode
): ElementNode;Usage Examples:
import { $wrapNodeInElement } from "@lexical/utils";
// Wrap text node in paragraph
const textNode = $createTextNode('Some text');
const paragraph = $wrapNodeInElement(textNode, () => $createParagraphNode());
// Wrap in quote
const quotedParagraph = $wrapNodeInElement(paragraph, () => $createQuoteNode());
// Wrap in custom element
function createHighlightNode() {
const element = $createElementNode();
element.setFormat('highlight');
return element;
}
const highlightedText = $wrapNodeInElement(
$createTextNode('Important text'),
createHighlightNode
);Replaces a node with its children, effectively removing the wrapper.
/**
* Replace this node with its children
* @param node - The ElementNode to unwrap and remove
*/
function $unwrapNode(node: ElementNode): void;Usage Examples:
import { $unwrapNode } from "@lexical/utils";
// Remove paragraph wrapper, keeping text
const paragraph = $getNodeByKey('paragraph-key') as ElementNode;
$unwrapNode(paragraph); // Text nodes become direct children of parent
// Remove formatting wrapper
const boldElement = $getNodeByKey('bold-key') as ElementNode;
$unwrapNode(boldElement); // Contents lose bold formatting but remainRemoves or unwraps nodes that don't match a predicate in a tree structure.
/**
* A depth first last-to-first traversal of root that stops at each node that matches
* $predicate and ensures that its parent is root. This is typically used to discard
* invalid or unsupported wrapping nodes. For example, a TableNode must only have
* TableRowNode as children, but an importer might add invalid nodes based on
* caption, tbody, thead, etc. and this will unwrap and discard those.
* @param root - The root to start the traversal
* @param $predicate - Should return true for nodes that are permitted to be children of root
* @returns true if this unwrapped or removed any nodes
*/
function $unwrapAndFilterDescendants(
root: ElementNode,
$predicate: (node: LexicalNode) => boolean
): boolean;Usage Examples:
import { $unwrapAndFilterDescendants } from "@lexical/utils";
import { $isTableRowNode } from "@lexical/table";
// Clean up table structure - only allow table rows
const tableNode = $getNodeByKey('table-key') as TableNode;
const wasModified = $unwrapAndFilterDescendants(
tableNode,
(node) => $isTableRowNode(node)
);
if (wasModified) {
console.log('Removed invalid table children');
}
// Clean up list structure - only allow list items
const listNode = $getNodeByKey('list-key') as ListNode;
$unwrapAndFilterDescendants(
listNode,
(node) => $isListItemNode(node)
);Collects descendants that match a predicate without mutating the tree.
/**
* A depth first traversal of the children array that stops at and collects
* each node that `$predicate` matches. This is typically used to discard
* invalid or unsupported wrapping nodes on a children array in the `after`
* of an {@link lexical!DOMConversionOutput}. For example, a TableNode must only have
* TableRowNode as children, but an importer might add invalid nodes based on
* caption, tbody, thead, etc. and this will unwrap and discard those.
* @param children - The children to traverse
* @param $predicate - Should return true for nodes that are permitted to be children of root
* @returns The children or their descendants that match $predicate
*/
function $descendantsMatching<T extends LexicalNode>(
children: LexicalNode[],
$predicate: (node: LexicalNode) => node is T
): T[];
function $descendantsMatching(
children: LexicalNode[],
$predicate: (node: LexicalNode) => boolean
): LexicalNode[];Clones and restores an editor state with full reconciliation.
/**
* Clones the editor and marks it as dirty to be reconciled. If there was a selection,
* it would be set back to its previous state, or null otherwise.
* @param editor - The lexical editor
* @param editorState - The editor's state
*/
function $restoreEditorState(
editor: LexicalEditor,
editorState: EditorState
): void;Usage Examples:
import { $restoreEditorState } from "@lexical/utils";
// Save and restore editor state for undo functionality
let savedState: EditorState;
function saveCurrentState() {
savedState = editor.getEditorState();
}
function restoreToSavedState() {
if (savedState) {
editor.update(() => {
$restoreEditorState(editor, savedState);
});
}
}
// Implement custom undo/redo system
class CustomHistoryManager {
private states: EditorState[] = [];
private currentIndex = -1;
save(state: EditorState) {
this.states = this.states.slice(0, this.currentIndex + 1);
this.states.push(state);
this.currentIndex++;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
const state = this.states[this.currentIndex];
editor.update(() => {
$restoreEditorState(editor, state);
});
}
}
redo() {
if (this.currentIndex < this.states.length - 1) {
this.currentIndex++;
const state = this.states[this.currentIndex];
editor.update(() => {
$restoreEditorState(editor, state);
});
}
}
}Registers a transform to resolve nested elements of the same type.
/**
* Attempts to resolve nested element nodes of the same type into a single node of that type.
* It is generally used for marks/commenting
* @param editor - The lexical editor
* @param targetNode - The target for the nested element to be extracted from.
* @param cloneNode - See {@link $createMarkNode}
* @param handleOverlap - Handles any overlap between the node to extract and the targetNode
* @returns The lexical editor
*/
function registerNestedElementResolver<N extends ElementNode>(
editor: LexicalEditor,
targetNode: Klass<N>,
cloneNode: (from: N) => N,
handleOverlap: (from: N, to: N) => void
): () => void;Usage Examples:
import { registerNestedElementResolver } from "@lexical/utils";
// Resolve nested mark nodes (e.g., bold inside bold)
class MarkNode extends ElementNode {
static getType() { return 'mark'; }
// ... implementation
}
const removeResolver = registerNestedElementResolver(
editor,
MarkNode,
(from) => {
const clone = new MarkNode();
clone.setFormat(from.getFormat());
return clone;
},
(from, to) => {
// Merge formatting properties
to.toggleFormat(from.getFormat());
}
);
// Clean up when done
removeResolver();Determines if an editor is nested within another editor.
/**
* Checks if the editor is a nested editor created by LexicalNestedComposer
*/
function $isEditorIsNestedEditor(editor: LexicalEditor): boolean;Usage Examples:
import { $isEditorIsNestedEditor } from "@lexical/utils";
// Conditional behavior based on nesting
if ($isEditorIsNestedEditor(editor)) {
// Different behavior for nested editors
console.log('This is a nested editor');
// Maybe disable certain features or modify behavior
} else {
console.log('This is a root editor');
// Full feature set available
}Creates a convenient wrapper for working with state configurations.
/**
* EXPERIMENTAL
*
* A convenience interface for working with {@link $getState} and
* {@link $setState}.
*
* @param stateConfig - The stateConfig to wrap with convenience functionality
* @returns a StateWrapper
*/
function makeStateWrapper<K extends string, V>(
stateConfig: StateConfig<K, V>
): StateConfigWrapper<K, V>;
interface StateConfigWrapper<K extends string, V> {
readonly stateConfig: StateConfig<K, V>;
readonly $get: <T extends LexicalNode>(node: T) => V;
readonly $set: <T extends LexicalNode>(
node: T,
valueOrUpdater: ValueOrUpdater<V>
) => T;
readonly accessors: readonly [$get: this['$get'], $set: this['$set']];
makeGetterMethod<T extends LexicalNode>(): (this: T) => V;
makeSetterMethod<T extends LexicalNode>(): (
this: T,
valueOrUpdater: ValueOrUpdater<V>
) => T;
}Usage Examples:
import { makeStateWrapper } from "@lexical/utils";
// Create state configuration for custom metadata
const metadataConfig = createStateConfig<'metadata', CustomMetadata>();
const metadataWrapper = makeStateWrapper(metadataConfig);
// Use convenient accessors
const metadata = metadataWrapper.$get(someNode);
metadataWrapper.$set(someNode, { lastModified: Date.now() });
// Create bound methods for custom node class
class CustomNode extends ElementNode {
getMetadata = metadataWrapper.makeGetterMethod<this>();
setMetadata = metadataWrapper.makeSetterMethod<this>();
updateLastModified() {
return this.setMetadata({ lastModified: Date.now() });
}
}
``