0
# Editor State Management
1
2
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.
3
4
## Capabilities
5
6
### Node Insertion
7
8
#### Insert Node to Nearest Root
9
10
Inserts a node at the nearest root position with automatic paragraph wrapping and intelligent selection management.
11
12
```typescript { .api }
13
/**
14
* If the selected insertion area is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
15
* the node will be appended there, otherwise, it will be inserted before the insertion area.
16
* If there is no selection where the node is to be inserted, it will be appended after any current nodes
17
* within the tree, as a child of the root node. A paragraph will then be added after the inserted node and selected.
18
* @param node - The node to be inserted
19
* @returns The node after its insertion
20
*/
21
function $insertNodeToNearestRoot<T extends LexicalNode>(node: T): T;
22
```
23
24
**Usage Examples:**
25
26
```typescript
27
import { $insertNodeToNearestRoot } from "@lexical/utils";
28
import { $createHeadingNode, $createImageNode, $createQuoteNode } from "lexical";
29
30
// Insert heading at current selection or root
31
const headingNode = $createHeadingNode('h1');
32
headingNode.append($createTextNode('New Heading'));
33
$insertNodeToNearestRoot(headingNode);
34
35
// Insert image node
36
const imageNode = $createImageNode({
37
src: 'image.jpg',
38
alt: 'Description'
39
});
40
$insertNodeToNearestRoot(imageNode);
41
42
// Insert quote block
43
const quoteNode = $createQuoteNode();
44
quoteNode.append($createTextNode('Quoted text here'));
45
$insertNodeToNearestRoot(quoteNode);
46
47
// Chain multiple insertions
48
editor.update(() => {
49
const title = $createHeadingNode('h1');
50
title.append($createTextNode('Document Title'));
51
$insertNodeToNearestRoot(title);
52
53
const content = $createParagraphNode();
54
content.append($createTextNode('Document content starts here.'));
55
$insertNodeToNearestRoot(content);
56
});
57
```
58
59
#### Insert Node at Caret Position
60
61
More precise node insertion with caret-based positioning and splitting options.
62
63
```typescript { .api }
64
/**
65
* If the insertion caret is the root/shadow root node (see {@link lexical!$isRootOrShadowRoot}),
66
* the node will be inserted there, otherwise the parent nodes will be split according to the
67
* given options.
68
* @param node - The node to be inserted
69
* @param caret - The location to insert or split from
70
* @param options - Options for splitting behavior
71
* @returns The node after its insertion
72
*/
73
function $insertNodeToNearestRootAtCaret<
74
T extends LexicalNode,
75
D extends CaretDirection
76
>(
77
node: T,
78
caret: PointCaret<D>,
79
options?: SplitAtPointCaretNextOptions
80
): NodeCaret<D>;
81
```
82
83
#### Insert as First Child
84
85
Inserts a node as the first child of a parent element.
86
87
```typescript { .api }
88
/**
89
* Appends the node before the first child of the parent node
90
* @param parent - A parent node
91
* @param node - Node that needs to be appended
92
*/
93
function $insertFirst(parent: ElementNode, node: LexicalNode): void;
94
```
95
96
**Usage Examples:**
97
98
```typescript
99
import { $insertFirst } from "@lexical/utils";
100
101
const listNode = $createListNode('bullet');
102
const firstItem = $createListItemNode();
103
firstItem.append($createTextNode('First item'));
104
105
// Insert as first child
106
$insertFirst(listNode, firstItem);
107
108
// Insert multiple nodes in order
109
const items = ['First', 'Second', 'Third'];
110
items.reverse().forEach(text => {
111
const item = $createListItemNode();
112
item.append($createTextNode(text));
113
$insertFirst(listNode, item);
114
});
115
```
116
117
### Node Wrapping and Unwrapping
118
119
#### Wrap Node in Element
120
121
Wraps a node within a newly created element node.
122
123
```typescript { .api }
124
/**
125
* Wraps the node into another node created from a createElementNode function, eg. $createParagraphNode
126
* @param node - Node to be wrapped.
127
* @param createElementNode - Creates a new lexical element to wrap the to-be-wrapped node and returns it.
128
* @returns A new lexical element with the previous node appended within (as a child, including its children).
129
*/
130
function $wrapNodeInElement(
131
node: LexicalNode,
132
createElementNode: () => ElementNode
133
): ElementNode;
134
```
135
136
**Usage Examples:**
137
138
```typescript
139
import { $wrapNodeInElement } from "@lexical/utils";
140
141
// Wrap text node in paragraph
142
const textNode = $createTextNode('Some text');
143
const paragraph = $wrapNodeInElement(textNode, () => $createParagraphNode());
144
145
// Wrap in quote
146
const quotedParagraph = $wrapNodeInElement(paragraph, () => $createQuoteNode());
147
148
// Wrap in custom element
149
function createHighlightNode() {
150
const element = $createElementNode();
151
element.setFormat('highlight');
152
return element;
153
}
154
155
const highlightedText = $wrapNodeInElement(
156
$createTextNode('Important text'),
157
createHighlightNode
158
);
159
```
160
161
#### Unwrap Node
162
163
Replaces a node with its children, effectively removing the wrapper.
164
165
```typescript { .api }
166
/**
167
* Replace this node with its children
168
* @param node - The ElementNode to unwrap and remove
169
*/
170
function $unwrapNode(node: ElementNode): void;
171
```
172
173
**Usage Examples:**
174
175
```typescript
176
import { $unwrapNode } from "@lexical/utils";
177
178
// Remove paragraph wrapper, keeping text
179
const paragraph = $getNodeByKey('paragraph-key') as ElementNode;
180
$unwrapNode(paragraph); // Text nodes become direct children of parent
181
182
// Remove formatting wrapper
183
const boldElement = $getNodeByKey('bold-key') as ElementNode;
184
$unwrapNode(boldElement); // Contents lose bold formatting but remain
185
```
186
187
### Advanced Tree Operations
188
189
#### Unwrap and Filter Descendants
190
191
Removes or unwraps nodes that don't match a predicate in a tree structure.
192
193
```typescript { .api }
194
/**
195
* A depth first last-to-first traversal of root that stops at each node that matches
196
* $predicate and ensures that its parent is root. This is typically used to discard
197
* invalid or unsupported wrapping nodes. For example, a TableNode must only have
198
* TableRowNode as children, but an importer might add invalid nodes based on
199
* caption, tbody, thead, etc. and this will unwrap and discard those.
200
* @param root - The root to start the traversal
201
* @param $predicate - Should return true for nodes that are permitted to be children of root
202
* @returns true if this unwrapped or removed any nodes
203
*/
204
function $unwrapAndFilterDescendants(
205
root: ElementNode,
206
$predicate: (node: LexicalNode) => boolean
207
): boolean;
208
```
209
210
**Usage Examples:**
211
212
```typescript
213
import { $unwrapAndFilterDescendants } from "@lexical/utils";
214
import { $isTableRowNode } from "@lexical/table";
215
216
// Clean up table structure - only allow table rows
217
const tableNode = $getNodeByKey('table-key') as TableNode;
218
const wasModified = $unwrapAndFilterDescendants(
219
tableNode,
220
(node) => $isTableRowNode(node)
221
);
222
223
if (wasModified) {
224
console.log('Removed invalid table children');
225
}
226
227
// Clean up list structure - only allow list items
228
const listNode = $getNodeByKey('list-key') as ListNode;
229
$unwrapAndFilterDescendants(
230
listNode,
231
(node) => $isListItemNode(node)
232
);
233
```
234
235
#### Descendants Matching
236
237
Collects descendants that match a predicate without mutating the tree.
238
239
```typescript { .api }
240
/**
241
* A depth first traversal of the children array that stops at and collects
242
* each node that `$predicate` matches. This is typically used to discard
243
* invalid or unsupported wrapping nodes on a children array in the `after`
244
* of an {@link lexical!DOMConversionOutput}. For example, a TableNode must only have
245
* TableRowNode as children, but an importer might add invalid nodes based on
246
* caption, tbody, thead, etc. and this will unwrap and discard those.
247
* @param children - The children to traverse
248
* @param $predicate - Should return true for nodes that are permitted to be children of root
249
* @returns The children or their descendants that match $predicate
250
*/
251
function $descendantsMatching<T extends LexicalNode>(
252
children: LexicalNode[],
253
$predicate: (node: LexicalNode) => node is T
254
): T[];
255
function $descendantsMatching(
256
children: LexicalNode[],
257
$predicate: (node: LexicalNode) => boolean
258
): LexicalNode[];
259
```
260
261
### Editor State Restoration
262
263
Clones and restores an editor state with full reconciliation.
264
265
```typescript { .api }
266
/**
267
* Clones the editor and marks it as dirty to be reconciled. If there was a selection,
268
* it would be set back to its previous state, or null otherwise.
269
* @param editor - The lexical editor
270
* @param editorState - The editor's state
271
*/
272
function $restoreEditorState(
273
editor: LexicalEditor,
274
editorState: EditorState
275
): void;
276
```
277
278
**Usage Examples:**
279
280
```typescript
281
import { $restoreEditorState } from "@lexical/utils";
282
283
// Save and restore editor state for undo functionality
284
let savedState: EditorState;
285
286
function saveCurrentState() {
287
savedState = editor.getEditorState();
288
}
289
290
function restoreToSavedState() {
291
if (savedState) {
292
editor.update(() => {
293
$restoreEditorState(editor, savedState);
294
});
295
}
296
}
297
298
// Implement custom undo/redo system
299
class CustomHistoryManager {
300
private states: EditorState[] = [];
301
private currentIndex = -1;
302
303
save(state: EditorState) {
304
this.states = this.states.slice(0, this.currentIndex + 1);
305
this.states.push(state);
306
this.currentIndex++;
307
}
308
309
undo() {
310
if (this.currentIndex > 0) {
311
this.currentIndex--;
312
const state = this.states[this.currentIndex];
313
editor.update(() => {
314
$restoreEditorState(editor, state);
315
});
316
}
317
}
318
319
redo() {
320
if (this.currentIndex < this.states.length - 1) {
321
this.currentIndex++;
322
const state = this.states[this.currentIndex];
323
editor.update(() => {
324
$restoreEditorState(editor, state);
325
});
326
}
327
}
328
}
329
```
330
331
### Nested Element Resolution
332
333
Registers a transform to resolve nested elements of the same type.
334
335
```typescript { .api }
336
/**
337
* Attempts to resolve nested element nodes of the same type into a single node of that type.
338
* It is generally used for marks/commenting
339
* @param editor - The lexical editor
340
* @param targetNode - The target for the nested element to be extracted from.
341
* @param cloneNode - See {@link $createMarkNode}
342
* @param handleOverlap - Handles any overlap between the node to extract and the targetNode
343
* @returns The lexical editor
344
*/
345
function registerNestedElementResolver<N extends ElementNode>(
346
editor: LexicalEditor,
347
targetNode: Klass<N>,
348
cloneNode: (from: N) => N,
349
handleOverlap: (from: N, to: N) => void
350
): () => void;
351
```
352
353
**Usage Examples:**
354
355
```typescript
356
import { registerNestedElementResolver } from "@lexical/utils";
357
358
// Resolve nested mark nodes (e.g., bold inside bold)
359
class MarkNode extends ElementNode {
360
static getType() { return 'mark'; }
361
// ... implementation
362
}
363
364
const removeResolver = registerNestedElementResolver(
365
editor,
366
MarkNode,
367
(from) => {
368
const clone = new MarkNode();
369
clone.setFormat(from.getFormat());
370
return clone;
371
},
372
(from, to) => {
373
// Merge formatting properties
374
to.toggleFormat(from.getFormat());
375
}
376
);
377
378
// Clean up when done
379
removeResolver();
380
```
381
382
### Editor State Utilities
383
384
#### Check if Editor is Nested
385
386
Determines if an editor is nested within another editor.
387
388
```typescript { .api }
389
/**
390
* Checks if the editor is a nested editor created by LexicalNestedComposer
391
*/
392
function $isEditorIsNestedEditor(editor: LexicalEditor): boolean;
393
```
394
395
**Usage Examples:**
396
397
```typescript
398
import { $isEditorIsNestedEditor } from "@lexical/utils";
399
400
// Conditional behavior based on nesting
401
if ($isEditorIsNestedEditor(editor)) {
402
// Different behavior for nested editors
403
console.log('This is a nested editor');
404
// Maybe disable certain features or modify behavior
405
} else {
406
console.log('This is a root editor');
407
// Full feature set available
408
}
409
```
410
411
### State Configuration Wrapper
412
413
Creates a convenient wrapper for working with state configurations.
414
415
```typescript { .api }
416
/**
417
* EXPERIMENTAL
418
*
419
* A convenience interface for working with {@link $getState} and
420
* {@link $setState}.
421
*
422
* @param stateConfig - The stateConfig to wrap with convenience functionality
423
* @returns a StateWrapper
424
*/
425
function makeStateWrapper<K extends string, V>(
426
stateConfig: StateConfig<K, V>
427
): StateConfigWrapper<K, V>;
428
429
interface StateConfigWrapper<K extends string, V> {
430
readonly stateConfig: StateConfig<K, V>;
431
readonly $get: <T extends LexicalNode>(node: T) => V;
432
readonly $set: <T extends LexicalNode>(
433
node: T,
434
valueOrUpdater: ValueOrUpdater<V>
435
) => T;
436
readonly accessors: readonly [$get: this['$get'], $set: this['$set']];
437
makeGetterMethod<T extends LexicalNode>(): (this: T) => V;
438
makeSetterMethod<T extends LexicalNode>(): (
439
this: T,
440
valueOrUpdater: ValueOrUpdater<V>
441
) => T;
442
}
443
```
444
445
**Usage Examples:**
446
447
```typescript
448
import { makeStateWrapper } from "@lexical/utils";
449
450
// Create state configuration for custom metadata
451
const metadataConfig = createStateConfig<'metadata', CustomMetadata>();
452
const metadataWrapper = makeStateWrapper(metadataConfig);
453
454
// Use convenient accessors
455
const metadata = metadataWrapper.$get(someNode);
456
metadataWrapper.$set(someNode, { lastModified: Date.now() });
457
458
// Create bound methods for custom node class
459
class CustomNode extends ElementNode {
460
getMetadata = metadataWrapper.makeGetterMethod<this>();
461
setMetadata = metadataWrapper.makeSetterMethod<this>();
462
463
updateLastModified() {
464
return this.setMetadata({ lastModified: Date.now() });
465
}
466
}
467
``