@lexical/table 0.44.1-nightly.20260518.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/{LexicalTable.dev.js → dist/LexicalTable.dev.js} +243 -33
  2. package/{LexicalTable.dev.mjs → dist/LexicalTable.dev.mjs} +239 -33
  3. package/{LexicalTable.mjs → dist/LexicalTable.mjs} +4 -0
  4. package/{LexicalTable.node.mjs → dist/LexicalTable.node.mjs} +4 -0
  5. package/dist/LexicalTable.prod.js +9 -0
  6. package/dist/LexicalTable.prod.mjs +9 -0
  7. package/dist/TableImportExtension.d.ts +38 -0
  8. package/{index.d.ts → dist/index.d.ts} +1 -0
  9. package/package.json +34 -18
  10. package/src/LexicalTableCellNode.ts +479 -0
  11. package/src/LexicalTableCommands.ts +27 -0
  12. package/src/LexicalTableExtension.ts +104 -0
  13. package/src/LexicalTableNode.ts +678 -0
  14. package/src/LexicalTableObserver.ts +575 -0
  15. package/src/LexicalTablePluginHelpers.ts +694 -0
  16. package/src/LexicalTableRowNode.ts +154 -0
  17. package/src/LexicalTableSelection.ts +460 -0
  18. package/src/LexicalTableSelectionHelpers.ts +2409 -0
  19. package/src/LexicalTableUtils.ts +1386 -0
  20. package/src/TableImportExtension.ts +302 -0
  21. package/src/constants.ts +13 -0
  22. package/src/index.ts +97 -0
  23. package/LexicalTable.prod.js +0 -9
  24. package/LexicalTable.prod.mjs +0 -9
  25. /package/{LexicalTable.js → dist/LexicalTable.js} +0 -0
  26. /package/{LexicalTable.js.flow → dist/LexicalTable.js.flow} +0 -0
  27. /package/{LexicalTableCellNode.d.ts → dist/LexicalTableCellNode.d.ts} +0 -0
  28. /package/{LexicalTableCommands.d.ts → dist/LexicalTableCommands.d.ts} +0 -0
  29. /package/{LexicalTableExtension.d.ts → dist/LexicalTableExtension.d.ts} +0 -0
  30. /package/{LexicalTableNode.d.ts → dist/LexicalTableNode.d.ts} +0 -0
  31. /package/{LexicalTableObserver.d.ts → dist/LexicalTableObserver.d.ts} +0 -0
  32. /package/{LexicalTablePluginHelpers.d.ts → dist/LexicalTablePluginHelpers.d.ts} +0 -0
  33. /package/{LexicalTableRowNode.d.ts → dist/LexicalTableRowNode.d.ts} +0 -0
  34. /package/{LexicalTableSelection.d.ts → dist/LexicalTableSelection.d.ts} +0 -0
  35. /package/{LexicalTableSelectionHelpers.d.ts → dist/LexicalTableSelectionHelpers.d.ts} +0 -0
  36. /package/{LexicalTableUtils.d.ts → dist/LexicalTableUtils.d.ts} +0 -0
  37. /package/{constants.d.ts → dist/constants.d.ts} +0 -0
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {BaseSelection, LexicalUpdateJSON, Spread} from 'lexical';
10
+
11
+ import {$descendantsMatching, addClassNamesToElement} from '@lexical/utils';
12
+ import {
13
+ $applyNodeReplacement,
14
+ DOMConversionMap,
15
+ DOMConversionOutput,
16
+ EditorConfig,
17
+ ElementNode,
18
+ LexicalNode,
19
+ NodeKey,
20
+ SerializedElementNode,
21
+ } from 'lexical';
22
+
23
+ import {PIXEL_VALUE_REG_EXP} from './constants';
24
+ import {$isTableCellNode} from './LexicalTableCellNode';
25
+
26
+ export type SerializedTableRowNode = Spread<
27
+ {
28
+ height?: number;
29
+ },
30
+ SerializedElementNode
31
+ >;
32
+
33
+ /** @noInheritDoc */
34
+ export class TableRowNode extends ElementNode {
35
+ /** @internal */
36
+ __height?: number;
37
+
38
+ static getType(): string {
39
+ return 'tablerow';
40
+ }
41
+
42
+ static clone(node: TableRowNode): TableRowNode {
43
+ return new TableRowNode(node.__height, node.__key);
44
+ }
45
+
46
+ afterCloneFrom(prevNode: this): void {
47
+ super.afterCloneFrom(prevNode);
48
+ this.__height = prevNode.__height;
49
+ }
50
+
51
+ static importDOM(): DOMConversionMap | null {
52
+ return {
53
+ tr: (node: Node) => ({
54
+ conversion: $convertTableRowElement,
55
+ priority: 0,
56
+ }),
57
+ };
58
+ }
59
+
60
+ static importJSON(serializedNode: SerializedTableRowNode): TableRowNode {
61
+ return $createTableRowNode().updateFromJSON(serializedNode);
62
+ }
63
+
64
+ updateFromJSON(
65
+ serializedNode: LexicalUpdateJSON<SerializedTableRowNode>,
66
+ ): this {
67
+ return super
68
+ .updateFromJSON(serializedNode)
69
+ .setHeight(serializedNode.height);
70
+ }
71
+
72
+ constructor(height?: number, key?: NodeKey) {
73
+ super(key);
74
+ this.__height = height;
75
+ }
76
+
77
+ exportJSON(): SerializedTableRowNode {
78
+ const height = this.getHeight();
79
+ return {
80
+ ...super.exportJSON(),
81
+ ...(height === undefined ? undefined : {height}),
82
+ };
83
+ }
84
+
85
+ createDOM(config: EditorConfig): HTMLElement {
86
+ const element = document.createElement('tr');
87
+
88
+ if (this.__height) {
89
+ element.style.height = `${this.__height}px`;
90
+ }
91
+
92
+ addClassNamesToElement(element, config.theme.tableRow);
93
+
94
+ return element;
95
+ }
96
+
97
+ extractWithChild(
98
+ child: LexicalNode,
99
+ selection: BaseSelection | null,
100
+ destination: 'clone' | 'html',
101
+ ): boolean {
102
+ return destination === 'html';
103
+ }
104
+
105
+ isShadowRoot(): boolean {
106
+ return true;
107
+ }
108
+
109
+ setHeight(height?: number | undefined): this {
110
+ const self = this.getWritable();
111
+ self.__height = height;
112
+ return self;
113
+ }
114
+
115
+ getHeight(): number | undefined {
116
+ return this.getLatest().__height;
117
+ }
118
+
119
+ updateDOM(prevNode: this): boolean {
120
+ return prevNode.__height !== this.__height;
121
+ }
122
+
123
+ canBeEmpty(): false {
124
+ return false;
125
+ }
126
+
127
+ canIndent(): false {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ export function $convertTableRowElement(domNode: Node): DOMConversionOutput {
133
+ const domNode_ = domNode as HTMLTableCellElement;
134
+ let height: number | undefined = undefined;
135
+
136
+ if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
137
+ height = parseFloat(domNode_.style.height);
138
+ }
139
+
140
+ return {
141
+ after: children => $descendantsMatching(children, $isTableCellNode),
142
+ node: $createTableRowNode(height),
143
+ };
144
+ }
145
+
146
+ export function $createTableRowNode(height?: number): TableRowNode {
147
+ return $applyNodeReplacement(new TableRowNode(height));
148
+ }
149
+
150
+ export function $isTableRowNode(
151
+ node: LexicalNode | null | undefined,
152
+ ): node is TableRowNode {
153
+ return node instanceof TableRowNode;
154
+ }
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import invariant from '@lexical/internal/invariant';
10
+ import {$findMatchingParent} from '@lexical/utils';
11
+ import {
12
+ $createPoint,
13
+ $getNodeByKey,
14
+ $getSelection,
15
+ $isElementNode,
16
+ $isParagraphNode,
17
+ $normalizeSelection__EXPERIMENTAL,
18
+ BaseSelection,
19
+ ElementNode,
20
+ isCurrentlyReadOnlyMode,
21
+ LexicalNode,
22
+ NodeKey,
23
+ PointType,
24
+ TEXT_TYPE_TO_FORMAT,
25
+ TextFormatType,
26
+ TextNode,
27
+ } from 'lexical';
28
+
29
+ import {$isTableCellNode, TableCellNode} from './LexicalTableCellNode';
30
+ import {$isTableNode, TableNode} from './LexicalTableNode';
31
+ import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
32
+ import {$findTableNode} from './LexicalTableSelectionHelpers';
33
+ import {
34
+ $computeTableCellRectBoundary,
35
+ $computeTableMap,
36
+ $getTableCellNodeRect,
37
+ } from './LexicalTableUtils';
38
+
39
+ const __DEV__ = process.env.NODE_ENV !== 'production';
40
+
41
+ export type TableSelectionShape = {
42
+ fromX: number;
43
+ fromY: number;
44
+ toX: number;
45
+ toY: number;
46
+ };
47
+
48
+ export type TableMapValueType = {
49
+ cell: TableCellNode;
50
+ startRow: number;
51
+ startColumn: number;
52
+ };
53
+ export type TableMapType = Array<Array<TableMapValueType>>;
54
+
55
+ function $getCellNodes(tableSelection: TableSelection): {
56
+ anchorCell: TableCellNode;
57
+ anchorNode: TextNode | ElementNode;
58
+ anchorRow: TableRowNode;
59
+ anchorTable: TableNode;
60
+ focusCell: TableCellNode;
61
+ focusNode: TextNode | ElementNode;
62
+ focusRow: TableRowNode;
63
+ focusTable: TableNode;
64
+ } {
65
+ const [
66
+ [anchorNode, anchorCell, anchorRow, anchorTable],
67
+ [focusNode, focusCell, focusRow, focusTable],
68
+ ] = (['anchor', 'focus'] as const).map(
69
+ (k): [ElementNode | TextNode, TableCellNode, TableRowNode, TableNode] => {
70
+ const node = tableSelection[k].getNode();
71
+ const cellNode = $findMatchingParent(node, $isTableCellNode);
72
+ invariant(
73
+ $isTableCellNode(cellNode),
74
+ 'Expected TableSelection %s to be (or a child of) TableCellNode, got key %s of type %s',
75
+ k,
76
+ node.getKey(),
77
+ node.getType(),
78
+ );
79
+ const rowNode = cellNode.getParent();
80
+ invariant(
81
+ $isTableRowNode(rowNode),
82
+ 'Expected TableSelection %s cell parent to be a TableRowNode',
83
+ k,
84
+ );
85
+ const tableNode = rowNode.getParent();
86
+ invariant(
87
+ $isTableNode(tableNode),
88
+ 'Expected TableSelection %s row parent to be a TableNode',
89
+ k,
90
+ );
91
+ return [node, cellNode, rowNode, tableNode];
92
+ },
93
+ );
94
+ // TODO: nested tables may violate this
95
+ invariant(
96
+ anchorTable.is(focusTable),
97
+ 'Expected TableSelection anchor and focus to be in the same table',
98
+ );
99
+ return {
100
+ anchorCell,
101
+ anchorNode,
102
+ anchorRow,
103
+ anchorTable,
104
+ focusCell,
105
+ focusNode,
106
+ focusRow,
107
+ focusTable,
108
+ };
109
+ }
110
+
111
+ export class TableSelection implements BaseSelection {
112
+ tableKey: NodeKey;
113
+ anchor: PointType;
114
+ focus: PointType;
115
+ _cachedNodes: Array<LexicalNode> | null;
116
+ dirty: boolean;
117
+
118
+ constructor(tableKey: NodeKey, anchor: PointType, focus: PointType) {
119
+ this.anchor = anchor;
120
+ this.focus = focus;
121
+ anchor._selection = this;
122
+ focus._selection = this;
123
+ this._cachedNodes = null;
124
+ this.dirty = false;
125
+ this.tableKey = tableKey;
126
+ }
127
+
128
+ getStartEndPoints(): [PointType, PointType] {
129
+ return [this.anchor, this.focus];
130
+ }
131
+
132
+ /**
133
+ * {@link $createTableSelection} unfortunately makes it very easy to create
134
+ * nonsense selections, so we have a method to see if the selection probably
135
+ * makes sense.
136
+ *
137
+ * @returns true if the TableSelection is (probably) valid
138
+ */
139
+ isValid(): boolean {
140
+ if (
141
+ this.tableKey === 'root' ||
142
+ this.anchor.key === 'root' ||
143
+ this.anchor.type !== 'element' ||
144
+ this.focus.key === 'root' ||
145
+ this.focus.type !== 'element'
146
+ ) {
147
+ return false;
148
+ }
149
+
150
+ // Check if the referenced nodes still exist in the editor
151
+ const tableNode = $getNodeByKey(this.tableKey);
152
+ const anchorNode = $getNodeByKey(this.anchor.key);
153
+ const focusNode = $getNodeByKey(this.focus.key);
154
+
155
+ return tableNode !== null && anchorNode !== null && focusNode !== null;
156
+ }
157
+
158
+ /**
159
+ * Returns whether the Selection is "backwards", meaning the focus
160
+ * logically precedes the anchor in the EditorState.
161
+ * @returns true if the Selection is backwards, false otherwise.
162
+ */
163
+ isBackward(): boolean {
164
+ return this.focus.isBefore(this.anchor);
165
+ }
166
+
167
+ getCachedNodes(): LexicalNode[] | null {
168
+ return this._cachedNodes;
169
+ }
170
+
171
+ setCachedNodes(nodes: LexicalNode[] | null): void {
172
+ this._cachedNodes = nodes;
173
+ }
174
+
175
+ is(selection: null | BaseSelection): boolean {
176
+ return (
177
+ $isTableSelection(selection) &&
178
+ this.tableKey === selection.tableKey &&
179
+ this.anchor.is(selection.anchor) &&
180
+ this.focus.is(selection.focus)
181
+ );
182
+ }
183
+
184
+ set(tableKey: NodeKey, anchorCellKey: NodeKey, focusCellKey: NodeKey): void {
185
+ // note: closure compiler's acorn does not support ||=
186
+ this.dirty =
187
+ this.dirty ||
188
+ tableKey !== this.tableKey ||
189
+ anchorCellKey !== this.anchor.key ||
190
+ focusCellKey !== this.focus.key;
191
+ this.tableKey = tableKey;
192
+ this.anchor.key = anchorCellKey;
193
+ this.focus.key = focusCellKey;
194
+ this._cachedNodes = null;
195
+ }
196
+
197
+ clone(): TableSelection {
198
+ return new TableSelection(
199
+ this.tableKey,
200
+ $createPoint(this.anchor.key, this.anchor.offset, this.anchor.type),
201
+ $createPoint(this.focus.key, this.focus.offset, this.focus.type),
202
+ );
203
+ }
204
+
205
+ isCollapsed(): boolean {
206
+ return false;
207
+ }
208
+
209
+ extract(): Array<LexicalNode> {
210
+ return this.getNodes();
211
+ }
212
+
213
+ insertRawText(text: string): void {
214
+ // Do nothing?
215
+ }
216
+
217
+ insertText(): void {
218
+ // Do nothing?
219
+ }
220
+
221
+ /**
222
+ * Returns whether the provided TextFormatType is present on the Selection.
223
+ * This will be true if any paragraph in table cells has the specified format.
224
+ *
225
+ * @param type the TextFormatType to check for.
226
+ * @returns true if the provided format is currently toggled on on the Selection, false otherwise.
227
+ */
228
+ hasFormat(type: TextFormatType): boolean {
229
+ let format = 0;
230
+
231
+ const cellNodes = this.getNodes().filter($isTableCellNode);
232
+ cellNodes.forEach((cellNode: TableCellNode) => {
233
+ const paragraph = cellNode.getFirstChild();
234
+ if ($isParagraphNode(paragraph)) {
235
+ format |= paragraph.getTextFormat();
236
+ }
237
+ });
238
+
239
+ const formatFlag = TEXT_TYPE_TO_FORMAT[type];
240
+ return (format & formatFlag) !== 0;
241
+ }
242
+
243
+ insertNodes(nodes: Array<LexicalNode>) {
244
+ const focusNode = this.focus.getNode();
245
+ invariant(
246
+ $isElementNode(focusNode),
247
+ 'Expected TableSelection focus to be an ElementNode',
248
+ );
249
+ const selection = $normalizeSelection__EXPERIMENTAL(
250
+ focusNode.select(0, focusNode.getChildrenSize()),
251
+ );
252
+ selection.insertNodes(nodes);
253
+ }
254
+
255
+ // TODO Deprecate this method. It's confusing when used with colspan|rowspan
256
+ getShape(): TableSelectionShape {
257
+ const {anchorCell, focusCell} = $getCellNodes(this);
258
+ const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
259
+ invariant(
260
+ anchorCellNodeRect !== null,
261
+ 'getCellRect: expected to find AnchorNode',
262
+ );
263
+ const focusCellNodeRect = $getTableCellNodeRect(focusCell);
264
+ invariant(
265
+ focusCellNodeRect !== null,
266
+ 'getCellRect: expected to find focusCellNode',
267
+ );
268
+
269
+ const startX = Math.min(
270
+ anchorCellNodeRect.columnIndex,
271
+ focusCellNodeRect.columnIndex,
272
+ );
273
+ const stopX = Math.max(
274
+ anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1,
275
+ focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1,
276
+ );
277
+
278
+ const startY = Math.min(
279
+ anchorCellNodeRect.rowIndex,
280
+ focusCellNodeRect.rowIndex,
281
+ );
282
+ const stopY = Math.max(
283
+ anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1,
284
+ focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1,
285
+ );
286
+
287
+ return {
288
+ fromX: Math.min(startX, stopX),
289
+ fromY: Math.min(startY, stopY),
290
+ toX: Math.max(startX, stopX),
291
+ toY: Math.max(startY, stopY),
292
+ };
293
+ }
294
+
295
+ getNodes(): Array<LexicalNode> {
296
+ if (!this.isValid()) {
297
+ return [];
298
+ }
299
+ const cachedNodes = this._cachedNodes;
300
+ if (cachedNodes !== null) {
301
+ return cachedNodes;
302
+ }
303
+
304
+ const {anchorTable: tableNode, anchorCell, focusCell} = $getCellNodes(this);
305
+
306
+ const focusCellGrid = focusCell.getParents()[1];
307
+ if (focusCellGrid !== tableNode) {
308
+ if (!tableNode.isParentOf(focusCell)) {
309
+ // focus is on higher Grid level than anchor
310
+ const gridParent = tableNode.getParent();
311
+ invariant(gridParent != null, 'Expected gridParent to have a parent');
312
+ this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
313
+ } else {
314
+ // anchor is on higher Grid level than focus
315
+ const focusCellParent = focusCellGrid.getParent();
316
+ invariant(
317
+ focusCellParent != null,
318
+ 'Expected focusCellParent to have a parent',
319
+ );
320
+ this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
321
+ }
322
+ return this.getNodes();
323
+ }
324
+
325
+ // TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
326
+ // once (on load) and iterate on it as updates occur. However, to do this we need to have the
327
+ // ability to store a state. Killing TableSelection and moving the logic to the plugin would make
328
+ // this possible.
329
+ const [map, cellAMap, cellBMap] = $computeTableMap(
330
+ tableNode,
331
+ anchorCell,
332
+ focusCell,
333
+ );
334
+ const {minColumn, maxColumn, minRow, maxRow} =
335
+ $computeTableCellRectBoundary(map, cellAMap, cellBMap);
336
+
337
+ // We use a Map here because merged cells in the grid would otherwise
338
+ // show up multiple times in the nodes array
339
+ const nodeMap: Map<NodeKey, LexicalNode> = new Map([
340
+ [tableNode.getKey(), tableNode],
341
+ ]);
342
+ let lastRow: null | TableRowNode = null;
343
+ for (let i = minRow; i <= maxRow; i++) {
344
+ for (let j = minColumn; j <= maxColumn; j++) {
345
+ const {cell} = map[i][j];
346
+ const currentRow = cell.getParent();
347
+ invariant(
348
+ $isTableRowNode(currentRow),
349
+ 'Expected TableCellNode parent to be a TableRowNode',
350
+ );
351
+ if (currentRow !== lastRow) {
352
+ nodeMap.set(currentRow.getKey(), currentRow);
353
+ lastRow = currentRow;
354
+ }
355
+ if (!nodeMap.has(cell.getKey())) {
356
+ $visitRecursively(cell, childNode => {
357
+ nodeMap.set(childNode.getKey(), childNode);
358
+ });
359
+ }
360
+ }
361
+ }
362
+ const nodes = Array.from(nodeMap.values());
363
+
364
+ if (!isCurrentlyReadOnlyMode()) {
365
+ this._cachedNodes = nodes;
366
+ }
367
+ return nodes;
368
+ }
369
+
370
+ getTextContent(): string {
371
+ const nodes = this.getNodes().filter(node => $isTableCellNode(node));
372
+ let textContent = '';
373
+ for (let i = 0; i < nodes.length; i++) {
374
+ const node = nodes[i];
375
+ const row = node.__parent;
376
+ const nextRow = (nodes[i + 1] || {}).__parent;
377
+ textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
378
+ }
379
+ return textContent;
380
+ }
381
+ }
382
+
383
+ export function $isTableSelection(x: unknown): x is TableSelection {
384
+ return x instanceof TableSelection;
385
+ }
386
+
387
+ export function $createTableSelection(): TableSelection {
388
+ // TODO this is a suboptimal design, it doesn't make sense to have
389
+ // a table selection that isn't associated with a table. This
390
+ // constructor should have required arguments and in __DEV__ we
391
+ // should check that they point to a table and are element points to
392
+ // cell nodes of that table.
393
+ const anchor = $createPoint('root', 0, 'element');
394
+ const focus = $createPoint('root', 0, 'element');
395
+ return new TableSelection('root', anchor, focus);
396
+ }
397
+
398
+ export function $createTableSelectionFrom(
399
+ tableNode: TableNode,
400
+ anchorCell: TableCellNode,
401
+ focusCell: TableCellNode,
402
+ ): TableSelection {
403
+ const tableNodeKey = tableNode.getKey();
404
+ const anchorCellKey = anchorCell.getKey();
405
+ const focusCellKey = focusCell.getKey();
406
+ if (__DEV__) {
407
+ invariant(
408
+ tableNode.isAttached(),
409
+ '$createTableSelectionFrom: tableNode %s is not attached',
410
+ tableNodeKey,
411
+ );
412
+ invariant(
413
+ tableNode.is($findTableNode(anchorCell)),
414
+ '$createTableSelectionFrom: anchorCell %s is not in table %s',
415
+ anchorCellKey,
416
+ tableNodeKey,
417
+ );
418
+ invariant(
419
+ tableNode.is($findTableNode(focusCell)),
420
+ '$createTableSelectionFrom: focusCell %s is not in table %s',
421
+ focusCellKey,
422
+ tableNodeKey,
423
+ );
424
+ // TODO: Check for rectangular grid
425
+ }
426
+ const prevSelection = $getSelection();
427
+ const nextSelection = $isTableSelection(prevSelection)
428
+ ? prevSelection.clone()
429
+ : $createTableSelection();
430
+ nextSelection.set(
431
+ tableNode.getKey(),
432
+ anchorCell.getKey(),
433
+ focusCell.getKey(),
434
+ );
435
+ return nextSelection;
436
+ }
437
+
438
+ /**
439
+ * Depth first visitor
440
+ * @param node The starting node
441
+ * @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
442
+ */
443
+ export function $visitRecursively(
444
+ node: LexicalNode,
445
+ $visit: (childNode: LexicalNode) => boolean | undefined | void,
446
+ ): void {
447
+ const stack = [[node]];
448
+ for (
449
+ let currentArray = stack.at(-1);
450
+ currentArray !== undefined && stack.length > 0;
451
+ currentArray = stack.at(-1)
452
+ ) {
453
+ const currentNode = currentArray.pop();
454
+ if (currentNode === undefined) {
455
+ stack.pop();
456
+ } else if ($visit(currentNode) !== false && $isElementNode(currentNode)) {
457
+ stack.push(currentNode.getChildren());
458
+ }
459
+ }
460
+ }