or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

dom-manipulation.mdeditor-state.mdfile-handling.mdindex.mdspecialized-utilities.mdtree-traversal.md
tile.json

editor-state.mddocs/

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

``