@ekz/lexical-table 0.40.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.
@@ -0,0 +1,4315 @@
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
+ 'use strict';
10
+
11
+ var lexicalUtils = require('@ekz/lexical-utils');
12
+ var lexical = require('@ekz/lexical');
13
+ var lexicalExtension = require('@ekz/lexical-extension');
14
+ var lexicalClipboard = require('@ekz/lexical-clipboard');
15
+
16
+ /**
17
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
18
+ *
19
+ * This source code is licensed under the MIT license found in the
20
+ * LICENSE file in the root directory of this source tree.
21
+ *
22
+ */
23
+
24
+ const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
25
+
26
+ // .PlaygroundEditorTheme__tableCell width value from
27
+ // packages/lexical-playground/src/themes/PlaygroundEditorTheme.css
28
+ const COLUMN_WIDTH = 75;
29
+
30
+ /**
31
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
32
+ *
33
+ * This source code is licensed under the MIT license found in the
34
+ * LICENSE file in the root directory of this source tree.
35
+ *
36
+ */
37
+
38
+ const TableCellHeaderStates = {
39
+ BOTH: 3,
40
+ COLUMN: 2,
41
+ NO_STATUS: 0,
42
+ ROW: 1
43
+ };
44
+ /** @noInheritDoc */
45
+ class TableCellNode extends lexical.ElementNode {
46
+ /** @internal */
47
+ __colSpan;
48
+ /** @internal */
49
+ __rowSpan;
50
+ /** @internal */
51
+ __headerState;
52
+ /** @internal */
53
+ __width;
54
+ /** @internal */
55
+ __backgroundColor;
56
+ /** @internal */
57
+ __verticalAlign;
58
+ static getType() {
59
+ return 'tablecell';
60
+ }
61
+ static clone(node) {
62
+ return new TableCellNode(node.__headerState, node.__colSpan, node.__width, node.__key);
63
+ }
64
+ afterCloneFrom(node) {
65
+ super.afterCloneFrom(node);
66
+ this.__rowSpan = node.__rowSpan;
67
+ this.__backgroundColor = node.__backgroundColor;
68
+ this.__verticalAlign = node.__verticalAlign;
69
+ }
70
+ static importDOM() {
71
+ return {
72
+ td: node => ({
73
+ conversion: $convertTableCellNodeElement,
74
+ priority: 0
75
+ }),
76
+ th: node => ({
77
+ conversion: $convertTableCellNodeElement,
78
+ priority: 0
79
+ })
80
+ };
81
+ }
82
+ static importJSON(serializedNode) {
83
+ return $createTableCellNode().updateFromJSON(serializedNode);
84
+ }
85
+ updateFromJSON(serializedNode) {
86
+ return super.updateFromJSON(serializedNode).setHeaderStyles(serializedNode.headerState).setColSpan(serializedNode.colSpan || 1).setRowSpan(serializedNode.rowSpan || 1).setWidth(serializedNode.width || undefined).setBackgroundColor(serializedNode.backgroundColor || null).setVerticalAlign(serializedNode.verticalAlign || undefined);
87
+ }
88
+ constructor(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width, key) {
89
+ super(key);
90
+ this.__colSpan = colSpan;
91
+ this.__rowSpan = 1;
92
+ this.__headerState = headerState;
93
+ this.__width = width;
94
+ this.__backgroundColor = null;
95
+ this.__verticalAlign = undefined;
96
+ }
97
+ createDOM(config) {
98
+ const element = document.createElement(this.getTag());
99
+ if (this.__width) {
100
+ element.style.width = `${this.__width}px`;
101
+ }
102
+ if (this.__colSpan > 1) {
103
+ element.colSpan = this.__colSpan;
104
+ }
105
+ if (this.__rowSpan > 1) {
106
+ element.rowSpan = this.__rowSpan;
107
+ }
108
+ if (this.__backgroundColor !== null) {
109
+ element.style.backgroundColor = this.__backgroundColor;
110
+ }
111
+ if (isValidVerticalAlign(this.__verticalAlign)) {
112
+ element.style.verticalAlign = this.__verticalAlign;
113
+ }
114
+ lexicalUtils.addClassNamesToElement(element, config.theme.tableCell, this.hasHeader() && config.theme.tableCellHeader);
115
+ return element;
116
+ }
117
+ exportDOM(editor) {
118
+ const output = super.exportDOM(editor);
119
+ if (lexical.isHTMLElement(output.element)) {
120
+ const element = output.element;
121
+ element.setAttribute('data-temporary-table-cell-lexical-key', this.getKey());
122
+ element.style.border = '1px solid black';
123
+ if (this.__colSpan > 1) {
124
+ element.colSpan = this.__colSpan;
125
+ }
126
+ if (this.__rowSpan > 1) {
127
+ element.rowSpan = this.__rowSpan;
128
+ }
129
+ element.style.width = `${this.getWidth() || COLUMN_WIDTH}px`;
130
+ element.style.verticalAlign = this.getVerticalAlign() || 'top';
131
+ element.style.textAlign = 'start';
132
+ if (this.__backgroundColor === null && this.hasHeader()) {
133
+ element.style.backgroundColor = '#f2f3f5';
134
+ }
135
+ }
136
+ return output;
137
+ }
138
+ exportJSON() {
139
+ return {
140
+ ...super.exportJSON(),
141
+ ...(isValidVerticalAlign(this.__verticalAlign) && {
142
+ verticalAlign: this.__verticalAlign
143
+ }),
144
+ backgroundColor: this.getBackgroundColor(),
145
+ colSpan: this.__colSpan,
146
+ headerState: this.__headerState,
147
+ rowSpan: this.__rowSpan,
148
+ width: this.getWidth()
149
+ };
150
+ }
151
+ getColSpan() {
152
+ return this.getLatest().__colSpan;
153
+ }
154
+ setColSpan(colSpan) {
155
+ const self = this.getWritable();
156
+ self.__colSpan = colSpan;
157
+ return self;
158
+ }
159
+ getRowSpan() {
160
+ return this.getLatest().__rowSpan;
161
+ }
162
+ setRowSpan(rowSpan) {
163
+ const self = this.getWritable();
164
+ self.__rowSpan = rowSpan;
165
+ return self;
166
+ }
167
+ getTag() {
168
+ return this.hasHeader() ? 'th' : 'td';
169
+ }
170
+ setHeaderStyles(headerState, mask = TableCellHeaderStates.BOTH) {
171
+ const self = this.getWritable();
172
+ self.__headerState = headerState & mask | self.__headerState & ~mask;
173
+ return self;
174
+ }
175
+ getHeaderStyles() {
176
+ return this.getLatest().__headerState;
177
+ }
178
+ setWidth(width) {
179
+ const self = this.getWritable();
180
+ self.__width = width;
181
+ return self;
182
+ }
183
+ getWidth() {
184
+ return this.getLatest().__width;
185
+ }
186
+ getBackgroundColor() {
187
+ return this.getLatest().__backgroundColor;
188
+ }
189
+ setBackgroundColor(newBackgroundColor) {
190
+ const self = this.getWritable();
191
+ self.__backgroundColor = newBackgroundColor;
192
+ return self;
193
+ }
194
+ getVerticalAlign() {
195
+ return this.getLatest().__verticalAlign;
196
+ }
197
+ setVerticalAlign(newVerticalAlign) {
198
+ const self = this.getWritable();
199
+ self.__verticalAlign = newVerticalAlign || undefined;
200
+ return self;
201
+ }
202
+ toggleHeaderStyle(headerStateToToggle) {
203
+ const self = this.getWritable();
204
+ if ((self.__headerState & headerStateToToggle) === headerStateToToggle) {
205
+ self.__headerState -= headerStateToToggle;
206
+ } else {
207
+ self.__headerState += headerStateToToggle;
208
+ }
209
+ return self;
210
+ }
211
+ hasHeaderState(headerState) {
212
+ return (this.getHeaderStyles() & headerState) === headerState;
213
+ }
214
+ hasHeader() {
215
+ return this.getLatest().__headerState !== TableCellHeaderStates.NO_STATUS;
216
+ }
217
+ updateDOM(prevNode) {
218
+ return prevNode.__headerState !== this.__headerState || prevNode.__width !== this.__width || prevNode.__colSpan !== this.__colSpan || prevNode.__rowSpan !== this.__rowSpan || prevNode.__backgroundColor !== this.__backgroundColor || prevNode.__verticalAlign !== this.__verticalAlign;
219
+ }
220
+ isShadowRoot() {
221
+ return true;
222
+ }
223
+ collapseAtStart() {
224
+ return true;
225
+ }
226
+ canBeEmpty() {
227
+ return false;
228
+ }
229
+ canIndent() {
230
+ return false;
231
+ }
232
+ }
233
+ function isValidVerticalAlign(verticalAlign) {
234
+ return verticalAlign === 'middle' || verticalAlign === 'bottom';
235
+ }
236
+ function $convertTableCellNodeElement(domNode) {
237
+ const domNode_ = domNode;
238
+ const nodeName = domNode.nodeName.toLowerCase();
239
+ let width = undefined;
240
+ if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
241
+ width = parseFloat(domNode_.style.width);
242
+ }
243
+ const tableCellNode = $createTableCellNode(nodeName === 'th' ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width);
244
+ tableCellNode.__rowSpan = domNode_.rowSpan;
245
+ const backgroundColor = domNode_.style.backgroundColor;
246
+ if (backgroundColor !== '') {
247
+ tableCellNode.__backgroundColor = backgroundColor;
248
+ }
249
+ const verticalAlign = domNode_.style.verticalAlign;
250
+ if (isValidVerticalAlign(verticalAlign)) {
251
+ tableCellNode.__verticalAlign = verticalAlign;
252
+ }
253
+ const style = domNode_.style;
254
+ const textDecoration = (style && style.textDecoration || '').split(' ');
255
+ const hasBoldFontWeight = style.fontWeight === '700' || style.fontWeight === 'bold';
256
+ const hasLinethroughTextDecoration = textDecoration.includes('line-through');
257
+ const hasItalicFontStyle = style.fontStyle === 'italic';
258
+ const hasUnderlineTextDecoration = textDecoration.includes('underline');
259
+ return {
260
+ after: childLexicalNodes => {
261
+ const result = [];
262
+ let paragraphNode = null;
263
+ const removeSingleLineBreakNode = () => {
264
+ if (paragraphNode) {
265
+ const firstChild = paragraphNode.getFirstChild();
266
+ if (lexical.$isLineBreakNode(firstChild) && paragraphNode.getChildrenSize() === 1) {
267
+ firstChild.remove();
268
+ }
269
+ }
270
+ };
271
+ for (const child of childLexicalNodes) {
272
+ if (lexical.$isInlineElementOrDecoratorNode(child) || lexical.$isTextNode(child) || lexical.$isLineBreakNode(child)) {
273
+ if (lexical.$isTextNode(child)) {
274
+ if (hasBoldFontWeight) {
275
+ child.toggleFormat('bold');
276
+ }
277
+ if (hasLinethroughTextDecoration) {
278
+ child.toggleFormat('strikethrough');
279
+ }
280
+ if (hasItalicFontStyle) {
281
+ child.toggleFormat('italic');
282
+ }
283
+ if (hasUnderlineTextDecoration) {
284
+ child.toggleFormat('underline');
285
+ }
286
+ }
287
+ if (paragraphNode) {
288
+ paragraphNode.append(child);
289
+ } else {
290
+ paragraphNode = lexical.$createParagraphNode().append(child);
291
+ result.push(paragraphNode);
292
+ }
293
+ } else {
294
+ result.push(child);
295
+ removeSingleLineBreakNode();
296
+ paragraphNode = null;
297
+ }
298
+ }
299
+ removeSingleLineBreakNode();
300
+ if (result.length === 0) {
301
+ result.push(lexical.$createParagraphNode());
302
+ }
303
+ return result;
304
+ },
305
+ node: tableCellNode
306
+ };
307
+ }
308
+ function $createTableCellNode(headerState = TableCellHeaderStates.NO_STATUS, colSpan = 1, width) {
309
+ return lexical.$applyNodeReplacement(new TableCellNode(headerState, colSpan, width));
310
+ }
311
+ function $isTableCellNode(node) {
312
+ return node instanceof TableCellNode;
313
+ }
314
+
315
+ /**
316
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
317
+ *
318
+ * This source code is licensed under the MIT license found in the
319
+ * LICENSE file in the root directory of this source tree.
320
+ *
321
+ */
322
+
323
+ const INSERT_TABLE_COMMAND = lexical.createCommand('INSERT_TABLE_COMMAND');
324
+
325
+ /**
326
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
327
+ *
328
+ * This source code is licensed under the MIT license found in the
329
+ * LICENSE file in the root directory of this source tree.
330
+ *
331
+ */
332
+
333
+ // Do not require this module directly! Use normal `invariant` calls.
334
+
335
+ function formatDevErrorMessage(message) {
336
+ throw new Error(message);
337
+ }
338
+
339
+ /**
340
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
341
+ *
342
+ * This source code is licensed under the MIT license found in the
343
+ * LICENSE file in the root directory of this source tree.
344
+ *
345
+ */
346
+
347
+ /** @noInheritDoc */
348
+ class TableRowNode extends lexical.ElementNode {
349
+ /** @internal */
350
+ __height;
351
+ static getType() {
352
+ return 'tablerow';
353
+ }
354
+ static clone(node) {
355
+ return new TableRowNode(node.__height, node.__key);
356
+ }
357
+ static importDOM() {
358
+ return {
359
+ tr: node => ({
360
+ conversion: $convertTableRowElement,
361
+ priority: 0
362
+ })
363
+ };
364
+ }
365
+ static importJSON(serializedNode) {
366
+ return $createTableRowNode().updateFromJSON(serializedNode);
367
+ }
368
+ updateFromJSON(serializedNode) {
369
+ return super.updateFromJSON(serializedNode).setHeight(serializedNode.height);
370
+ }
371
+ constructor(height, key) {
372
+ super(key);
373
+ this.__height = height;
374
+ }
375
+ exportJSON() {
376
+ const height = this.getHeight();
377
+ return {
378
+ ...super.exportJSON(),
379
+ ...(height === undefined ? undefined : {
380
+ height
381
+ })
382
+ };
383
+ }
384
+ createDOM(config) {
385
+ const element = document.createElement('tr');
386
+ if (this.__height) {
387
+ element.style.height = `${this.__height}px`;
388
+ }
389
+ lexicalUtils.addClassNamesToElement(element, config.theme.tableRow);
390
+ return element;
391
+ }
392
+ extractWithChild(child, selection, destination) {
393
+ return destination === 'html';
394
+ }
395
+ isShadowRoot() {
396
+ return true;
397
+ }
398
+ setHeight(height) {
399
+ const self = this.getWritable();
400
+ self.__height = height;
401
+ return self;
402
+ }
403
+ getHeight() {
404
+ return this.getLatest().__height;
405
+ }
406
+ updateDOM(prevNode) {
407
+ return prevNode.__height !== this.__height;
408
+ }
409
+ canBeEmpty() {
410
+ return false;
411
+ }
412
+ canIndent() {
413
+ return false;
414
+ }
415
+ }
416
+ function $convertTableRowElement(domNode) {
417
+ const domNode_ = domNode;
418
+ let height = undefined;
419
+ if (PIXEL_VALUE_REG_EXP.test(domNode_.style.height)) {
420
+ height = parseFloat(domNode_.style.height);
421
+ }
422
+ return {
423
+ after: children => lexicalUtils.$descendantsMatching(children, $isTableCellNode),
424
+ node: $createTableRowNode(height)
425
+ };
426
+ }
427
+ function $createTableRowNode(height) {
428
+ return lexical.$applyNodeReplacement(new TableRowNode(height));
429
+ }
430
+ function $isTableRowNode(node) {
431
+ return node instanceof TableRowNode;
432
+ }
433
+
434
+ /**
435
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
436
+ *
437
+ * This source code is licensed under the MIT license found in the
438
+ * LICENSE file in the root directory of this source tree.
439
+ *
440
+ */
441
+
442
+ const CAN_USE_DOM = typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined';
443
+
444
+ /**
445
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
446
+ *
447
+ * This source code is licensed under the MIT license found in the
448
+ * LICENSE file in the root directory of this source tree.
449
+ *
450
+ */
451
+
452
+ const documentMode = CAN_USE_DOM && 'documentMode' in document ? document.documentMode : null;
453
+ const IS_FIREFOX = CAN_USE_DOM && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
454
+ CAN_USE_DOM && 'InputEvent' in window && !documentMode ? 'getTargetRanges' in new window.InputEvent('input') : false;
455
+
456
+ function $createTableNodeWithDimensions(rowCount, columnCount, includeHeaders = true) {
457
+ const tableNode = $createTableNode();
458
+ for (let iRow = 0; iRow < rowCount; iRow++) {
459
+ const tableRowNode = $createTableRowNode();
460
+ for (let iColumn = 0; iColumn < columnCount; iColumn++) {
461
+ let headerState = TableCellHeaderStates.NO_STATUS;
462
+ if (typeof includeHeaders === 'object') {
463
+ if (iRow === 0 && includeHeaders.rows) {
464
+ headerState |= TableCellHeaderStates.ROW;
465
+ }
466
+ if (iColumn === 0 && includeHeaders.columns) {
467
+ headerState |= TableCellHeaderStates.COLUMN;
468
+ }
469
+ } else if (includeHeaders) {
470
+ if (iRow === 0) {
471
+ headerState |= TableCellHeaderStates.ROW;
472
+ }
473
+ if (iColumn === 0) {
474
+ headerState |= TableCellHeaderStates.COLUMN;
475
+ }
476
+ }
477
+ const tableCellNode = $createTableCellNode(headerState);
478
+ const paragraphNode = lexical.$createParagraphNode();
479
+ paragraphNode.append(lexical.$createTextNode());
480
+ tableCellNode.append(paragraphNode);
481
+ tableRowNode.append(tableCellNode);
482
+ }
483
+ tableNode.append(tableRowNode);
484
+ }
485
+ return tableNode;
486
+ }
487
+ function $getTableCellNodeFromLexicalNode(startingNode) {
488
+ const node = lexicalUtils.$findMatchingParent(startingNode, n => $isTableCellNode(n));
489
+ if ($isTableCellNode(node)) {
490
+ return node;
491
+ }
492
+ return null;
493
+ }
494
+ function $getTableRowNodeFromTableCellNodeOrThrow(startingNode) {
495
+ const node = lexicalUtils.$findMatchingParent(startingNode, n => $isTableRowNode(n));
496
+ if ($isTableRowNode(node)) {
497
+ return node;
498
+ }
499
+ throw new Error('Expected table cell to be inside of table row.');
500
+ }
501
+ function $getTableNodeFromLexicalNodeOrThrow(startingNode) {
502
+ const node = lexicalUtils.$findMatchingParent(startingNode, n => $isTableNode(n));
503
+ if ($isTableNode(node)) {
504
+ return node;
505
+ }
506
+ throw new Error('Expected table cell to be inside of table.');
507
+ }
508
+ function $getTableRowIndexFromTableCellNode(tableCellNode) {
509
+ const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
510
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableRowNode);
511
+ return tableNode.getChildren().findIndex(n => n.is(tableRowNode));
512
+ }
513
+ function $getTableColumnIndexFromTableCellNode(tableCellNode) {
514
+ const tableRowNode = $getTableRowNodeFromTableCellNodeOrThrow(tableCellNode);
515
+ return tableRowNode.getChildren().findIndex(n => n.is(tableCellNode));
516
+ }
517
+ function $getTableCellSiblingsFromTableCellNode(tableCellNode, table) {
518
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(tableCellNode);
519
+ const {
520
+ x,
521
+ y
522
+ } = tableNode.getCordsFromCellNode(tableCellNode, table);
523
+ return {
524
+ above: tableNode.getCellNodeFromCords(x, y - 1, table),
525
+ below: tableNode.getCellNodeFromCords(x, y + 1, table),
526
+ left: tableNode.getCellNodeFromCords(x - 1, y, table),
527
+ right: tableNode.getCellNodeFromCords(x + 1, y, table)
528
+ };
529
+ }
530
+ function $removeTableRowAtIndex(tableNode, indexToDelete) {
531
+ const tableRows = tableNode.getChildren();
532
+ if (indexToDelete >= tableRows.length || indexToDelete < 0) {
533
+ throw new Error('Expected table cell to be inside of table row.');
534
+ }
535
+ const targetRowNode = tableRows[indexToDelete];
536
+ targetRowNode.remove();
537
+ return tableNode;
538
+ }
539
+
540
+ /**
541
+ * @deprecated This function does not support merged cells. Use {@link $insertTableRowAtSelection} or {@link $insertTableRowAtNode} instead.
542
+ */
543
+ function $insertTableRow(tableNode, targetIndex, shouldInsertAfter = true, rowCount, table) {
544
+ const tableRows = tableNode.getChildren();
545
+ if (targetIndex >= tableRows.length || targetIndex < 0) {
546
+ throw new Error('Table row target index out of range');
547
+ }
548
+ const targetRowNode = tableRows[targetIndex];
549
+ if ($isTableRowNode(targetRowNode)) {
550
+ for (let r = 0; r < rowCount; r++) {
551
+ const tableRowCells = targetRowNode.getChildren();
552
+ const tableColumnCount = tableRowCells.length;
553
+ const newTableRowNode = $createTableRowNode();
554
+ for (let c = 0; c < tableColumnCount; c++) {
555
+ const tableCellFromTargetRow = tableRowCells[c];
556
+ if (!$isTableCellNode(tableCellFromTargetRow)) {
557
+ formatDevErrorMessage(`Expected table cell`);
558
+ }
559
+ const {
560
+ above,
561
+ below
562
+ } = $getTableCellSiblingsFromTableCellNode(tableCellFromTargetRow, table);
563
+ let headerState = TableCellHeaderStates.NO_STATUS;
564
+ const width = above && above.getWidth() || below && below.getWidth() || undefined;
565
+ if (above && above.hasHeaderState(TableCellHeaderStates.COLUMN) || below && below.hasHeaderState(TableCellHeaderStates.COLUMN)) {
566
+ headerState |= TableCellHeaderStates.COLUMN;
567
+ }
568
+ const tableCellNode = $createTableCellNode(headerState, 1, width);
569
+ tableCellNode.append(lexical.$createParagraphNode());
570
+ newTableRowNode.append(tableCellNode);
571
+ }
572
+ if (shouldInsertAfter) {
573
+ targetRowNode.insertAfter(newTableRowNode);
574
+ } else {
575
+ targetRowNode.insertBefore(newTableRowNode);
576
+ }
577
+ }
578
+ } else {
579
+ throw new Error('Row before insertion index does not exist.');
580
+ }
581
+ return tableNode;
582
+ }
583
+ const getHeaderState = (currentState, possibleState) => {
584
+ if (currentState === TableCellHeaderStates.BOTH || currentState === possibleState) {
585
+ return possibleState;
586
+ }
587
+ return TableCellHeaderStates.NO_STATUS;
588
+ };
589
+
590
+ /**
591
+ * Inserts a table row before or after the current focus cell node,
592
+ * taking into account any spans. If successful, returns the
593
+ * inserted table row node.
594
+ */
595
+ function $insertTableRowAtSelection(insertAfter = true) {
596
+ const selection = lexical.$getSelection();
597
+ if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
598
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
599
+ }
600
+ const anchor = selection.anchor.getNode();
601
+ const focus = selection.focus.getNode();
602
+ const [anchorCell] = $getNodeTriplet(anchor);
603
+ const [focusCell,, grid] = $getNodeTriplet(focus);
604
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
605
+ const {
606
+ startRow: anchorStartRow
607
+ } = anchorCellMap;
608
+ const {
609
+ startRow: focusStartRow
610
+ } = focusCellMap;
611
+ if (insertAfter) {
612
+ return $insertTableRowAtNode(anchorStartRow + anchorCell.__rowSpan > focusStartRow + focusCell.__rowSpan ? anchorCell : focusCell, true);
613
+ } else {
614
+ return $insertTableRowAtNode(focusStartRow < anchorStartRow ? focusCell : anchorCell, false);
615
+ }
616
+ }
617
+
618
+ /**
619
+ * @deprecated renamed to {@link $insertTableRowAtSelection}
620
+ */
621
+ const $insertTableRow__EXPERIMENTAL = $insertTableRowAtSelection;
622
+
623
+ /**
624
+ * Inserts a table row before or after the given cell node,
625
+ * taking into account any spans. If successful, returns the
626
+ * inserted table row node.
627
+ */
628
+ function $insertTableRowAtNode(cellNode, insertAfter = true) {
629
+ const [,, grid] = $getNodeTriplet(cellNode);
630
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
631
+ const columnCount = gridMap[0].length;
632
+ const {
633
+ startRow: cellStartRow
634
+ } = cellMap;
635
+ let insertedRow = null;
636
+ if (insertAfter) {
637
+ const insertAfterEndRow = cellStartRow + cellNode.__rowSpan - 1;
638
+ const insertAfterEndRowMap = gridMap[insertAfterEndRow];
639
+ const newRow = $createTableRowNode();
640
+ for (let i = 0; i < columnCount; i++) {
641
+ const {
642
+ cell,
643
+ startRow
644
+ } = insertAfterEndRowMap[i];
645
+ if (startRow + cell.__rowSpan - 1 <= insertAfterEndRow) {
646
+ const currentCell = insertAfterEndRowMap[i].cell;
647
+ const currentCellHeaderState = currentCell.__headerState;
648
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
649
+ newRow.append($createTableCellNode(headerState).append(lexical.$createParagraphNode()));
650
+ } else {
651
+ cell.setRowSpan(cell.__rowSpan + 1);
652
+ }
653
+ }
654
+ const insertAfterEndRowNode = grid.getChildAtIndex(insertAfterEndRow);
655
+ if (!$isTableRowNode(insertAfterEndRowNode)) {
656
+ formatDevErrorMessage(`insertAfterEndRow is not a TableRowNode`);
657
+ }
658
+ insertAfterEndRowNode.insertAfter(newRow);
659
+ insertedRow = newRow;
660
+ } else {
661
+ const insertBeforeStartRow = cellStartRow;
662
+ const insertBeforeStartRowMap = gridMap[insertBeforeStartRow];
663
+ const newRow = $createTableRowNode();
664
+ for (let i = 0; i < columnCount; i++) {
665
+ const {
666
+ cell,
667
+ startRow
668
+ } = insertBeforeStartRowMap[i];
669
+ if (startRow === insertBeforeStartRow) {
670
+ const currentCell = insertBeforeStartRowMap[i].cell;
671
+ const currentCellHeaderState = currentCell.__headerState;
672
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.COLUMN);
673
+ newRow.append($createTableCellNode(headerState).append(lexical.$createParagraphNode()));
674
+ } else {
675
+ cell.setRowSpan(cell.__rowSpan + 1);
676
+ }
677
+ }
678
+ const insertBeforeStartRowNode = grid.getChildAtIndex(insertBeforeStartRow);
679
+ if (!$isTableRowNode(insertBeforeStartRowNode)) {
680
+ formatDevErrorMessage(`insertBeforeStartRow is not a TableRowNode`);
681
+ }
682
+ insertBeforeStartRowNode.insertBefore(newRow);
683
+ insertedRow = newRow;
684
+ }
685
+ return insertedRow;
686
+ }
687
+
688
+ /**
689
+ * @deprecated This function does not support merged cells. Use {@link $insertTableColumnAtSelection} or {@link $insertTableColumnAtNode} instead.
690
+ */
691
+ function $insertTableColumn(tableNode, targetIndex, shouldInsertAfter = true, columnCount, table) {
692
+ const tableRows = tableNode.getChildren();
693
+ const tableCellsToBeInserted = [];
694
+ for (let r = 0; r < tableRows.length; r++) {
695
+ const currentTableRowNode = tableRows[r];
696
+ if ($isTableRowNode(currentTableRowNode)) {
697
+ for (let c = 0; c < columnCount; c++) {
698
+ const tableRowChildren = currentTableRowNode.getChildren();
699
+ if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
700
+ throw new Error('Table column target index out of range');
701
+ }
702
+ const targetCell = tableRowChildren[targetIndex];
703
+ if (!$isTableCellNode(targetCell)) {
704
+ formatDevErrorMessage(`Expected table cell`);
705
+ }
706
+ const {
707
+ left,
708
+ right
709
+ } = $getTableCellSiblingsFromTableCellNode(targetCell, table);
710
+ let headerState = TableCellHeaderStates.NO_STATUS;
711
+ if (left && left.hasHeaderState(TableCellHeaderStates.ROW) || right && right.hasHeaderState(TableCellHeaderStates.ROW)) {
712
+ headerState |= TableCellHeaderStates.ROW;
713
+ }
714
+ const newTableCell = $createTableCellNode(headerState);
715
+ newTableCell.append(lexical.$createParagraphNode());
716
+ tableCellsToBeInserted.push({
717
+ newTableCell,
718
+ targetCell
719
+ });
720
+ }
721
+ }
722
+ }
723
+ tableCellsToBeInserted.forEach(({
724
+ newTableCell,
725
+ targetCell
726
+ }) => {
727
+ if (shouldInsertAfter) {
728
+ targetCell.insertAfter(newTableCell);
729
+ } else {
730
+ targetCell.insertBefore(newTableCell);
731
+ }
732
+ });
733
+ return tableNode;
734
+ }
735
+
736
+ /**
737
+ * Inserts a column before or after the current focus cell node,
738
+ * taking into account any spans. If successful, returns the
739
+ * first inserted cell node.
740
+ */
741
+ function $insertTableColumnAtSelection(insertAfter = true) {
742
+ const selection = lexical.$getSelection();
743
+ if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
744
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
745
+ }
746
+ const anchor = selection.anchor.getNode();
747
+ const focus = selection.focus.getNode();
748
+ const [anchorCell] = $getNodeTriplet(anchor);
749
+ const [focusCell,, grid] = $getNodeTriplet(focus);
750
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
751
+ const {
752
+ startColumn: anchorStartColumn
753
+ } = anchorCellMap;
754
+ const {
755
+ startColumn: focusStartColumn
756
+ } = focusCellMap;
757
+ if (insertAfter) {
758
+ return $insertTableColumnAtNode(anchorStartColumn + anchorCell.__colSpan > focusStartColumn + focusCell.__colSpan ? anchorCell : focusCell, true);
759
+ } else {
760
+ return $insertTableColumnAtNode(focusStartColumn < anchorStartColumn ? focusCell : anchorCell, false);
761
+ }
762
+ }
763
+
764
+ /**
765
+ * @deprecated renamed to {@link $insertTableColumnAtSelection}
766
+ */
767
+ const $insertTableColumn__EXPERIMENTAL = $insertTableColumnAtSelection;
768
+
769
+ /**
770
+ * Inserts a column before or after the given cell node,
771
+ * taking into account any spans. If successful, returns the
772
+ * first inserted cell node.
773
+ */
774
+ function $insertTableColumnAtNode(cellNode, insertAfter = true, shouldSetSelection = true) {
775
+ const [,, grid] = $getNodeTriplet(cellNode);
776
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
777
+ const rowCount = gridMap.length;
778
+ const {
779
+ startColumn
780
+ } = cellMap;
781
+ const insertAfterColumn = insertAfter ? startColumn + cellNode.__colSpan - 1 : startColumn - 1;
782
+ const gridFirstChild = grid.getFirstChild();
783
+ if (!$isTableRowNode(gridFirstChild)) {
784
+ formatDevErrorMessage(`Expected firstTable child to be a row`);
785
+ }
786
+ let firstInsertedCell = null;
787
+ function $createTableCellNodeForInsertTableColumn(headerState = TableCellHeaderStates.NO_STATUS) {
788
+ const cell = $createTableCellNode(headerState).append(lexical.$createParagraphNode());
789
+ if (firstInsertedCell === null) {
790
+ firstInsertedCell = cell;
791
+ }
792
+ return cell;
793
+ }
794
+ let loopRow = gridFirstChild;
795
+ rowLoop: for (let i = 0; i < rowCount; i++) {
796
+ if (i !== 0) {
797
+ const currentRow = loopRow.getNextSibling();
798
+ if (!$isTableRowNode(currentRow)) {
799
+ formatDevErrorMessage(`Expected row nextSibling to be a row`);
800
+ }
801
+ loopRow = currentRow;
802
+ }
803
+ const rowMap = gridMap[i];
804
+ const currentCellHeaderState = rowMap[insertAfterColumn < 0 ? 0 : insertAfterColumn].cell.__headerState;
805
+ const headerState = getHeaderState(currentCellHeaderState, TableCellHeaderStates.ROW);
806
+ if (insertAfterColumn < 0) {
807
+ $insertFirst(loopRow, $createTableCellNodeForInsertTableColumn(headerState));
808
+ continue;
809
+ }
810
+ const {
811
+ cell: currentCell,
812
+ startColumn: currentStartColumn,
813
+ startRow: currentStartRow
814
+ } = rowMap[insertAfterColumn];
815
+ if (currentStartColumn + currentCell.__colSpan - 1 <= insertAfterColumn) {
816
+ let insertAfterCell = currentCell;
817
+ let insertAfterCellRowStart = currentStartRow;
818
+ let prevCellIndex = insertAfterColumn;
819
+ while (insertAfterCellRowStart !== i && insertAfterCell.__rowSpan > 1) {
820
+ prevCellIndex -= currentCell.__colSpan;
821
+ if (prevCellIndex >= 0) {
822
+ const {
823
+ cell: cell_,
824
+ startRow: startRow_
825
+ } = rowMap[prevCellIndex];
826
+ insertAfterCell = cell_;
827
+ insertAfterCellRowStart = startRow_;
828
+ } else {
829
+ loopRow.append($createTableCellNodeForInsertTableColumn(headerState));
830
+ continue rowLoop;
831
+ }
832
+ }
833
+ insertAfterCell.insertAfter($createTableCellNodeForInsertTableColumn(headerState));
834
+ } else {
835
+ currentCell.setColSpan(currentCell.__colSpan + 1);
836
+ }
837
+ }
838
+ if (firstInsertedCell !== null && shouldSetSelection) {
839
+ $moveSelectionToCell(firstInsertedCell);
840
+ }
841
+ const colWidths = grid.getColWidths();
842
+ if (colWidths) {
843
+ const newColWidths = [...colWidths];
844
+ const columnIndex = insertAfterColumn < 0 ? 0 : insertAfterColumn;
845
+ const newWidth = newColWidths[columnIndex];
846
+ newColWidths.splice(columnIndex, 0, newWidth);
847
+ grid.setColWidths(newColWidths);
848
+ }
849
+ return firstInsertedCell;
850
+ }
851
+
852
+ /**
853
+ * @deprecated This function does not support merged cells. Use {@link $deleteTableColumnAtSelection} instead.
854
+ */
855
+ function $deleteTableColumn(tableNode, targetIndex) {
856
+ const tableRows = tableNode.getChildren();
857
+ for (let i = 0; i < tableRows.length; i++) {
858
+ const currentTableRowNode = tableRows[i];
859
+ if ($isTableRowNode(currentTableRowNode)) {
860
+ const tableRowChildren = currentTableRowNode.getChildren();
861
+ if (targetIndex >= tableRowChildren.length || targetIndex < 0) {
862
+ throw new Error('Table column target index out of range');
863
+ }
864
+ tableRowChildren[targetIndex].remove();
865
+ }
866
+ }
867
+ return tableNode;
868
+ }
869
+ function $deleteTableRowAtSelection() {
870
+ const selection = lexical.$getSelection();
871
+ if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
872
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
873
+ }
874
+ const [anchor, focus] = selection.isBackward() ? [selection.focus.getNode(), selection.anchor.getNode()] : [selection.anchor.getNode(), selection.focus.getNode()];
875
+ const [anchorCell,, grid] = $getNodeTriplet(anchor);
876
+ const [focusCell] = $getNodeTriplet(focus);
877
+ const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(grid, anchorCell, focusCell);
878
+ const {
879
+ startRow: anchorStartRow
880
+ } = anchorCellMap;
881
+ const {
882
+ startRow: focusStartRow
883
+ } = focusCellMap;
884
+ const focusEndRow = focusStartRow + focusCell.__rowSpan - 1;
885
+ if (gridMap.length === focusEndRow - anchorStartRow + 1) {
886
+ // Empty grid
887
+ grid.remove();
888
+ return;
889
+ }
890
+ const columnCount = gridMap[0].length;
891
+ const nextRow = gridMap[focusEndRow + 1];
892
+ const nextRowNode = grid.getChildAtIndex(focusEndRow + 1);
893
+ for (let row = focusEndRow; row >= anchorStartRow; row--) {
894
+ for (let column = columnCount - 1; column >= 0; column--) {
895
+ const {
896
+ cell,
897
+ startRow: cellStartRow,
898
+ startColumn: cellStartColumn
899
+ } = gridMap[row][column];
900
+ if (cellStartColumn !== column) {
901
+ // Don't repeat work for the same Cell
902
+ continue;
903
+ }
904
+ // Rows overflowing top or bottom have to be trimmed
905
+ if (cellStartRow < anchorStartRow || cellStartRow + cell.__rowSpan - 1 > focusEndRow) {
906
+ const intersectionStart = Math.max(cellStartRow, anchorStartRow);
907
+ const intersectionEnd = Math.min(cell.__rowSpan + cellStartRow - 1, focusEndRow);
908
+ const overflowRowsCount = intersectionStart <= intersectionEnd ? intersectionEnd - intersectionStart + 1 : 0;
909
+ cell.setRowSpan(cell.__rowSpan - overflowRowsCount);
910
+ }
911
+ // Rows overflowing bottom have to be moved to the next row
912
+ if (cellStartRow >= anchorStartRow && cellStartRow + cell.__rowSpan - 1 > focusEndRow &&
913
+ // Handle overflow only once
914
+ row === focusEndRow) {
915
+ if (!(nextRowNode !== null)) {
916
+ formatDevErrorMessage(`Expected nextRowNode not to be null`);
917
+ }
918
+ let insertAfterCell = null;
919
+ for (let columnIndex = 0; columnIndex < column; columnIndex++) {
920
+ const currentCellMap = nextRow[columnIndex];
921
+ const currentCell = currentCellMap.cell;
922
+ // Checking the cell having startRow as same as nextRow
923
+ if (currentCellMap.startRow === row + 1) {
924
+ insertAfterCell = currentCell;
925
+ }
926
+ if (currentCell.__colSpan > 1) {
927
+ columnIndex += currentCell.__colSpan - 1;
928
+ }
929
+ }
930
+ if (insertAfterCell === null) {
931
+ $insertFirst(nextRowNode, cell);
932
+ } else {
933
+ insertAfterCell.insertAfter(cell);
934
+ }
935
+ }
936
+ }
937
+ const rowNode = grid.getChildAtIndex(row);
938
+ if (!$isTableRowNode(rowNode)) {
939
+ formatDevErrorMessage(`Expected TableNode childAtIndex(${String(row)}) to be RowNode`);
940
+ }
941
+ rowNode.remove();
942
+ }
943
+ if (nextRow !== undefined) {
944
+ const {
945
+ cell
946
+ } = nextRow[0];
947
+ $moveSelectionToCell(cell);
948
+ } else {
949
+ const previousRow = gridMap[anchorStartRow - 1];
950
+ const {
951
+ cell
952
+ } = previousRow[0];
953
+ $moveSelectionToCell(cell);
954
+ }
955
+ }
956
+
957
+ /**
958
+ * @deprecated renamed to {@link $deleteTableRowAtSelection}
959
+ */
960
+ const $deleteTableRow__EXPERIMENTAL = $deleteTableRowAtSelection;
961
+ function $deleteTableColumnAtSelection() {
962
+ const selection = lexical.$getSelection();
963
+ if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
964
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
965
+ }
966
+ const anchor = selection.anchor.getNode();
967
+ const focus = selection.focus.getNode();
968
+ const [anchorCell,, grid] = $getNodeTriplet(anchor);
969
+ const [focusCell] = $getNodeTriplet(focus);
970
+ const [gridMap, anchorCellMap, focusCellMap] = $computeTableMap(grid, anchorCell, focusCell);
971
+ const {
972
+ startColumn: anchorStartColumn
973
+ } = anchorCellMap;
974
+ const {
975
+ startRow: focusStartRow,
976
+ startColumn: focusStartColumn
977
+ } = focusCellMap;
978
+ const startColumn = Math.min(anchorStartColumn, focusStartColumn);
979
+ const endColumn = Math.max(anchorStartColumn + anchorCell.__colSpan - 1, focusStartColumn + focusCell.__colSpan - 1);
980
+ const selectedColumnCount = endColumn - startColumn + 1;
981
+ const columnCount = gridMap[0].length;
982
+ if (columnCount === endColumn - startColumn + 1) {
983
+ // Empty grid
984
+ grid.selectPrevious();
985
+ grid.remove();
986
+ return;
987
+ }
988
+ const rowCount = gridMap.length;
989
+ for (let row = 0; row < rowCount; row++) {
990
+ for (let column = startColumn; column <= endColumn; column++) {
991
+ const {
992
+ cell,
993
+ startColumn: cellStartColumn
994
+ } = gridMap[row][column];
995
+ if (cellStartColumn < startColumn) {
996
+ if (column === startColumn) {
997
+ const overflowLeft = startColumn - cellStartColumn;
998
+ // Overflowing left
999
+ cell.setColSpan(cell.__colSpan -
1000
+ // Possible overflow right too
1001
+ Math.min(selectedColumnCount, cell.__colSpan - overflowLeft));
1002
+ }
1003
+ } else if (cellStartColumn + cell.__colSpan - 1 > endColumn) {
1004
+ if (column === endColumn) {
1005
+ // Overflowing right
1006
+ const inSelectedArea = endColumn - cellStartColumn + 1;
1007
+ cell.setColSpan(cell.__colSpan - inSelectedArea);
1008
+ }
1009
+ } else {
1010
+ cell.remove();
1011
+ }
1012
+ }
1013
+ }
1014
+ const focusRowMap = gridMap[focusStartRow];
1015
+ const nextColumn = anchorStartColumn > focusStartColumn ? focusRowMap[anchorStartColumn + anchorCell.__colSpan] : focusRowMap[focusStartColumn + focusCell.__colSpan];
1016
+ if (nextColumn !== undefined) {
1017
+ const {
1018
+ cell
1019
+ } = nextColumn;
1020
+ $moveSelectionToCell(cell);
1021
+ } else {
1022
+ const previousRow = focusStartColumn < anchorStartColumn ? focusRowMap[focusStartColumn - 1] : focusRowMap[anchorStartColumn - 1];
1023
+ const {
1024
+ cell
1025
+ } = previousRow;
1026
+ $moveSelectionToCell(cell);
1027
+ }
1028
+ const colWidths = grid.getColWidths();
1029
+ if (colWidths) {
1030
+ const newColWidths = [...colWidths];
1031
+ newColWidths.splice(startColumn, selectedColumnCount);
1032
+ grid.setColWidths(newColWidths);
1033
+ }
1034
+ }
1035
+
1036
+ /**
1037
+ * @deprecated renamed to {@link $deleteTableColumnAtSelection}
1038
+ */
1039
+ const $deleteTableColumn__EXPERIMENTAL = $deleteTableColumnAtSelection;
1040
+ function $moveSelectionToCell(cell) {
1041
+ const firstDescendant = cell.getFirstDescendant();
1042
+ if (firstDescendant == null) {
1043
+ cell.selectStart();
1044
+ } else {
1045
+ firstDescendant.getParentOrThrow().selectStart();
1046
+ }
1047
+ }
1048
+ function $insertFirst(parent, node) {
1049
+ const firstChild = parent.getFirstChild();
1050
+ if (firstChild !== null) {
1051
+ firstChild.insertBefore(node);
1052
+ } else {
1053
+ parent.append(node);
1054
+ }
1055
+ }
1056
+ function $mergeCells(cellNodes) {
1057
+ if (cellNodes.length === 0) {
1058
+ return null;
1059
+ }
1060
+
1061
+ // Find the table node
1062
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNodes[0]);
1063
+ const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
1064
+
1065
+ // Find the boundaries of the selection including merged cells
1066
+ let minRow = Infinity;
1067
+ let maxRow = -Infinity;
1068
+ let minCol = Infinity;
1069
+ let maxCol = -Infinity;
1070
+
1071
+ // First pass: find the actual boundaries considering merged cells
1072
+ const processedCells = new Set();
1073
+ for (const row of gridMap) {
1074
+ for (const mapCell of row) {
1075
+ if (!mapCell || !mapCell.cell) {
1076
+ continue;
1077
+ }
1078
+ const cellKey = mapCell.cell.getKey();
1079
+ if (processedCells.has(cellKey)) {
1080
+ continue;
1081
+ }
1082
+ if (cellNodes.some(cell => cell.is(mapCell.cell))) {
1083
+ processedCells.add(cellKey);
1084
+ // Get the actual position of this cell in the grid
1085
+ const cellStartRow = mapCell.startRow;
1086
+ const cellStartCol = mapCell.startColumn;
1087
+ const cellRowSpan = mapCell.cell.__rowSpan || 1;
1088
+ const cellColSpan = mapCell.cell.__colSpan || 1;
1089
+
1090
+ // Update boundaries considering the cell's actual position and span
1091
+ minRow = Math.min(minRow, cellStartRow);
1092
+ maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1);
1093
+ minCol = Math.min(minCol, cellStartCol);
1094
+ maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1);
1095
+ }
1096
+ }
1097
+ }
1098
+
1099
+ // Validate boundaries
1100
+ if (minRow === Infinity || minCol === Infinity) {
1101
+ return null;
1102
+ }
1103
+
1104
+ // The total span of the merged cell
1105
+ const totalRowSpan = maxRow - minRow + 1;
1106
+ const totalColSpan = maxCol - minCol + 1;
1107
+
1108
+ // Use the top-left cell as the target cell
1109
+ const targetCellMap = gridMap[minRow][minCol];
1110
+ if (!targetCellMap.cell) {
1111
+ return null;
1112
+ }
1113
+ const targetCell = targetCellMap.cell;
1114
+
1115
+ // Set the spans for the target cell
1116
+ targetCell.setColSpan(totalColSpan);
1117
+ targetCell.setRowSpan(totalRowSpan);
1118
+
1119
+ // Move content from other cells to the target cell
1120
+ const seenCells = new Set([targetCell.getKey()]);
1121
+
1122
+ // Second pass: merge content and remove other cells
1123
+ for (let row = minRow; row <= maxRow; row++) {
1124
+ for (let col = minCol; col <= maxCol; col++) {
1125
+ const mapCell = gridMap[row][col];
1126
+ if (!mapCell.cell) {
1127
+ continue;
1128
+ }
1129
+ const currentCell = mapCell.cell;
1130
+ const key = currentCell.getKey();
1131
+ if (!seenCells.has(key)) {
1132
+ seenCells.add(key);
1133
+ const isEmpty = $cellContainsEmptyParagraph(currentCell);
1134
+ if (!isEmpty) {
1135
+ targetCell.append(...currentCell.getChildren());
1136
+ }
1137
+ currentCell.remove();
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ // Ensure target cell has content
1143
+ if (targetCell.getChildrenSize() === 0) {
1144
+ targetCell.append(lexical.$createParagraphNode());
1145
+ }
1146
+ return targetCell;
1147
+ }
1148
+ function $cellContainsEmptyParagraph(cell) {
1149
+ if (cell.getChildrenSize() !== 1) {
1150
+ return false;
1151
+ }
1152
+ const firstChild = cell.getFirstChildOrThrow();
1153
+ if (!lexical.$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
1154
+ return false;
1155
+ }
1156
+ return true;
1157
+ }
1158
+ function $unmergeCell() {
1159
+ const selection = lexical.$getSelection();
1160
+ if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
1161
+ formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
1162
+ }
1163
+ const anchor = selection.anchor.getNode();
1164
+ const cellNode = lexicalUtils.$findMatchingParent(anchor, $isTableCellNode);
1165
+ if (!$isTableCellNode(cellNode)) {
1166
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1167
+ }
1168
+ return $unmergeCellNode(cellNode);
1169
+ }
1170
+ function $unmergeCellNode(cellNode) {
1171
+ const [cell, row, grid] = $getNodeTriplet(cellNode);
1172
+ const colSpan = cell.__colSpan;
1173
+ const rowSpan = cell.__rowSpan;
1174
+ if (colSpan === 1 && rowSpan === 1) {
1175
+ return;
1176
+ }
1177
+ const [map, cellMap] = $computeTableMap(grid, cell, cell);
1178
+ const {
1179
+ startColumn,
1180
+ startRow
1181
+ } = cellMap;
1182
+ // Create a heuristic for what the style of the unmerged cells should be
1183
+ // based on whether every row or column already had that state before the
1184
+ // unmerge.
1185
+ const baseColStyle = cell.__headerState & TableCellHeaderStates.COLUMN;
1186
+ const colStyles = Array.from({
1187
+ length: colSpan
1188
+ }, (_v, i) => {
1189
+ let colStyle = baseColStyle;
1190
+ for (let rowIdx = 0; colStyle !== 0 && rowIdx < map.length; rowIdx++) {
1191
+ colStyle &= map[rowIdx][i + startColumn].cell.__headerState;
1192
+ }
1193
+ return colStyle;
1194
+ });
1195
+ const baseRowStyle = cell.__headerState & TableCellHeaderStates.ROW;
1196
+ const rowStyles = Array.from({
1197
+ length: rowSpan
1198
+ }, (_v, i) => {
1199
+ let rowStyle = baseRowStyle;
1200
+ for (let colIdx = 0; rowStyle !== 0 && colIdx < map[0].length; colIdx++) {
1201
+ rowStyle &= map[i + startRow][colIdx].cell.__headerState;
1202
+ }
1203
+ return rowStyle;
1204
+ });
1205
+ if (colSpan > 1) {
1206
+ for (let i = 1; i < colSpan; i++) {
1207
+ cell.insertAfter($createTableCellNode(colStyles[i] | rowStyles[0]).append(lexical.$createParagraphNode()));
1208
+ }
1209
+ cell.setColSpan(1);
1210
+ }
1211
+ if (rowSpan > 1) {
1212
+ let currentRowNode;
1213
+ for (let i = 1; i < rowSpan; i++) {
1214
+ const currentRow = startRow + i;
1215
+ const currentRowMap = map[currentRow];
1216
+ currentRowNode = (currentRowNode || row).getNextSibling();
1217
+ if (!$isTableRowNode(currentRowNode)) {
1218
+ formatDevErrorMessage(`Expected row next sibling to be a row`);
1219
+ }
1220
+ let insertAfterCell = null;
1221
+ for (let column = 0; column < startColumn; column++) {
1222
+ const currentCellMap = currentRowMap[column];
1223
+ const currentCell = currentCellMap.cell;
1224
+ if (currentCellMap.startRow === currentRow) {
1225
+ insertAfterCell = currentCell;
1226
+ }
1227
+ if (currentCell.__colSpan > 1) {
1228
+ column += currentCell.__colSpan - 1;
1229
+ }
1230
+ }
1231
+ if (insertAfterCell === null) {
1232
+ for (let j = colSpan - 1; j >= 0; j--) {
1233
+ $insertFirst(currentRowNode, $createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
1234
+ }
1235
+ } else {
1236
+ for (let j = colSpan - 1; j >= 0; j--) {
1237
+ insertAfterCell.insertAfter($createTableCellNode(colStyles[j] | rowStyles[i]).append(lexical.$createParagraphNode()));
1238
+ }
1239
+ }
1240
+ }
1241
+ cell.setRowSpan(1);
1242
+ }
1243
+ }
1244
+ function $computeTableMap(tableNode, cellA, cellB) {
1245
+ const [tableMap, cellAValue, cellBValue] = $computeTableMapSkipCellCheck(tableNode, cellA, cellB);
1246
+ if (!(cellAValue !== null)) {
1247
+ formatDevErrorMessage(`Anchor not found in Table`);
1248
+ }
1249
+ if (!(cellBValue !== null)) {
1250
+ formatDevErrorMessage(`Focus not found in Table`);
1251
+ }
1252
+ return [tableMap, cellAValue, cellBValue];
1253
+ }
1254
+ function $computeTableMapSkipCellCheck(tableNode, cellA, cellB) {
1255
+ const tableMap = [];
1256
+ let cellAValue = null;
1257
+ let cellBValue = null;
1258
+ function getMapRow(i) {
1259
+ let row = tableMap[i];
1260
+ if (row === undefined) {
1261
+ tableMap[i] = row = [];
1262
+ }
1263
+ return row;
1264
+ }
1265
+ const gridChildren = tableNode.getChildren();
1266
+ for (let rowIdx = 0; rowIdx < gridChildren.length; rowIdx++) {
1267
+ const row = gridChildren[rowIdx];
1268
+ if (!$isTableRowNode(row)) {
1269
+ formatDevErrorMessage(`Expected TableNode children to be TableRowNode`);
1270
+ }
1271
+ const startMapRow = getMapRow(rowIdx);
1272
+ for (let cell = row.getFirstChild(), colIdx = 0; cell != null; cell = cell.getNextSibling()) {
1273
+ if (!$isTableCellNode(cell)) {
1274
+ formatDevErrorMessage(`Expected TableRowNode children to be TableCellNode`);
1275
+ } // Skip past any columns that were merged from a higher row
1276
+ while (startMapRow[colIdx] !== undefined) {
1277
+ colIdx++;
1278
+ }
1279
+ const value = {
1280
+ cell,
1281
+ startColumn: colIdx,
1282
+ startRow: rowIdx
1283
+ };
1284
+ const {
1285
+ __rowSpan: rowSpan,
1286
+ __colSpan: colSpan
1287
+ } = cell;
1288
+ for (let j = 0; j < rowSpan; j++) {
1289
+ if (rowIdx + j >= gridChildren.length) {
1290
+ // The table is non-rectangular with a rowSpan
1291
+ // below the last <tr> in the table.
1292
+ // We should probably handle this with a node transform
1293
+ // to ensure that tables are always rectangular but this
1294
+ // will avoid crashes such as #6584
1295
+ // Note that there are probably still latent bugs
1296
+ // regarding colSpan or general cell count mismatches.
1297
+ break;
1298
+ }
1299
+ const mapRow = getMapRow(rowIdx + j);
1300
+ for (let i = 0; i < colSpan; i++) {
1301
+ mapRow[colIdx + i] = value;
1302
+ }
1303
+ }
1304
+ if (cellA !== null && cellAValue === null && cellA.is(cell)) {
1305
+ cellAValue = value;
1306
+ }
1307
+ if (cellB !== null && cellBValue === null && cellB.is(cell)) {
1308
+ cellBValue = value;
1309
+ }
1310
+ }
1311
+ }
1312
+ return [tableMap, cellAValue, cellBValue];
1313
+ }
1314
+ function $getNodeTriplet(source) {
1315
+ let cell;
1316
+ if (source instanceof TableCellNode) {
1317
+ cell = source;
1318
+ } else if ('__type' in source) {
1319
+ const cell_ = lexicalUtils.$findMatchingParent(source, $isTableCellNode);
1320
+ if (!$isTableCellNode(cell_)) {
1321
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1322
+ }
1323
+ cell = cell_;
1324
+ } else {
1325
+ const cell_ = lexicalUtils.$findMatchingParent(source.getNode(), $isTableCellNode);
1326
+ if (!$isTableCellNode(cell_)) {
1327
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1328
+ }
1329
+ cell = cell_;
1330
+ }
1331
+ const row = cell.getParent();
1332
+ if (!$isTableRowNode(row)) {
1333
+ formatDevErrorMessage(`Expected TableCellNode to have a parent TableRowNode`);
1334
+ }
1335
+ const grid = row.getParent();
1336
+ if (!$isTableNode(grid)) {
1337
+ formatDevErrorMessage(`Expected TableRowNode to have a parent TableNode`);
1338
+ }
1339
+ return [cell, row, grid];
1340
+ }
1341
+ function $computeTableCellRectSpans(map, boundary) {
1342
+ const {
1343
+ minColumn,
1344
+ maxColumn,
1345
+ minRow,
1346
+ maxRow
1347
+ } = boundary;
1348
+ let topSpan = 1;
1349
+ let leftSpan = 1;
1350
+ let rightSpan = 1;
1351
+ let bottomSpan = 1;
1352
+ const topRow = map[minRow];
1353
+ const bottomRow = map[maxRow];
1354
+ for (let col = minColumn; col <= maxColumn; col++) {
1355
+ topSpan = Math.max(topSpan, topRow[col].cell.__rowSpan);
1356
+ bottomSpan = Math.max(bottomSpan, bottomRow[col].cell.__rowSpan);
1357
+ }
1358
+ for (let row = minRow; row <= maxRow; row++) {
1359
+ leftSpan = Math.max(leftSpan, map[row][minColumn].cell.__colSpan);
1360
+ rightSpan = Math.max(rightSpan, map[row][maxColumn].cell.__colSpan);
1361
+ }
1362
+ return {
1363
+ bottomSpan,
1364
+ leftSpan,
1365
+ rightSpan,
1366
+ topSpan
1367
+ };
1368
+ }
1369
+ function $computeTableCellRectBoundary(map, cellAMap, cellBMap) {
1370
+ // Initial boundaries based on the anchor and focus cells
1371
+ let minColumn = Math.min(cellAMap.startColumn, cellBMap.startColumn);
1372
+ let minRow = Math.min(cellAMap.startRow, cellBMap.startRow);
1373
+ let maxColumn = Math.max(cellAMap.startColumn + cellAMap.cell.__colSpan - 1, cellBMap.startColumn + cellBMap.cell.__colSpan - 1);
1374
+ let maxRow = Math.max(cellAMap.startRow + cellAMap.cell.__rowSpan - 1, cellBMap.startRow + cellBMap.cell.__rowSpan - 1);
1375
+
1376
+ // Keep expanding until we have a complete rectangle
1377
+ let hasChanges;
1378
+ do {
1379
+ hasChanges = false;
1380
+
1381
+ // Check all cells in the table
1382
+ for (let row = 0; row < map.length; row++) {
1383
+ for (let col = 0; col < map[0].length; col++) {
1384
+ const cell = map[row][col];
1385
+ if (!cell) {
1386
+ continue;
1387
+ }
1388
+ const cellEndCol = cell.startColumn + cell.cell.__colSpan - 1;
1389
+ const cellEndRow = cell.startRow + cell.cell.__rowSpan - 1;
1390
+
1391
+ // Check if this cell intersects with our current selection rectangle
1392
+ const intersectsHorizontally = cell.startColumn <= maxColumn && cellEndCol >= minColumn;
1393
+ const intersectsVertically = cell.startRow <= maxRow && cellEndRow >= minRow;
1394
+
1395
+ // If the cell intersects either horizontally or vertically
1396
+ if (intersectsHorizontally && intersectsVertically) {
1397
+ // Expand boundaries to include this cell completely
1398
+ const newMinColumn = Math.min(minColumn, cell.startColumn);
1399
+ const newMaxColumn = Math.max(maxColumn, cellEndCol);
1400
+ const newMinRow = Math.min(minRow, cell.startRow);
1401
+ const newMaxRow = Math.max(maxRow, cellEndRow);
1402
+
1403
+ // Check if boundaries changed
1404
+ if (newMinColumn !== minColumn || newMaxColumn !== maxColumn || newMinRow !== minRow || newMaxRow !== maxRow) {
1405
+ minColumn = newMinColumn;
1406
+ maxColumn = newMaxColumn;
1407
+ minRow = newMinRow;
1408
+ maxRow = newMaxRow;
1409
+ hasChanges = true;
1410
+ }
1411
+ }
1412
+ }
1413
+ }
1414
+ } while (hasChanges);
1415
+ return {
1416
+ maxColumn,
1417
+ maxRow,
1418
+ minColumn,
1419
+ minRow
1420
+ };
1421
+ }
1422
+ function $getTableCellNodeRect(tableCellNode) {
1423
+ const [cellNode,, gridNode] = $getNodeTriplet(tableCellNode);
1424
+ const rows = gridNode.getChildren();
1425
+ const rowCount = rows.length;
1426
+ const columnCount = rows[0].getChildren().length;
1427
+
1428
+ // Create a matrix of the same size as the table to track the position of each cell
1429
+ const cellMatrix = new Array(rowCount);
1430
+ for (let i = 0; i < rowCount; i++) {
1431
+ cellMatrix[i] = new Array(columnCount);
1432
+ }
1433
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
1434
+ const row = rows[rowIndex];
1435
+ const cells = row.getChildren();
1436
+ let columnIndex = 0;
1437
+ for (let cellIndex = 0; cellIndex < cells.length; cellIndex++) {
1438
+ // Find the next available position in the matrix, skip the position of merged cells
1439
+ while (cellMatrix[rowIndex][columnIndex]) {
1440
+ columnIndex++;
1441
+ }
1442
+ const cell = cells[cellIndex];
1443
+ const rowSpan = cell.__rowSpan || 1;
1444
+ const colSpan = cell.__colSpan || 1;
1445
+
1446
+ // Put the cell into the corresponding position in the matrix
1447
+ for (let i = 0; i < rowSpan; i++) {
1448
+ for (let j = 0; j < colSpan; j++) {
1449
+ cellMatrix[rowIndex + i][columnIndex + j] = cell;
1450
+ }
1451
+ }
1452
+
1453
+ // Return to the original index, row span and column span of the cell.
1454
+ if (cellNode === cell) {
1455
+ return {
1456
+ colSpan,
1457
+ columnIndex,
1458
+ rowIndex,
1459
+ rowSpan
1460
+ };
1461
+ }
1462
+ columnIndex += colSpan;
1463
+ }
1464
+ }
1465
+ return null;
1466
+ }
1467
+
1468
+ function $getCellNodes(tableSelection) {
1469
+ const [[anchorNode, anchorCell, anchorRow, anchorTable], [focusNode, focusCell, focusRow, focusTable]] = ['anchor', 'focus'].map(k => {
1470
+ const node = tableSelection[k].getNode();
1471
+ const cellNode = lexicalUtils.$findMatchingParent(node, $isTableCellNode);
1472
+ if (!$isTableCellNode(cellNode)) {
1473
+ formatDevErrorMessage(`Expected TableSelection ${k} to be (or a child of) TableCellNode, got key ${node.getKey()} of type ${node.getType()}`);
1474
+ }
1475
+ const rowNode = cellNode.getParent();
1476
+ if (!$isTableRowNode(rowNode)) {
1477
+ formatDevErrorMessage(`Expected TableSelection ${k} cell parent to be a TableRowNode`);
1478
+ }
1479
+ const tableNode = rowNode.getParent();
1480
+ if (!$isTableNode(tableNode)) {
1481
+ formatDevErrorMessage(`Expected TableSelection ${k} row parent to be a TableNode`);
1482
+ }
1483
+ return [node, cellNode, rowNode, tableNode];
1484
+ });
1485
+ // TODO: nested tables may violate this
1486
+ if (!anchorTable.is(focusTable)) {
1487
+ formatDevErrorMessage(`Expected TableSelection anchor and focus to be in the same table`);
1488
+ }
1489
+ return {
1490
+ anchorCell,
1491
+ anchorNode,
1492
+ anchorRow,
1493
+ anchorTable,
1494
+ focusCell,
1495
+ focusNode,
1496
+ focusRow,
1497
+ focusTable
1498
+ };
1499
+ }
1500
+ class TableSelection {
1501
+ tableKey;
1502
+ anchor;
1503
+ focus;
1504
+ _cachedNodes;
1505
+ dirty;
1506
+ constructor(tableKey, anchor, focus) {
1507
+ this.anchor = anchor;
1508
+ this.focus = focus;
1509
+ anchor._selection = this;
1510
+ focus._selection = this;
1511
+ this._cachedNodes = null;
1512
+ this.dirty = false;
1513
+ this.tableKey = tableKey;
1514
+ }
1515
+ getStartEndPoints() {
1516
+ return [this.anchor, this.focus];
1517
+ }
1518
+
1519
+ /**
1520
+ * {@link $createTableSelection} unfortunately makes it very easy to create
1521
+ * nonsense selections, so we have a method to see if the selection probably
1522
+ * makes sense.
1523
+ *
1524
+ * @returns true if the TableSelection is (probably) valid
1525
+ */
1526
+ isValid() {
1527
+ if (this.tableKey === 'root' || this.anchor.key === 'root' || this.anchor.type !== 'element' || this.focus.key === 'root' || this.focus.type !== 'element') {
1528
+ return false;
1529
+ }
1530
+
1531
+ // Check if the referenced nodes still exist in the editor
1532
+ const tableNode = lexical.$getNodeByKey(this.tableKey);
1533
+ const anchorNode = lexical.$getNodeByKey(this.anchor.key);
1534
+ const focusNode = lexical.$getNodeByKey(this.focus.key);
1535
+ return tableNode !== null && anchorNode !== null && focusNode !== null;
1536
+ }
1537
+
1538
+ /**
1539
+ * Returns whether the Selection is "backwards", meaning the focus
1540
+ * logically precedes the anchor in the EditorState.
1541
+ * @returns true if the Selection is backwards, false otherwise.
1542
+ */
1543
+ isBackward() {
1544
+ return this.focus.isBefore(this.anchor);
1545
+ }
1546
+ getCachedNodes() {
1547
+ return this._cachedNodes;
1548
+ }
1549
+ setCachedNodes(nodes) {
1550
+ this._cachedNodes = nodes;
1551
+ }
1552
+ is(selection) {
1553
+ return $isTableSelection(selection) && this.tableKey === selection.tableKey && this.anchor.is(selection.anchor) && this.focus.is(selection.focus);
1554
+ }
1555
+ set(tableKey, anchorCellKey, focusCellKey) {
1556
+ // note: closure compiler's acorn does not support ||=
1557
+ this.dirty = this.dirty || tableKey !== this.tableKey || anchorCellKey !== this.anchor.key || focusCellKey !== this.focus.key;
1558
+ this.tableKey = tableKey;
1559
+ this.anchor.key = anchorCellKey;
1560
+ this.focus.key = focusCellKey;
1561
+ this._cachedNodes = null;
1562
+ }
1563
+ clone() {
1564
+ return new TableSelection(this.tableKey, lexical.$createPoint(this.anchor.key, this.anchor.offset, this.anchor.type), lexical.$createPoint(this.focus.key, this.focus.offset, this.focus.type));
1565
+ }
1566
+ isCollapsed() {
1567
+ return false;
1568
+ }
1569
+ extract() {
1570
+ return this.getNodes();
1571
+ }
1572
+ insertRawText(text) {
1573
+ // Do nothing?
1574
+ }
1575
+ insertText() {
1576
+ // Do nothing?
1577
+ }
1578
+
1579
+ /**
1580
+ * Returns whether the provided TextFormatType is present on the Selection.
1581
+ * This will be true if any paragraph in table cells has the specified format.
1582
+ *
1583
+ * @param type the TextFormatType to check for.
1584
+ * @returns true if the provided format is currently toggled on on the Selection, false otherwise.
1585
+ */
1586
+ hasFormat(type) {
1587
+ let format = 0;
1588
+ const cellNodes = this.getNodes().filter($isTableCellNode);
1589
+ cellNodes.forEach(cellNode => {
1590
+ const paragraph = cellNode.getFirstChild();
1591
+ if (lexical.$isParagraphNode(paragraph)) {
1592
+ format |= paragraph.getTextFormat();
1593
+ }
1594
+ });
1595
+ const formatFlag = lexical.TEXT_TYPE_TO_FORMAT[type];
1596
+ return (format & formatFlag) !== 0;
1597
+ }
1598
+ insertNodes(nodes) {
1599
+ const focusNode = this.focus.getNode();
1600
+ if (!lexical.$isElementNode(focusNode)) {
1601
+ formatDevErrorMessage(`Expected TableSelection focus to be an ElementNode`);
1602
+ }
1603
+ const selection = lexical.$normalizeSelection__EXPERIMENTAL(focusNode.select(0, focusNode.getChildrenSize()));
1604
+ selection.insertNodes(nodes);
1605
+ }
1606
+
1607
+ // TODO Deprecate this method. It's confusing when used with colspan|rowspan
1608
+ getShape() {
1609
+ const {
1610
+ anchorCell,
1611
+ focusCell
1612
+ } = $getCellNodes(this);
1613
+ const anchorCellNodeRect = $getTableCellNodeRect(anchorCell);
1614
+ if (!(anchorCellNodeRect !== null)) {
1615
+ formatDevErrorMessage(`getCellRect: expected to find AnchorNode`);
1616
+ }
1617
+ const focusCellNodeRect = $getTableCellNodeRect(focusCell);
1618
+ if (!(focusCellNodeRect !== null)) {
1619
+ formatDevErrorMessage(`getCellRect: expected to find focusCellNode`);
1620
+ }
1621
+ const startX = Math.min(anchorCellNodeRect.columnIndex, focusCellNodeRect.columnIndex);
1622
+ const stopX = Math.max(anchorCellNodeRect.columnIndex + anchorCellNodeRect.colSpan - 1, focusCellNodeRect.columnIndex + focusCellNodeRect.colSpan - 1);
1623
+ const startY = Math.min(anchorCellNodeRect.rowIndex, focusCellNodeRect.rowIndex);
1624
+ const stopY = Math.max(anchorCellNodeRect.rowIndex + anchorCellNodeRect.rowSpan - 1, focusCellNodeRect.rowIndex + focusCellNodeRect.rowSpan - 1);
1625
+ return {
1626
+ fromX: Math.min(startX, stopX),
1627
+ fromY: Math.min(startY, stopY),
1628
+ toX: Math.max(startX, stopX),
1629
+ toY: Math.max(startY, stopY)
1630
+ };
1631
+ }
1632
+ getNodes() {
1633
+ if (!this.isValid()) {
1634
+ return [];
1635
+ }
1636
+ const cachedNodes = this._cachedNodes;
1637
+ if (cachedNodes !== null) {
1638
+ return cachedNodes;
1639
+ }
1640
+ const {
1641
+ anchorTable: tableNode,
1642
+ anchorCell,
1643
+ focusCell
1644
+ } = $getCellNodes(this);
1645
+ const focusCellGrid = focusCell.getParents()[1];
1646
+ if (focusCellGrid !== tableNode) {
1647
+ if (!tableNode.isParentOf(focusCell)) {
1648
+ // focus is on higher Grid level than anchor
1649
+ const gridParent = tableNode.getParent();
1650
+ if (!(gridParent != null)) {
1651
+ formatDevErrorMessage(`Expected gridParent to have a parent`);
1652
+ }
1653
+ this.set(this.tableKey, gridParent.getKey(), focusCell.getKey());
1654
+ } else {
1655
+ // anchor is on higher Grid level than focus
1656
+ const focusCellParent = focusCellGrid.getParent();
1657
+ if (!(focusCellParent != null)) {
1658
+ formatDevErrorMessage(`Expected focusCellParent to have a parent`);
1659
+ }
1660
+ this.set(this.tableKey, focusCell.getKey(), focusCellParent.getKey());
1661
+ }
1662
+ return this.getNodes();
1663
+ }
1664
+
1665
+ // TODO Mapping the whole Grid every time not efficient. We need to compute the entire state only
1666
+ // once (on load) and iterate on it as updates occur. However, to do this we need to have the
1667
+ // ability to store a state. Killing TableSelection and moving the logic to the plugin would make
1668
+ // this possible.
1669
+ const [map, cellAMap, cellBMap] = $computeTableMap(tableNode, anchorCell, focusCell);
1670
+ const {
1671
+ minColumn,
1672
+ maxColumn,
1673
+ minRow,
1674
+ maxRow
1675
+ } = $computeTableCellRectBoundary(map, cellAMap, cellBMap);
1676
+
1677
+ // We use a Map here because merged cells in the grid would otherwise
1678
+ // show up multiple times in the nodes array
1679
+ const nodeMap = new Map([[tableNode.getKey(), tableNode]]);
1680
+ let lastRow = null;
1681
+ for (let i = minRow; i <= maxRow; i++) {
1682
+ for (let j = minColumn; j <= maxColumn; j++) {
1683
+ const {
1684
+ cell
1685
+ } = map[i][j];
1686
+ const currentRow = cell.getParent();
1687
+ if (!$isTableRowNode(currentRow)) {
1688
+ formatDevErrorMessage(`Expected TableCellNode parent to be a TableRowNode`);
1689
+ }
1690
+ if (currentRow !== lastRow) {
1691
+ nodeMap.set(currentRow.getKey(), currentRow);
1692
+ lastRow = currentRow;
1693
+ }
1694
+ if (!nodeMap.has(cell.getKey())) {
1695
+ $visitRecursively(cell, childNode => {
1696
+ nodeMap.set(childNode.getKey(), childNode);
1697
+ });
1698
+ }
1699
+ }
1700
+ }
1701
+ const nodes = Array.from(nodeMap.values());
1702
+ if (!lexical.isCurrentlyReadOnlyMode()) {
1703
+ this._cachedNodes = nodes;
1704
+ }
1705
+ return nodes;
1706
+ }
1707
+ getTextContent() {
1708
+ const nodes = this.getNodes().filter(node => $isTableCellNode(node));
1709
+ let textContent = '';
1710
+ for (let i = 0; i < nodes.length; i++) {
1711
+ const node = nodes[i];
1712
+ const row = node.__parent;
1713
+ const nextRow = (nodes[i + 1] || {}).__parent;
1714
+ textContent += node.getTextContent() + (nextRow !== row ? '\n' : '\t');
1715
+ }
1716
+ return textContent;
1717
+ }
1718
+ }
1719
+ function $isTableSelection(x) {
1720
+ return x instanceof TableSelection;
1721
+ }
1722
+ function $createTableSelection() {
1723
+ // TODO this is a suboptimal design, it doesn't make sense to have
1724
+ // a table selection that isn't associated with a table. This
1725
+ // constructor should have required arguments and in true we
1726
+ // should check that they point to a table and are element points to
1727
+ // cell nodes of that table.
1728
+ const anchor = lexical.$createPoint('root', 0, 'element');
1729
+ const focus = lexical.$createPoint('root', 0, 'element');
1730
+ return new TableSelection('root', anchor, focus);
1731
+ }
1732
+ function $createTableSelectionFrom(tableNode, anchorCell, focusCell) {
1733
+ const tableNodeKey = tableNode.getKey();
1734
+ const anchorCellKey = anchorCell.getKey();
1735
+ const focusCellKey = focusCell.getKey();
1736
+ {
1737
+ if (!tableNode.isAttached()) {
1738
+ formatDevErrorMessage(`$createTableSelectionFrom: tableNode ${tableNodeKey} is not attached`);
1739
+ }
1740
+ if (!tableNode.is($findTableNode(anchorCell))) {
1741
+ formatDevErrorMessage(`$createTableSelectionFrom: anchorCell ${anchorCellKey} is not in table ${tableNodeKey}`);
1742
+ }
1743
+ if (!tableNode.is($findTableNode(focusCell))) {
1744
+ formatDevErrorMessage(`$createTableSelectionFrom: focusCell ${focusCellKey} is not in table ${tableNodeKey}`);
1745
+ } // TODO: Check for rectangular grid
1746
+ }
1747
+ const prevSelection = lexical.$getSelection();
1748
+ const nextSelection = $isTableSelection(prevSelection) ? prevSelection.clone() : $createTableSelection();
1749
+ nextSelection.set(tableNode.getKey(), anchorCell.getKey(), focusCell.getKey());
1750
+ return nextSelection;
1751
+ }
1752
+
1753
+ /**
1754
+ * Depth first visitor
1755
+ * @param node The starting node
1756
+ * @param $visit The function to call for each node. If the function returns false, then children of this node will not be explored
1757
+ */
1758
+ function $visitRecursively(node, $visit) {
1759
+ const stack = [[node]];
1760
+ for (let currentArray = stack.at(-1); currentArray !== undefined && stack.length > 0; currentArray = stack.at(-1)) {
1761
+ const currentNode = currentArray.pop();
1762
+ if (currentNode === undefined) {
1763
+ stack.pop();
1764
+ } else if ($visit(currentNode) !== false && lexical.$isElementNode(currentNode)) {
1765
+ stack.push(currentNode.getChildren());
1766
+ }
1767
+ }
1768
+ }
1769
+
1770
+ function $getTableAndElementByKey(tableNodeKey, editor = lexical.$getEditor()) {
1771
+ const tableNode = lexical.$getNodeByKey(tableNodeKey);
1772
+ if (!$isTableNode(tableNode)) {
1773
+ formatDevErrorMessage(`TableObserver: Expected tableNodeKey ${tableNodeKey} to be a TableNode`);
1774
+ }
1775
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNodeKey));
1776
+ if (!(tableElement !== null)) {
1777
+ formatDevErrorMessage(`TableObserver: Expected to find TableElement in DOM for key ${tableNodeKey}`);
1778
+ }
1779
+ return {
1780
+ tableElement,
1781
+ tableNode
1782
+ };
1783
+ }
1784
+ class TableObserver {
1785
+ focusX;
1786
+ focusY;
1787
+ listenersToRemove;
1788
+ table;
1789
+ isHighlightingCells;
1790
+ anchorX;
1791
+ anchorY;
1792
+ tableNodeKey;
1793
+ anchorCell;
1794
+ focusCell;
1795
+ anchorCellNodeKey;
1796
+ focusCellNodeKey;
1797
+ editor;
1798
+ tableSelection;
1799
+ hasHijackedSelectionStyles;
1800
+ isSelecting;
1801
+ pointerType;
1802
+ shouldCheckSelection;
1803
+ abortController;
1804
+ listenerOptions;
1805
+ nextFocus;
1806
+ constructor(editor, tableNodeKey) {
1807
+ this.isHighlightingCells = false;
1808
+ this.anchorX = -1;
1809
+ this.anchorY = -1;
1810
+ this.focusX = -1;
1811
+ this.focusY = -1;
1812
+ this.listenersToRemove = new Set();
1813
+ this.tableNodeKey = tableNodeKey;
1814
+ this.editor = editor;
1815
+ this.table = {
1816
+ columns: 0,
1817
+ domRows: [],
1818
+ rows: 0
1819
+ };
1820
+ this.tableSelection = null;
1821
+ this.anchorCellNodeKey = null;
1822
+ this.focusCellNodeKey = null;
1823
+ this.anchorCell = null;
1824
+ this.focusCell = null;
1825
+ this.hasHijackedSelectionStyles = false;
1826
+ this.isSelecting = false;
1827
+ this.pointerType = null;
1828
+ this.shouldCheckSelection = false;
1829
+ this.abortController = new AbortController();
1830
+ this.listenerOptions = {
1831
+ signal: this.abortController.signal
1832
+ };
1833
+ this.nextFocus = null;
1834
+ this.trackTable();
1835
+ }
1836
+ getTable() {
1837
+ return this.table;
1838
+ }
1839
+ removeListeners() {
1840
+ this.abortController.abort('removeListeners');
1841
+ Array.from(this.listenersToRemove).forEach(removeListener => removeListener());
1842
+ this.listenersToRemove.clear();
1843
+ }
1844
+ $lookup() {
1845
+ return $getTableAndElementByKey(this.tableNodeKey, this.editor);
1846
+ }
1847
+ trackTable() {
1848
+ const observer = new MutationObserver(records => {
1849
+ this.editor.getEditorState().read(() => {
1850
+ let gridNeedsRedraw = false;
1851
+ for (let i = 0; i < records.length; i++) {
1852
+ const record = records[i];
1853
+ const target = record.target;
1854
+ const nodeName = target.nodeName;
1855
+ if (nodeName === 'TABLE' || nodeName === 'TBODY' || nodeName === 'THEAD' || nodeName === 'TR') {
1856
+ gridNeedsRedraw = true;
1857
+ break;
1858
+ }
1859
+ }
1860
+ if (!gridNeedsRedraw) {
1861
+ return;
1862
+ }
1863
+ const {
1864
+ tableNode,
1865
+ tableElement
1866
+ } = this.$lookup();
1867
+ this.table = getTable(tableNode, tableElement);
1868
+ }, {
1869
+ editor: this.editor
1870
+ });
1871
+ });
1872
+ this.editor.getEditorState().read(() => {
1873
+ const {
1874
+ tableNode,
1875
+ tableElement
1876
+ } = this.$lookup();
1877
+ this.table = getTable(tableNode, tableElement);
1878
+ observer.observe(tableElement, {
1879
+ attributes: true,
1880
+ childList: true,
1881
+ subtree: true
1882
+ });
1883
+ }, {
1884
+ editor: this.editor
1885
+ });
1886
+ }
1887
+ $clearHighlight() {
1888
+ const editor = this.editor;
1889
+ this.isHighlightingCells = false;
1890
+ this.anchorX = -1;
1891
+ this.anchorY = -1;
1892
+ this.focusX = -1;
1893
+ this.focusY = -1;
1894
+ this.tableSelection = null;
1895
+ this.anchorCellNodeKey = null;
1896
+ this.focusCellNodeKey = null;
1897
+ this.anchorCell = null;
1898
+ this.focusCell = null;
1899
+ this.hasHijackedSelectionStyles = false;
1900
+ this.$enableHighlightStyle();
1901
+ const {
1902
+ tableNode,
1903
+ tableElement
1904
+ } = this.$lookup();
1905
+ const grid = getTable(tableNode, tableElement);
1906
+ $updateDOMForSelection(editor, grid, null);
1907
+ if (lexical.$getSelection() !== null) {
1908
+ lexical.$setSelection(null);
1909
+ editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
1910
+ }
1911
+ }
1912
+ $enableHighlightStyle() {
1913
+ const editor = this.editor;
1914
+ const {
1915
+ tableElement
1916
+ } = this.$lookup();
1917
+ lexicalUtils.removeClassNamesFromElement(tableElement, editor._config.theme.tableSelection);
1918
+ tableElement.classList.remove('disable-selection');
1919
+ this.hasHijackedSelectionStyles = false;
1920
+ }
1921
+ $disableHighlightStyle() {
1922
+ const {
1923
+ tableElement
1924
+ } = this.$lookup();
1925
+ lexicalUtils.addClassNamesToElement(tableElement, this.editor._config.theme.tableSelection);
1926
+ this.hasHijackedSelectionStyles = true;
1927
+ }
1928
+ $updateTableTableSelection(selection) {
1929
+ if (selection !== null) {
1930
+ if (!(selection.tableKey === this.tableNodeKey)) {
1931
+ formatDevErrorMessage(`TableObserver.$updateTableTableSelection: selection.tableKey !== this.tableNodeKey ('${selection.tableKey}' !== '${this.tableNodeKey}')`);
1932
+ }
1933
+ const editor = this.editor;
1934
+ this.tableSelection = selection;
1935
+ this.isHighlightingCells = true;
1936
+ this.$disableHighlightStyle();
1937
+ this.updateDOMSelection();
1938
+ $updateDOMForSelection(editor, this.table, this.tableSelection);
1939
+ } else {
1940
+ this.$clearHighlight();
1941
+ }
1942
+ }
1943
+
1944
+ /**
1945
+ * @internal
1946
+ * Firefox has a strange behavior where pressing the down arrow key from
1947
+ * above the table will move the caret after the table and then lexical
1948
+ * will select the last cell instead of the first.
1949
+ * We do still want to let the browser handle caret movement but we will
1950
+ * use this property to "tag" the update so that we can recheck the
1951
+ * selection after the event is processed.
1952
+ */
1953
+ setShouldCheckSelection() {
1954
+ this.shouldCheckSelection = true;
1955
+ }
1956
+ /**
1957
+ * @internal
1958
+ */
1959
+ getAndClearShouldCheckSelection() {
1960
+ if (this.shouldCheckSelection) {
1961
+ this.shouldCheckSelection = false;
1962
+ return true;
1963
+ }
1964
+ return false;
1965
+ }
1966
+
1967
+ /**
1968
+ * @internal
1969
+ * When handling mousemove events we track what the focus cell should be, but
1970
+ * the DOM selection may end up somewhere else entirely. We don't have an elegant
1971
+ * way to handle this after the DOM selection has been resolved in a
1972
+ * SELECTION_CHANGE_COMMAND callback.
1973
+ */
1974
+ setNextFocus(nextFocus) {
1975
+ this.nextFocus = nextFocus;
1976
+ }
1977
+
1978
+ /** @internal */
1979
+ getAndClearNextFocus() {
1980
+ const {
1981
+ nextFocus
1982
+ } = this;
1983
+ if (nextFocus !== null) {
1984
+ this.nextFocus = null;
1985
+ }
1986
+ return nextFocus;
1987
+ }
1988
+
1989
+ /** @internal */
1990
+ updateDOMSelection() {
1991
+ if (this.anchorCell !== null && this.focusCell !== null) {
1992
+ const domSelection = lexical.getDOMSelectionForEditor(this.editor);
1993
+ // We are not using a native selection for tables, and if we
1994
+ // set one then the reconciler will undo it.
1995
+ // TODO - it would make sense to have one so that native
1996
+ // copy/paste worked. Right now we have to emulate with
1997
+ // keyboard events but it won't fire if triggered from the menu
1998
+ if (domSelection && domSelection.rangeCount > 0) {
1999
+ domSelection.removeAllRanges();
2000
+ }
2001
+ }
2002
+ }
2003
+ $setFocusCellForSelection(cell, ignoreStart = false) {
2004
+ const editor = this.editor;
2005
+ const {
2006
+ tableNode
2007
+ } = this.$lookup();
2008
+ const cellX = cell.x;
2009
+ const cellY = cell.y;
2010
+ this.focusCell = cell;
2011
+ if (!this.isHighlightingCells && (this.anchorX !== cellX || this.anchorY !== cellY || ignoreStart)) {
2012
+ this.isHighlightingCells = true;
2013
+ this.$disableHighlightStyle();
2014
+ } else if (cellX === this.focusX && cellY === this.focusY) {
2015
+ return false;
2016
+ }
2017
+ this.focusX = cellX;
2018
+ this.focusY = cellY;
2019
+ if (this.isHighlightingCells) {
2020
+ const focusTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
2021
+ if (this.tableSelection != null && this.anchorCellNodeKey != null && focusTableCellNode !== null) {
2022
+ this.focusCellNodeKey = focusTableCellNode.getKey();
2023
+ this.tableSelection = $createTableSelectionFrom(tableNode, this.$getAnchorTableCellOrThrow(), focusTableCellNode);
2024
+ lexical.$setSelection(this.tableSelection);
2025
+ editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2026
+ $updateDOMForSelection(editor, this.table, this.tableSelection);
2027
+ return true;
2028
+ }
2029
+ }
2030
+ return false;
2031
+ }
2032
+ $getAnchorTableCell() {
2033
+ return this.anchorCellNodeKey ? lexical.$getNodeByKey(this.anchorCellNodeKey) : null;
2034
+ }
2035
+ $getAnchorTableCellOrThrow() {
2036
+ const anchorTableCell = this.$getAnchorTableCell();
2037
+ if (!(anchorTableCell !== null)) {
2038
+ formatDevErrorMessage(`TableObserver anchorTableCell is null`);
2039
+ }
2040
+ return anchorTableCell;
2041
+ }
2042
+ $getFocusTableCell() {
2043
+ return this.focusCellNodeKey ? lexical.$getNodeByKey(this.focusCellNodeKey) : null;
2044
+ }
2045
+ $getFocusTableCellOrThrow() {
2046
+ const focusTableCell = this.$getFocusTableCell();
2047
+ if (!(focusTableCell !== null)) {
2048
+ formatDevErrorMessage(`TableObserver focusTableCell is null`);
2049
+ }
2050
+ return focusTableCell;
2051
+ }
2052
+ $setAnchorCellForSelection(cell) {
2053
+ this.isHighlightingCells = false;
2054
+ this.anchorCell = cell;
2055
+ this.anchorX = cell.x;
2056
+ this.anchorY = cell.y;
2057
+ const {
2058
+ tableNode
2059
+ } = this.$lookup();
2060
+ const anchorTableCellNode = $getNearestTableCellInTableFromDOMNode(tableNode, cell.elem);
2061
+ if (anchorTableCellNode !== null) {
2062
+ const anchorNodeKey = anchorTableCellNode.getKey();
2063
+ this.tableSelection = this.tableSelection != null ? this.tableSelection.clone() : $createTableSelection();
2064
+ this.anchorCellNodeKey = anchorNodeKey;
2065
+ }
2066
+ }
2067
+ $formatCells(type) {
2068
+ const selection = lexical.$getSelection();
2069
+ if (!$isTableSelection(selection)) {
2070
+ formatDevErrorMessage(`Expected Table selection`);
2071
+ }
2072
+ const formatSelection = lexical.$createRangeSelection();
2073
+ const anchor = formatSelection.anchor;
2074
+ const focus = formatSelection.focus;
2075
+ const cellNodes = selection.getNodes().filter($isTableCellNode);
2076
+ if (!(cellNodes.length > 0)) {
2077
+ formatDevErrorMessage(`No table cells present`);
2078
+ }
2079
+ const paragraph = cellNodes[0].getFirstChild();
2080
+ const alignFormatWith = lexical.$isParagraphNode(paragraph) ? paragraph.getFormatFlags(type, null) : null;
2081
+ cellNodes.forEach(cellNode => {
2082
+ anchor.set(cellNode.getKey(), 0, 'element');
2083
+ focus.set(cellNode.getKey(), cellNode.getChildrenSize(), 'element');
2084
+ formatSelection.formatText(type, alignFormatWith);
2085
+ });
2086
+ lexical.$setSelection(selection);
2087
+ this.editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2088
+ }
2089
+ $clearText() {
2090
+ const {
2091
+ editor
2092
+ } = this;
2093
+ const tableNode = lexical.$getNodeByKey(this.tableNodeKey);
2094
+ if (!$isTableNode(tableNode)) {
2095
+ throw new Error('Expected TableNode.');
2096
+ }
2097
+ const selection = lexical.$getSelection();
2098
+ if (!$isTableSelection(selection)) {
2099
+ formatDevErrorMessage(`Expected TableSelection`);
2100
+ }
2101
+ const selectedNodes = selection.getNodes().filter($isTableCellNode);
2102
+
2103
+ // Check if the entire table is selected by verifying first and last cells
2104
+ const firstRow = tableNode.getFirstChild();
2105
+ const lastRow = tableNode.getLastChild();
2106
+ const isEntireTableSelected = selectedNodes.length > 0 && firstRow !== null && lastRow !== null && $isTableRowNode(firstRow) && $isTableRowNode(lastRow) && selectedNodes[0] === firstRow.getFirstChild() && selectedNodes[selectedNodes.length - 1] === lastRow.getLastChild();
2107
+ if (isEntireTableSelected) {
2108
+ tableNode.selectPrevious();
2109
+ const parent = tableNode.getParent();
2110
+ // Delete entire table
2111
+ tableNode.remove();
2112
+ // Handle case when table was the only node
2113
+ if (lexical.$isRootNode(parent) && parent.isEmpty()) {
2114
+ editor.dispatchCommand(lexical.INSERT_PARAGRAPH_COMMAND, undefined);
2115
+ }
2116
+ return;
2117
+ }
2118
+ selectedNodes.forEach(cellNode => {
2119
+ if (lexical.$isElementNode(cellNode)) {
2120
+ const paragraphNode = lexical.$createParagraphNode();
2121
+ const textNode = lexical.$createTextNode();
2122
+ paragraphNode.append(textNode);
2123
+ cellNode.append(paragraphNode);
2124
+ cellNode.getChildren().forEach(child => {
2125
+ if (child !== paragraphNode) {
2126
+ child.remove();
2127
+ }
2128
+ });
2129
+ }
2130
+ });
2131
+ $updateDOMForSelection(editor, this.table, null);
2132
+ lexical.$setSelection(null);
2133
+ editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2134
+ }
2135
+ }
2136
+
2137
+ const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection';
2138
+ const isPointerDownOnEvent = event => {
2139
+ return (event.buttons & 1) === 1;
2140
+ };
2141
+ function isHTMLTableElement(el) {
2142
+ return lexical.isHTMLElement(el) && el.nodeName === 'TABLE';
2143
+ }
2144
+ function getTableElement(tableNode, dom) {
2145
+ if (!dom) {
2146
+ return dom;
2147
+ }
2148
+ const element = isHTMLTableElement(dom) ? dom : tableNode.getDOMSlot(dom).element;
2149
+ if (!(element.nodeName === 'TABLE')) {
2150
+ formatDevErrorMessage(`getTableElement: Expecting table in as DOM node for TableNode, not ${dom.nodeName}`);
2151
+ }
2152
+ return element;
2153
+ }
2154
+ function getEditorWindow(editor) {
2155
+ return editor._window;
2156
+ }
2157
+ function $findParentTableCellNodeInTable(tableNode, node) {
2158
+ for (let currentNode = node, lastTableCellNode = null; currentNode !== null; currentNode = currentNode.getParent()) {
2159
+ if (tableNode.is(currentNode)) {
2160
+ return lastTableCellNode;
2161
+ } else if ($isTableCellNode(currentNode)) {
2162
+ lastTableCellNode = currentNode;
2163
+ }
2164
+ }
2165
+ return null;
2166
+ }
2167
+ const ARROW_KEY_COMMANDS_WITH_DIRECTION = [[lexical.KEY_ARROW_DOWN_COMMAND, 'down'], [lexical.KEY_ARROW_UP_COMMAND, 'up'], [lexical.KEY_ARROW_LEFT_COMMAND, 'backward'], [lexical.KEY_ARROW_RIGHT_COMMAND, 'forward']];
2168
+ const DELETE_TEXT_COMMANDS = [lexical.DELETE_WORD_COMMAND, lexical.DELETE_LINE_COMMAND, lexical.DELETE_CHARACTER_COMMAND];
2169
+ const DELETE_KEY_COMMANDS = [lexical.KEY_BACKSPACE_COMMAND, lexical.KEY_DELETE_COMMAND];
2170
+ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2171
+ const rootElement = editor.getRootElement();
2172
+ const editorWindow = getEditorWindow(editor);
2173
+ if (!(rootElement !== null && editorWindow !== null)) {
2174
+ formatDevErrorMessage(`applyTableHandlers: editor has no root element set`);
2175
+ }
2176
+ const tableObserver = new TableObserver(editor, tableNode.getKey());
2177
+ const tableElement = getTableElement(tableNode, element);
2178
+ attachTableObserverToTableElement(tableElement, tableObserver);
2179
+ tableObserver.listenersToRemove.add(() => detachTableObserverFromTableElement(tableElement, tableObserver));
2180
+ const createPointerHandlers = () => {
2181
+ if (tableObserver.isSelecting) {
2182
+ return;
2183
+ }
2184
+ const onPointerUp = () => {
2185
+ tableObserver.isSelecting = false;
2186
+ editorWindow.removeEventListener('pointerup', onPointerUp);
2187
+ editorWindow.removeEventListener('pointermove', onPointerMove);
2188
+ };
2189
+ const onPointerMove = moveEvent => {
2190
+ if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
2191
+ tableObserver.isSelecting = false;
2192
+ editorWindow.removeEventListener('pointerup', onPointerUp);
2193
+ editorWindow.removeEventListener('pointermove', onPointerMove);
2194
+ return;
2195
+ }
2196
+ if (!lexical.isDOMNode(moveEvent.target)) {
2197
+ return;
2198
+ }
2199
+ let focusCell = null;
2200
+ // In firefox the moveEvent.target may be captured so we must always
2201
+ // consult the coordinates #7245
2202
+ const override = !(IS_FIREFOX || tableElement.contains(moveEvent.target));
2203
+ if (override) {
2204
+ focusCell = getDOMCellInTableFromTarget(tableElement, moveEvent.target);
2205
+ } else {
2206
+ for (const el of document.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
2207
+ focusCell = getDOMCellInTableFromTarget(tableElement, el);
2208
+ if (focusCell) {
2209
+ break;
2210
+ }
2211
+ }
2212
+ }
2213
+ if (focusCell && (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem)) {
2214
+ tableObserver.setNextFocus({
2215
+ focusCell,
2216
+ override
2217
+ });
2218
+ editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
2219
+ }
2220
+ };
2221
+ tableObserver.isSelecting = true;
2222
+ editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
2223
+ editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
2224
+ };
2225
+ const onPointerDown = event => {
2226
+ tableObserver.pointerType = event.pointerType;
2227
+ if (event.button !== 0 || !lexical.isDOMNode(event.target) || !editorWindow) {
2228
+ return;
2229
+ }
2230
+ const targetCell = getDOMCellFromTarget(event.target);
2231
+ if (targetCell !== null) {
2232
+ editor.update(() => {
2233
+ const prevSelection = lexical.$getPreviousSelection();
2234
+ // We can't trust Firefox to do the right thing with the selection and
2235
+ // we don't have a proper state machine to do this "correctly" but
2236
+ // if we go ahead and make the table selection now it will work
2237
+ if (IS_FIREFOX && event.shiftKey && $isSelectionInTable(prevSelection, tableNode) && (lexical.$isRangeSelection(prevSelection) || $isTableSelection(prevSelection))) {
2238
+ const prevAnchorNode = prevSelection.anchor.getNode();
2239
+ const prevAnchorCell = $findParentTableCellNodeInTable(tableNode, prevSelection.anchor.getNode());
2240
+ if (prevAnchorCell) {
2241
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCell));
2242
+ tableObserver.$setFocusCellForSelection(targetCell);
2243
+ stopEvent(event);
2244
+ } else {
2245
+ const newSelection = tableNode.isBefore(prevAnchorNode) ? tableNode.selectStart() : tableNode.selectEnd();
2246
+ newSelection.anchor.set(prevSelection.anchor.key, prevSelection.anchor.offset, prevSelection.anchor.type);
2247
+ }
2248
+ } else {
2249
+ // Only set anchor cell for selection if this is not a simple touch tap
2250
+ // Touch taps should not initiate table selection mode
2251
+ if (event.pointerType !== 'touch') {
2252
+ tableObserver.$setAnchorCellForSelection(targetCell);
2253
+ }
2254
+ }
2255
+ });
2256
+ }
2257
+ createPointerHandlers();
2258
+ };
2259
+ tableElement.addEventListener('pointerdown', onPointerDown, tableObserver.listenerOptions);
2260
+ tableObserver.listenersToRemove.add(() => {
2261
+ tableElement.removeEventListener('pointerdown', onPointerDown);
2262
+ });
2263
+ const onTripleClick = event => {
2264
+ if (event.detail >= 3 && lexical.isDOMNode(event.target)) {
2265
+ const targetCell = getDOMCellFromTarget(event.target);
2266
+ if (targetCell !== null) {
2267
+ event.preventDefault();
2268
+ }
2269
+ }
2270
+ };
2271
+ tableElement.addEventListener('mousedown', onTripleClick, tableObserver.listenerOptions);
2272
+ tableObserver.listenersToRemove.add(() => {
2273
+ tableElement.removeEventListener('mousedown', onTripleClick);
2274
+ });
2275
+
2276
+ // Clear selection when clicking outside of dom.
2277
+ const pointerDownCallback = event => {
2278
+ const target = event.target;
2279
+ if (event.button !== 0 || !lexical.isDOMNode(target)) {
2280
+ return;
2281
+ }
2282
+ editor.update(() => {
2283
+ const selection = lexical.$getSelection();
2284
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey && rootElement.contains(target)) {
2285
+ tableObserver.$clearHighlight();
2286
+ }
2287
+ });
2288
+ };
2289
+ editorWindow.addEventListener('pointerdown', pointerDownCallback, tableObserver.listenerOptions);
2290
+ tableObserver.listenersToRemove.add(() => {
2291
+ editorWindow.removeEventListener('pointerdown', pointerDownCallback);
2292
+ });
2293
+ for (const [command, direction] of ARROW_KEY_COMMANDS_WITH_DIRECTION) {
2294
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, event => $handleArrowKey(editor, event, direction, tableNode, tableObserver), lexical.COMMAND_PRIORITY_HIGH));
2295
+ }
2296
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_ESCAPE_COMMAND, event => {
2297
+ const selection = lexical.$getSelection();
2298
+ if ($isTableSelection(selection)) {
2299
+ const focusCellNode = $findParentTableCellNodeInTable(tableNode, selection.focus.getNode());
2300
+ if (focusCellNode !== null) {
2301
+ stopEvent(event);
2302
+ focusCellNode.selectEnd();
2303
+ return true;
2304
+ }
2305
+ }
2306
+ return false;
2307
+ }, lexical.COMMAND_PRIORITY_HIGH));
2308
+ const deleteTextHandler = command => () => {
2309
+ const selection = lexical.$getSelection();
2310
+ if (!$isSelectionInTable(selection, tableNode)) {
2311
+ return false;
2312
+ }
2313
+ if ($isTableSelection(selection)) {
2314
+ tableObserver.$clearText();
2315
+ return true;
2316
+ } else if (lexical.$isRangeSelection(selection)) {
2317
+ const tableCellNode = $findParentTableCellNodeInTable(tableNode, selection.anchor.getNode());
2318
+ if (!$isTableCellNode(tableCellNode)) {
2319
+ return false;
2320
+ }
2321
+ const anchorNode = selection.anchor.getNode();
2322
+ const focusNode = selection.focus.getNode();
2323
+ const isAnchorInside = tableNode.isParentOf(anchorNode);
2324
+ const isFocusInside = tableNode.isParentOf(focusNode);
2325
+ const selectionContainsPartialTable = isAnchorInside && !isFocusInside || isFocusInside && !isAnchorInside;
2326
+ if (selectionContainsPartialTable) {
2327
+ tableObserver.$clearText();
2328
+ return true;
2329
+ }
2330
+ const nearestElementNode = lexicalUtils.$findMatchingParent(selection.anchor.getNode(), n => lexical.$isElementNode(n));
2331
+ const topLevelCellElementNode = nearestElementNode && lexicalUtils.$findMatchingParent(nearestElementNode, n => lexical.$isElementNode(n) && $isTableCellNode(n.getParent()));
2332
+ if (!lexical.$isElementNode(topLevelCellElementNode) || !lexical.$isElementNode(nearestElementNode)) {
2333
+ return false;
2334
+ }
2335
+ if (command === lexical.DELETE_LINE_COMMAND && topLevelCellElementNode.getPreviousSibling() === null) {
2336
+ // TODO: Fix Delete Line in Table Cells.
2337
+ return true;
2338
+ }
2339
+ }
2340
+ return false;
2341
+ };
2342
+ for (const command of DELETE_TEXT_COMMANDS) {
2343
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, deleteTextHandler(command), lexical.COMMAND_PRIORITY_HIGH));
2344
+ }
2345
+ const $deleteCellHandler = event => {
2346
+ const selection = lexical.$getSelection();
2347
+ if (!($isTableSelection(selection) || lexical.$isRangeSelection(selection))) {
2348
+ return false;
2349
+ }
2350
+
2351
+ // If the selection is inside the table but should remove the whole table
2352
+ // we expand the selection so that both the anchor and focus are outside
2353
+ // the table and the editor's command listener will handle the delete
2354
+ const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
2355
+ const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
2356
+ if (isAnchorInside !== isFocusInside) {
2357
+ const tablePoint = isAnchorInside ? 'anchor' : 'focus';
2358
+ const outerPoint = isAnchorInside ? 'focus' : 'anchor';
2359
+ // Preserve the outer point
2360
+ const {
2361
+ key,
2362
+ offset,
2363
+ type
2364
+ } = selection[outerPoint];
2365
+ // Expand the selection around the table
2366
+ const newSelection = tableNode[selection[tablePoint].isBefore(selection[outerPoint]) ? 'selectPrevious' : 'selectNext']();
2367
+ // Restore the outer point of the selection
2368
+ newSelection[outerPoint].set(key, offset, type);
2369
+ // Let the base implementation handle the rest
2370
+ return false;
2371
+ }
2372
+ if (!$isSelectionInTable(selection, tableNode)) {
2373
+ return false;
2374
+ }
2375
+ if ($isTableSelection(selection)) {
2376
+ if (event) {
2377
+ event.preventDefault();
2378
+ event.stopPropagation();
2379
+ }
2380
+ tableObserver.$clearText();
2381
+ return true;
2382
+ }
2383
+ return false;
2384
+ };
2385
+ for (const command of DELETE_KEY_COMMANDS) {
2386
+ tableObserver.listenersToRemove.add(editor.registerCommand(command, $deleteCellHandler, lexical.COMMAND_PRIORITY_HIGH));
2387
+ }
2388
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.CUT_COMMAND, event => {
2389
+ const selection = lexical.$getSelection();
2390
+ if (selection) {
2391
+ if (!($isTableSelection(selection) || lexical.$isRangeSelection(selection))) {
2392
+ return false;
2393
+ }
2394
+ // Copying to the clipboard is async so we must capture the data
2395
+ // before we delete it
2396
+ void lexicalClipboard.copyToClipboard(editor, lexicalUtils.objectKlassEquals(event, ClipboardEvent) ? event : null, lexicalClipboard.$getClipboardDataFromSelection(selection));
2397
+ const intercepted = $deleteCellHandler(event);
2398
+ if (lexical.$isRangeSelection(selection)) {
2399
+ selection.removeText();
2400
+ return true;
2401
+ }
2402
+ return intercepted;
2403
+ }
2404
+ return false;
2405
+ }, lexical.COMMAND_PRIORITY_HIGH));
2406
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FORMAT_TEXT_COMMAND, payload => {
2407
+ const selection = lexical.$getSelection();
2408
+ if (!$isSelectionInTable(selection, tableNode)) {
2409
+ return false;
2410
+ }
2411
+ if ($isTableSelection(selection)) {
2412
+ tableObserver.$formatCells(payload);
2413
+ return true;
2414
+ } else if (lexical.$isRangeSelection(selection)) {
2415
+ const tableCellNode = lexicalUtils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
2416
+ if (!$isTableCellNode(tableCellNode)) {
2417
+ return false;
2418
+ }
2419
+ }
2420
+ return false;
2421
+ }, lexical.COMMAND_PRIORITY_HIGH));
2422
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FORMAT_ELEMENT_COMMAND, formatType => {
2423
+ const selection = lexical.$getSelection();
2424
+ if (!$isTableSelection(selection) || !$isSelectionInTable(selection, tableNode)) {
2425
+ return false;
2426
+ }
2427
+ const anchorNode = selection.anchor.getNode();
2428
+ const focusNode = selection.focus.getNode();
2429
+ if (!$isTableCellNode(anchorNode) || !$isTableCellNode(focusNode)) {
2430
+ return false;
2431
+ }
2432
+
2433
+ // Align the table if the entire table is selected
2434
+ if ($isFullTableSelection(selection, tableNode)) {
2435
+ tableNode.setFormat(formatType);
2436
+ return true;
2437
+ }
2438
+ const [tableMap, anchorCell, focusCell] = $computeTableMap(tableNode, anchorNode, focusNode);
2439
+ const maxRow = Math.max(anchorCell.startRow + anchorCell.cell.__rowSpan - 1, focusCell.startRow + focusCell.cell.__rowSpan - 1);
2440
+ const maxColumn = Math.max(anchorCell.startColumn + anchorCell.cell.__colSpan - 1, focusCell.startColumn + focusCell.cell.__colSpan - 1);
2441
+ const minRow = Math.min(anchorCell.startRow, focusCell.startRow);
2442
+ const minColumn = Math.min(anchorCell.startColumn, focusCell.startColumn);
2443
+ const visited = new Set();
2444
+ for (let i = minRow; i <= maxRow; i++) {
2445
+ for (let j = minColumn; j <= maxColumn; j++) {
2446
+ const cell = tableMap[i][j].cell;
2447
+ if (visited.has(cell)) {
2448
+ continue;
2449
+ }
2450
+ visited.add(cell);
2451
+ cell.setFormat(formatType);
2452
+ const cellChildren = cell.getChildren();
2453
+ for (let k = 0; k < cellChildren.length; k++) {
2454
+ const child = cellChildren[k];
2455
+ if (lexical.$isElementNode(child) && !child.isInline()) {
2456
+ child.setFormat(formatType);
2457
+ }
2458
+ }
2459
+ }
2460
+ }
2461
+ return true;
2462
+ }, lexical.COMMAND_PRIORITY_HIGH));
2463
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.CONTROLLED_TEXT_INSERTION_COMMAND, payload => {
2464
+ const selection = lexical.$getSelection();
2465
+ if (!$isSelectionInTable(selection, tableNode)) {
2466
+ return false;
2467
+ }
2468
+ if ($isTableSelection(selection)) {
2469
+ tableObserver.$clearHighlight();
2470
+ return false;
2471
+ } else if (lexical.$isRangeSelection(selection)) {
2472
+ const tableCellNode = lexicalUtils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
2473
+ if (!$isTableCellNode(tableCellNode)) {
2474
+ return false;
2475
+ }
2476
+ if (typeof payload === 'string') {
2477
+ const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
2478
+ if (edgePosition) {
2479
+ $insertParagraphAtTableEdge(edgePosition, tableNode, [lexical.$createTextNode(payload)]);
2480
+ return true;
2481
+ }
2482
+ }
2483
+ }
2484
+ return false;
2485
+ }, lexical.COMMAND_PRIORITY_HIGH));
2486
+ if (hasTabHandler) {
2487
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.KEY_TAB_COMMAND, event => {
2488
+ const selection = lexical.$getSelection();
2489
+ if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
2490
+ return false;
2491
+ }
2492
+ const tableCellNode = $findCellNode(selection.anchor.getNode());
2493
+ if (tableCellNode === null || !tableNode.is($findTableNode(tableCellNode))) {
2494
+ return false;
2495
+ }
2496
+ stopEvent(event);
2497
+ $selectAdjacentCell(tableCellNode, event.shiftKey ? 'previous' : 'next');
2498
+ return true;
2499
+ }, lexical.COMMAND_PRIORITY_HIGH));
2500
+ }
2501
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.FOCUS_COMMAND, payload => {
2502
+ return tableNode.isSelected();
2503
+ }, lexical.COMMAND_PRIORITY_HIGH));
2504
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, (selectionPayload, dispatchEditor) => {
2505
+ if (editor !== dispatchEditor) {
2506
+ return false;
2507
+ }
2508
+ const {
2509
+ nodes,
2510
+ selection
2511
+ } = selectionPayload;
2512
+ const anchorAndFocus = selection.getStartEndPoints();
2513
+ const isTableSelection = $isTableSelection(selection);
2514
+ const isRangeSelection = lexical.$isRangeSelection(selection);
2515
+ const isSelectionInsideOfGrid = isRangeSelection && lexicalUtils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n)) !== null && lexicalUtils.$findMatchingParent(selection.focus.getNode(), n => $isTableCellNode(n)) !== null || isTableSelection;
2516
+ if (nodes.length !== 1 || !$isTableNode(nodes[0]) || !isSelectionInsideOfGrid || anchorAndFocus === null) {
2517
+ return false;
2518
+ }
2519
+ const [anchor, focus] = anchorAndFocus;
2520
+ const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
2521
+ const focusCellNode = lexicalUtils.$findMatchingParent(focus.getNode(), n => $isTableCellNode(n));
2522
+ if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableRowNode(anchorRowNode) || !$isTableNode(gridNode)) {
2523
+ return false;
2524
+ }
2525
+ const templateGrid = nodes[0];
2526
+ const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(gridNode, anchorCellNode, focusCellNode);
2527
+ const [templateGridMap] = $computeTableMapSkipCellCheck(templateGrid, null, null);
2528
+ const initialRowCount = initialGridMap.length;
2529
+ const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
2530
+
2531
+ // If we have a range selection, we'll fit the template grid into the
2532
+ // table, growing the table if necessary.
2533
+ let startRow = anchorCellMap.startRow;
2534
+ let startCol = anchorCellMap.startColumn;
2535
+ let affectedRowCount = templateGridMap.length;
2536
+ let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
2537
+ if (isTableSelection) {
2538
+ // If we have a table selection, we'll only modify the cells within
2539
+ // the selection boundary.
2540
+ const selectionBoundary = $computeTableCellRectBoundary(initialGridMap, anchorCellMap, focusCellMap);
2541
+ const selectionRowCount = selectionBoundary.maxRow - selectionBoundary.minRow + 1;
2542
+ const selectionColCount = selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
2543
+ startRow = selectionBoundary.minRow;
2544
+ startCol = selectionBoundary.minColumn;
2545
+ affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
2546
+ affectedColCount = Math.min(affectedColCount, selectionColCount);
2547
+ }
2548
+
2549
+ // Step 1: Unmerge all merged cells within the affected area
2550
+ let didPerformMergeOperations = false;
2551
+ const lastRowForUnmerge = Math.min(initialRowCount, startRow + affectedRowCount) - 1;
2552
+ const lastColForUnmerge = Math.min(initialColCount, startCol + affectedColCount) - 1;
2553
+ const unmergedKeys = new Set();
2554
+ for (let row = startRow; row <= lastRowForUnmerge; row++) {
2555
+ for (let col = startCol; col <= lastColForUnmerge; col++) {
2556
+ const cellMap = initialGridMap[row][col];
2557
+ if (unmergedKeys.has(cellMap.cell.getKey())) {
2558
+ continue; // cell was a merged cell that was already handled
2559
+ }
2560
+ if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
2561
+ continue; // cell is not a merged cell
2562
+ }
2563
+ $unmergeCellNode(cellMap.cell);
2564
+ unmergedKeys.add(cellMap.cell.getKey());
2565
+ didPerformMergeOperations = true;
2566
+ }
2567
+ }
2568
+ let [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2569
+
2570
+ // Step 2: Expand current table (if needed)
2571
+ const rowsToInsert = affectedRowCount - initialRowCount + startRow;
2572
+ for (let i = 0; i < rowsToInsert; i++) {
2573
+ const cellMap = interimGridMap[initialRowCount - 1][0];
2574
+ $insertTableRowAtNode(cellMap.cell);
2575
+ }
2576
+ const colsToInsert = affectedColCount - initialColCount + startCol;
2577
+ for (let i = 0; i < colsToInsert; i++) {
2578
+ const cellMap = interimGridMap[0][initialColCount - 1];
2579
+ $insertTableColumnAtNode(cellMap.cell, true, false);
2580
+ }
2581
+ [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2582
+
2583
+ // Step 3: Merge cells and set cell content, to match template grid
2584
+ for (let row = startRow; row < startRow + affectedRowCount; row++) {
2585
+ for (let col = startCol; col < startCol + affectedColCount; col++) {
2586
+ const templateRow = row - startRow;
2587
+ const templateCol = col - startCol;
2588
+ const templateCellMap = templateGridMap[templateRow][templateCol];
2589
+ if (templateCellMap.startRow !== templateRow || templateCellMap.startColumn !== templateCol) {
2590
+ continue; // cell is a merged cell that was already handled
2591
+ }
2592
+ const templateCell = templateCellMap.cell;
2593
+ if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
2594
+ const cellsToMerge = [];
2595
+ const lastRowForMerge = Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) - 1;
2596
+ const lastColForMerge = Math.min(col + templateCell.__colSpan, startCol + affectedColCount) - 1;
2597
+ for (let r = row; r <= lastRowForMerge; r++) {
2598
+ for (let c = col; c <= lastColForMerge; c++) {
2599
+ const cellMap = interimGridMap[r][c];
2600
+ cellsToMerge.push(cellMap.cell);
2601
+ }
2602
+ }
2603
+ $mergeCells(cellsToMerge);
2604
+ didPerformMergeOperations = true;
2605
+ }
2606
+ const {
2607
+ cell
2608
+ } = interimGridMap[row][col];
2609
+ const originalChildren = cell.getChildren();
2610
+ templateCell.getChildren().forEach(child => {
2611
+ if (lexical.$isTextNode(child)) {
2612
+ const paragraphNode = lexical.$createParagraphNode();
2613
+ paragraphNode.append(child);
2614
+ cell.append(child);
2615
+ } else {
2616
+ cell.append(child);
2617
+ }
2618
+ });
2619
+ originalChildren.forEach(n => n.remove());
2620
+ }
2621
+ }
2622
+ if (isTableSelection && didPerformMergeOperations) {
2623
+ // reset the table selection in case the anchor or focus cell was
2624
+ // removed via merge operations
2625
+ const [finalGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2626
+ const newAnchorCellMap = finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
2627
+ newAnchorCellMap.cell.selectEnd();
2628
+ }
2629
+ return true;
2630
+ }, lexical.COMMAND_PRIORITY_HIGH));
2631
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.SELECTION_CHANGE_COMMAND, () => {
2632
+ const selection = lexical.$getSelection();
2633
+ const prevSelection = lexical.$getPreviousSelection();
2634
+ const nextFocus = tableObserver.getAndClearNextFocus();
2635
+ if (nextFocus !== null) {
2636
+ const {
2637
+ focusCell
2638
+ } = nextFocus;
2639
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2640
+ if (focusCell.x === tableObserver.focusX && focusCell.y === tableObserver.focusY) {
2641
+ // The selection is already the correct table selection
2642
+ return false;
2643
+ } else {
2644
+ tableObserver.$setFocusCellForSelection(focusCell);
2645
+ return true;
2646
+ }
2647
+ } else if (focusCell !== tableObserver.anchorCell && $isSelectionInTable(selection, tableNode)) {
2648
+ // The selection has crossed cells
2649
+ tableObserver.$setFocusCellForSelection(focusCell);
2650
+ return true;
2651
+ }
2652
+ }
2653
+ const shouldCheckSelection = tableObserver.getAndClearShouldCheckSelection();
2654
+ // If they pressed the down arrow with the selection outside of the
2655
+ // table, and then the selection ends up in the table but not in the
2656
+ // first cell, then move the selection to the first cell.
2657
+ if (shouldCheckSelection && lexical.$isRangeSelection(prevSelection) && lexical.$isRangeSelection(selection) && selection.isCollapsed()) {
2658
+ const anchor = selection.anchor.getNode();
2659
+ const firstRow = tableNode.getFirstChild();
2660
+ const anchorCell = $findCellNode(anchor);
2661
+ if (anchorCell !== null && $isTableRowNode(firstRow)) {
2662
+ const firstCell = firstRow.getFirstChild();
2663
+ if ($isTableCellNode(firstCell) && tableNode.is(lexicalUtils.$findMatchingParent(anchorCell, node => node.is(tableNode) || node.is(firstCell)))) {
2664
+ // The selection moved to the table, but not in the first cell
2665
+ firstCell.selectStart();
2666
+ return true;
2667
+ }
2668
+ }
2669
+ }
2670
+ if (lexical.$isRangeSelection(selection)) {
2671
+ const {
2672
+ anchor,
2673
+ focus
2674
+ } = selection;
2675
+ const anchorNode = anchor.getNode();
2676
+ const focusNode = focus.getNode();
2677
+ // Using explicit comparison with table node to ensure it's not a nested table
2678
+ // as in that case we'll leave selection resolving to that table
2679
+ const anchorCellNode = $findCellNode(anchorNode);
2680
+ const focusCellNode = $findCellNode(focusNode);
2681
+ const isAnchorInside = !!(anchorCellNode && tableNode.is($findTableNode(anchorCellNode)));
2682
+ const isFocusInside = !!(focusCellNode && tableNode.is($findTableNode(focusCellNode)));
2683
+ const isPartiallyWithinTable = isAnchorInside !== isFocusInside;
2684
+ const isWithinTable = isAnchorInside && isFocusInside;
2685
+ const isBackward = selection.isBackward();
2686
+ if (isPartiallyWithinTable) {
2687
+ const newSelection = selection.clone();
2688
+ if (isFocusInside) {
2689
+ const [tableMap] = $computeTableMap(tableNode, focusCellNode, focusCellNode);
2690
+ const firstCell = tableMap[0][0].cell;
2691
+ const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
2692
+ // When backward, focus should be at START of first cell (0)
2693
+ // When forward, focus should be at END of last cell (getChildrenSize)
2694
+ newSelection.focus.set(isBackward ? firstCell.getKey() : lastCell.getKey(), isBackward ? 0 : lastCell.getChildrenSize(), 'element');
2695
+ } else if (isAnchorInside) {
2696
+ const [tableMap] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
2697
+ const firstCell = tableMap[0][0].cell;
2698
+ const lastCell = tableMap[tableMap.length - 1].at(-1).cell;
2699
+ /**
2700
+ * If isBackward, set the anchor to be at the end of the table so that when the cursor moves outside of
2701
+ * the table in the backward direction, the entire table will be selected from its end.
2702
+ * Otherwise, if forward, set the anchor to be at the start of the table so that when the focus is dragged
2703
+ * outside th end of the table, it will start from the beginning of the table.
2704
+ */
2705
+ newSelection.anchor.set(isBackward ? lastCell.getKey() : firstCell.getKey(), isBackward ? lastCell.getChildrenSize() : 0, 'element');
2706
+ }
2707
+ lexical.$setSelection(newSelection);
2708
+ $addHighlightStyleToTable(editor, tableObserver);
2709
+ } else if (isWithinTable) {
2710
+ // Handle case when selection spans across multiple cells but still
2711
+ // has range selection, then we convert it into table selection
2712
+ if (!anchorCellNode.is(focusCellNode)) {
2713
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, anchorCellNode));
2714
+ tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
2715
+ }
2716
+
2717
+ // Handle case when the pointer type is touch and the current and
2718
+ // previous selection are collapsed, and the previous anchor and current
2719
+ // focus cell nodes are different, then we convert it into table selection
2720
+ // However, only do this if the table observer is actively selecting (user dragging)
2721
+ // to prevent unwanted selections when simply tapping between cells on mobile
2722
+ if (tableObserver.pointerType === 'touch' && tableObserver.isSelecting && selection.isCollapsed() && lexical.$isRangeSelection(prevSelection) && prevSelection.isCollapsed()) {
2723
+ const prevAnchorCellNode = $findCellNode(prevSelection.anchor.getNode());
2724
+ if (prevAnchorCellNode && !prevAnchorCellNode.is(focusCellNode)) {
2725
+ tableObserver.$setAnchorCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, prevAnchorCellNode));
2726
+ tableObserver.$setFocusCellForSelection($getObserverCellFromCellNodeOrThrow(tableObserver, focusCellNode), true);
2727
+ tableObserver.pointerType = null;
2728
+ }
2729
+ }
2730
+ }
2731
+ } else if (selection && $isTableSelection(selection) && selection.is(prevSelection) && selection.tableKey === tableNode.getKey()) {
2732
+ // if selection goes outside of the table we need to change it to Range selection
2733
+ const domSelection = lexical.getDOMSelectionForEditor(editor);
2734
+ if (domSelection && domSelection.anchorNode && domSelection.focusNode) {
2735
+ const focusNode = lexical.$getNearestNodeFromDOMNode(domSelection.focusNode);
2736
+ const isFocusOutside = focusNode && !tableNode.isParentOf(focusNode);
2737
+ const anchorNode = lexical.$getNearestNodeFromDOMNode(domSelection.anchorNode);
2738
+ const isAnchorInside = anchorNode && tableNode.isParentOf(anchorNode);
2739
+ if (isFocusOutside && isAnchorInside && domSelection.rangeCount > 0) {
2740
+ const newSelection = lexical.$createRangeSelectionFromDom(domSelection, editor);
2741
+ if (newSelection) {
2742
+ newSelection.anchor.set(tableNode.getKey(), selection.isBackward() ? tableNode.getChildrenSize() : 0, 'element');
2743
+ domSelection.removeAllRanges();
2744
+ lexical.$setSelection(newSelection);
2745
+ }
2746
+ }
2747
+ }
2748
+ }
2749
+ if (selection && !selection.is(prevSelection) && ($isTableSelection(selection) || $isTableSelection(prevSelection)) && tableObserver.tableSelection && !tableObserver.tableSelection.is(prevSelection)) {
2750
+ if ($isTableSelection(selection) && selection.tableKey === tableObserver.tableNodeKey) {
2751
+ tableObserver.$updateTableTableSelection(selection);
2752
+ } else if (!$isTableSelection(selection) && $isTableSelection(prevSelection) && prevSelection.tableKey === tableObserver.tableNodeKey) {
2753
+ tableObserver.$updateTableTableSelection(null);
2754
+ }
2755
+ return false;
2756
+ }
2757
+ if (tableObserver.hasHijackedSelectionStyles && !tableNode.isSelected()) {
2758
+ $removeHighlightStyleToTable(editor, tableObserver);
2759
+ } else if (!tableObserver.hasHijackedSelectionStyles && tableNode.isSelected()) {
2760
+ $addHighlightStyleToTable(editor, tableObserver);
2761
+ }
2762
+ return false;
2763
+ }, lexical.COMMAND_PRIORITY_HIGH));
2764
+ tableObserver.listenersToRemove.add(editor.registerCommand(lexical.INSERT_PARAGRAPH_COMMAND, () => {
2765
+ const selection = lexical.$getSelection();
2766
+ if (!lexical.$isRangeSelection(selection) || !selection.isCollapsed() || !$isSelectionInTable(selection, tableNode)) {
2767
+ return false;
2768
+ }
2769
+ const edgePosition = $getTableEdgeCursorPosition(editor, selection, tableNode);
2770
+ if (edgePosition) {
2771
+ $insertParagraphAtTableEdge(edgePosition, tableNode);
2772
+ return true;
2773
+ }
2774
+ return false;
2775
+ }, lexical.COMMAND_PRIORITY_HIGH));
2776
+ return tableObserver;
2777
+ }
2778
+ function detachTableObserverFromTableElement(tableElement, tableObserver) {
2779
+ if (getTableObserverFromTableElement(tableElement) === tableObserver) {
2780
+ delete tableElement[LEXICAL_ELEMENT_KEY];
2781
+ }
2782
+ }
2783
+ function attachTableObserverToTableElement(tableElement, tableObserver) {
2784
+ if (!(getTableObserverFromTableElement(tableElement) === null)) {
2785
+ formatDevErrorMessage(`tableElement already has an attached TableObserver`);
2786
+ }
2787
+ tableElement[LEXICAL_ELEMENT_KEY] = tableObserver;
2788
+ }
2789
+ function getTableObserverFromTableElement(tableElement) {
2790
+ return tableElement[LEXICAL_ELEMENT_KEY] || null;
2791
+ }
2792
+ function getDOMCellFromTarget(node) {
2793
+ let currentNode = node;
2794
+ while (currentNode != null) {
2795
+ const nodeName = currentNode.nodeName;
2796
+ if (nodeName === 'TD' || nodeName === 'TH') {
2797
+ // @ts-expect-error: internal field
2798
+ const cell = currentNode._cell;
2799
+ if (cell === undefined) {
2800
+ return null;
2801
+ }
2802
+ return cell;
2803
+ }
2804
+ currentNode = currentNode.parentNode;
2805
+ }
2806
+ return null;
2807
+ }
2808
+ function getDOMCellInTableFromTarget(table, node) {
2809
+ if (!table.contains(node)) {
2810
+ return null;
2811
+ }
2812
+ let cell = null;
2813
+ for (let currentNode = node; currentNode != null; currentNode = currentNode.parentNode) {
2814
+ if (currentNode === table) {
2815
+ return cell;
2816
+ }
2817
+ const nodeName = currentNode.nodeName;
2818
+ if (nodeName === 'TD' || nodeName === 'TH') {
2819
+ // @ts-expect-error: internal field
2820
+ cell = currentNode._cell || null;
2821
+ }
2822
+ }
2823
+ return null;
2824
+ }
2825
+ function getTable(tableNode, dom) {
2826
+ const tableElement = getTableElement(tableNode, dom);
2827
+ const domRows = [];
2828
+ const grid = {
2829
+ columns: 0,
2830
+ domRows,
2831
+ rows: 0
2832
+ };
2833
+ let currentNode = tableElement.querySelector('tr');
2834
+ let x = 0;
2835
+ let y = 0;
2836
+ domRows.length = 0;
2837
+ while (currentNode != null) {
2838
+ const nodeMame = currentNode.nodeName;
2839
+ if (nodeMame === 'TD' || nodeMame === 'TH') {
2840
+ const elem = currentNode;
2841
+ const cell = {
2842
+ elem,
2843
+ hasBackgroundColor: elem.style.backgroundColor !== '',
2844
+ highlighted: false,
2845
+ x,
2846
+ y
2847
+ };
2848
+
2849
+ // @ts-expect-error: internal field
2850
+ currentNode._cell = cell;
2851
+ let row = domRows[y];
2852
+ if (row === undefined) {
2853
+ row = domRows[y] = [];
2854
+ }
2855
+ row[x] = cell;
2856
+ } else {
2857
+ const child = currentNode.firstChild;
2858
+ if (child != null) {
2859
+ currentNode = child;
2860
+ continue;
2861
+ }
2862
+ }
2863
+ const sibling = currentNode.nextSibling;
2864
+ if (sibling != null) {
2865
+ x++;
2866
+ currentNode = sibling;
2867
+ continue;
2868
+ }
2869
+ const parent = currentNode.parentNode;
2870
+ if (parent != null) {
2871
+ const parentSibling = parent.nextSibling;
2872
+ if (parentSibling == null) {
2873
+ break;
2874
+ }
2875
+ y++;
2876
+ x = 0;
2877
+ currentNode = parentSibling;
2878
+ }
2879
+ }
2880
+ grid.columns = x + 1;
2881
+ grid.rows = y + 1;
2882
+ return grid;
2883
+ }
2884
+ function $updateDOMForSelection(editor, table, selection) {
2885
+ const selectedCellNodes = new Set(selection ? selection.getNodes() : []);
2886
+ $forEachTableCell(table, (cell, lexicalNode) => {
2887
+ const elem = cell.elem;
2888
+ if (selectedCellNodes.has(lexicalNode)) {
2889
+ cell.highlighted = true;
2890
+ $addHighlightToDOM(editor, cell);
2891
+ } else {
2892
+ cell.highlighted = false;
2893
+ $removeHighlightFromDOM(editor, cell);
2894
+ if (!elem.getAttribute('style')) {
2895
+ elem.removeAttribute('style');
2896
+ }
2897
+ }
2898
+ });
2899
+ }
2900
+ function $forEachTableCell(grid, cb) {
2901
+ const {
2902
+ domRows
2903
+ } = grid;
2904
+ for (let y = 0; y < domRows.length; y++) {
2905
+ const row = domRows[y];
2906
+ if (!row) {
2907
+ continue;
2908
+ }
2909
+ for (let x = 0; x < row.length; x++) {
2910
+ const cell = row[x];
2911
+ if (!cell) {
2912
+ continue;
2913
+ }
2914
+ const lexicalNode = lexical.$getNearestNodeFromDOMNode(cell.elem);
2915
+ if (lexicalNode !== null) {
2916
+ cb(cell, lexicalNode, {
2917
+ x,
2918
+ y
2919
+ });
2920
+ }
2921
+ }
2922
+ }
2923
+ }
2924
+ function $addHighlightStyleToTable(editor, tableSelection) {
2925
+ tableSelection.$disableHighlightStyle();
2926
+ $forEachTableCell(tableSelection.table, cell => {
2927
+ cell.highlighted = true;
2928
+ $addHighlightToDOM(editor, cell);
2929
+ });
2930
+ }
2931
+ function $removeHighlightStyleToTable(editor, tableObserver) {
2932
+ tableObserver.$enableHighlightStyle();
2933
+ $forEachTableCell(tableObserver.table, cell => {
2934
+ const elem = cell.elem;
2935
+ cell.highlighted = false;
2936
+ $removeHighlightFromDOM(editor, cell);
2937
+ if (!elem.getAttribute('style')) {
2938
+ elem.removeAttribute('style');
2939
+ }
2940
+ });
2941
+ }
2942
+ function $selectAdjacentCell(tableCellNode, direction) {
2943
+ const siblingMethod = direction === 'next' ? 'getNextSibling' : 'getPreviousSibling';
2944
+ const childMethod = direction === 'next' ? 'getFirstChild' : 'getLastChild';
2945
+ const sibling = tableCellNode[siblingMethod]();
2946
+ if (lexical.$isElementNode(sibling)) {
2947
+ return sibling.selectEnd();
2948
+ }
2949
+ const parentRow = lexicalUtils.$findMatchingParent(tableCellNode, $isTableRowNode);
2950
+ if (!(parentRow !== null)) {
2951
+ formatDevErrorMessage(`selectAdjacentCell: Cell not in table row`);
2952
+ }
2953
+ for (let nextRow = parentRow[siblingMethod](); $isTableRowNode(nextRow); nextRow = nextRow[siblingMethod]()) {
2954
+ const child = nextRow[childMethod]();
2955
+ if (lexical.$isElementNode(child)) {
2956
+ return child.selectEnd();
2957
+ }
2958
+ }
2959
+ const parentTable = lexicalUtils.$findMatchingParent(parentRow, $isTableNode);
2960
+ if (!(parentTable !== null)) {
2961
+ formatDevErrorMessage(`selectAdjacentCell: Row not in table`);
2962
+ }
2963
+ return direction === 'next' ? parentTable.selectNext() : parentTable.selectPrevious();
2964
+ }
2965
+ const selectTableNodeInDirection = (tableObserver, tableNode, x, y, direction) => {
2966
+ const isForward = direction === 'forward';
2967
+ switch (direction) {
2968
+ case 'backward':
2969
+ case 'forward':
2970
+ if (x !== (isForward ? tableObserver.table.columns - 1 : 0)) {
2971
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x + (isForward ? 1 : -1), y, tableObserver.table), isForward);
2972
+ } else {
2973
+ if (y !== (isForward ? tableObserver.table.rows - 1 : 0)) {
2974
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(isForward ? 0 : tableObserver.table.columns - 1, y + (isForward ? 1 : -1), tableObserver.table), isForward);
2975
+ } else if (!isForward) {
2976
+ tableNode.selectPrevious();
2977
+ } else {
2978
+ tableNode.selectNext();
2979
+ }
2980
+ }
2981
+ return true;
2982
+ case 'up':
2983
+ if (y !== 0) {
2984
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table), false);
2985
+ } else {
2986
+ tableNode.selectPrevious();
2987
+ }
2988
+ return true;
2989
+ case 'down':
2990
+ if (y !== tableObserver.table.rows - 1) {
2991
+ selectTableCellNode(tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table), true);
2992
+ } else {
2993
+ tableNode.selectNext();
2994
+ }
2995
+ return true;
2996
+ default:
2997
+ return false;
2998
+ }
2999
+ };
3000
+ function getCorner(rect, cellValue) {
3001
+ let colName;
3002
+ let rowName;
3003
+ if (cellValue.startColumn === rect.minColumn) {
3004
+ colName = 'minColumn';
3005
+ } else if (cellValue.startColumn + cellValue.cell.__colSpan - 1 === rect.maxColumn) {
3006
+ colName = 'maxColumn';
3007
+ } else {
3008
+ return null;
3009
+ }
3010
+ if (cellValue.startRow === rect.minRow) {
3011
+ rowName = 'minRow';
3012
+ } else if (cellValue.startRow + cellValue.cell.__rowSpan - 1 === rect.maxRow) {
3013
+ rowName = 'maxRow';
3014
+ } else {
3015
+ return null;
3016
+ }
3017
+ return [colName, rowName];
3018
+ }
3019
+ function getCornerOrThrow(rect, cellValue) {
3020
+ const corner = getCorner(rect, cellValue);
3021
+ if (!(corner !== null)) {
3022
+ formatDevErrorMessage(`getCornerOrThrow: cell ${cellValue.cell.getKey()} is not at a corner of rect`);
3023
+ }
3024
+ return corner;
3025
+ }
3026
+ function oppositeCorner([colName, rowName]) {
3027
+ return [colName === 'minColumn' ? 'maxColumn' : 'minColumn', rowName === 'minRow' ? 'maxRow' : 'minRow'];
3028
+ }
3029
+ function cellAtCornerOrThrow(tableMap, rect, [colName, rowName]) {
3030
+ const rowNum = rect[rowName];
3031
+ const rowMap = tableMap[rowNum];
3032
+ if (!(rowMap !== undefined)) {
3033
+ formatDevErrorMessage(`cellAtCornerOrThrow: ${rowName} = ${String(rowNum)} missing in tableMap`);
3034
+ }
3035
+ const colNum = rect[colName];
3036
+ const cell = rowMap[colNum];
3037
+ if (!(cell !== undefined)) {
3038
+ formatDevErrorMessage(`cellAtCornerOrThrow: ${colName} = ${String(colNum)} missing in tableMap`);
3039
+ }
3040
+ return cell;
3041
+ }
3042
+ function $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue) {
3043
+ // We are sure that the focus now either contracts or expands the rect
3044
+ // but both the anchor and focus might be moved to ensure a rectangle
3045
+ // given a potentially ragged merge shape
3046
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, newFocusCellValue);
3047
+ const anchorCorner = getCorner(rect, anchorCellValue);
3048
+ if (anchorCorner) {
3049
+ return [cellAtCornerOrThrow(tableMap, rect, anchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(anchorCorner))];
3050
+ }
3051
+ const newFocusCorner = getCorner(rect, newFocusCellValue);
3052
+ if (newFocusCorner) {
3053
+ return [cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newFocusCorner)), cellAtCornerOrThrow(tableMap, rect, newFocusCorner)];
3054
+ }
3055
+ // TODO this doesn't have to be arbitrary, use the closest corner instead
3056
+ const newAnchorCorner = ['minColumn', 'minRow'];
3057
+ return [cellAtCornerOrThrow(tableMap, rect, newAnchorCorner), cellAtCornerOrThrow(tableMap, rect, oppositeCorner(newAnchorCorner))];
3058
+ }
3059
+ function $adjustFocusInDirection(tableObserver, tableMap, anchorCellValue, focusCellValue, direction) {
3060
+ const rect = $computeTableCellRectBoundary(tableMap, anchorCellValue, focusCellValue);
3061
+ const spans = $computeTableCellRectSpans(tableMap, rect);
3062
+ const {
3063
+ topSpan,
3064
+ leftSpan,
3065
+ bottomSpan,
3066
+ rightSpan
3067
+ } = spans;
3068
+ const anchorCorner = getCornerOrThrow(rect, anchorCellValue);
3069
+ const [focusColumn, focusRow] = oppositeCorner(anchorCorner);
3070
+ let fCol = rect[focusColumn];
3071
+ let fRow = rect[focusRow];
3072
+ if (direction === 'forward') {
3073
+ fCol += focusColumn === 'maxColumn' ? 1 : leftSpan;
3074
+ } else if (direction === 'backward') {
3075
+ fCol -= focusColumn === 'minColumn' ? 1 : rightSpan;
3076
+ } else if (direction === 'down') {
3077
+ fRow += focusRow === 'maxRow' ? 1 : topSpan;
3078
+ } else if (direction === 'up') {
3079
+ fRow -= focusRow === 'minRow' ? 1 : bottomSpan;
3080
+ }
3081
+ const targetRowMap = tableMap[fRow];
3082
+ if (targetRowMap === undefined) {
3083
+ return false;
3084
+ }
3085
+ const newFocusCellValue = targetRowMap[fCol];
3086
+ if (newFocusCellValue === undefined) {
3087
+ return false;
3088
+ }
3089
+ // We can be certain that anchorCellValue and newFocusCellValue are
3090
+ // contained within the desired selection, but we are not certain if
3091
+ // they need to be expanded or not to maintain a rectangular shape
3092
+ const [finalAnchorCell, finalFocusCell] = $extractRectCorners(tableMap, anchorCellValue, newFocusCellValue);
3093
+ const anchorDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalAnchorCell.cell);
3094
+ const focusDOM = $getObserverCellFromCellNodeOrThrow(tableObserver, finalFocusCell.cell);
3095
+ tableObserver.$setAnchorCellForSelection(anchorDOM);
3096
+ tableObserver.$setFocusCellForSelection(focusDOM, true);
3097
+ return true;
3098
+ }
3099
+ function $isSelectionInTable(selection, tableNode) {
3100
+ if (lexical.$isRangeSelection(selection) || $isTableSelection(selection)) {
3101
+ // TODO this should probably return false if there's an unrelated
3102
+ // shadow root between the node and the table (e.g. another table,
3103
+ // collapsible, etc.)
3104
+ const isAnchorInside = tableNode.isParentOf(selection.anchor.getNode());
3105
+ const isFocusInside = tableNode.isParentOf(selection.focus.getNode());
3106
+ return isAnchorInside && isFocusInside;
3107
+ }
3108
+ return false;
3109
+ }
3110
+ function $isFullTableSelection(selection, tableNode) {
3111
+ if ($isTableSelection(selection)) {
3112
+ const anchorNode = selection.anchor.getNode();
3113
+ const focusNode = selection.focus.getNode();
3114
+ if (tableNode && anchorNode && focusNode) {
3115
+ const [map] = $computeTableMap(tableNode, anchorNode, focusNode);
3116
+ return anchorNode.getKey() === map[0][0].cell.getKey() && focusNode.getKey() === map[map.length - 1].at(-1).cell.getKey();
3117
+ }
3118
+ }
3119
+ return false;
3120
+ }
3121
+ function selectTableCellNode(tableCell, fromStart) {
3122
+ if (fromStart) {
3123
+ tableCell.selectStart();
3124
+ } else {
3125
+ tableCell.selectEnd();
3126
+ }
3127
+ }
3128
+ function $addHighlightToDOM(editor, cell) {
3129
+ const element = cell.elem;
3130
+ const editorThemeClasses = editor._config.theme;
3131
+ const node = lexical.$getNearestNodeFromDOMNode(element);
3132
+ if (!$isTableCellNode(node)) {
3133
+ formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
3134
+ }
3135
+ lexicalUtils.addClassNamesToElement(element, editorThemeClasses.tableCellSelected);
3136
+ }
3137
+ function $removeHighlightFromDOM(editor, cell) {
3138
+ const element = cell.elem;
3139
+ const node = lexical.$getNearestNodeFromDOMNode(element);
3140
+ if (!$isTableCellNode(node)) {
3141
+ formatDevErrorMessage(`Expected to find LexicalNode from Table Cell DOMNode`);
3142
+ }
3143
+ const editorThemeClasses = editor._config.theme;
3144
+ lexicalUtils.removeClassNamesFromElement(element, editorThemeClasses.tableCellSelected);
3145
+ }
3146
+ function $findCellNode(node) {
3147
+ const cellNode = lexicalUtils.$findMatchingParent(node, $isTableCellNode);
3148
+ return $isTableCellNode(cellNode) ? cellNode : null;
3149
+ }
3150
+ function $findTableNode(node) {
3151
+ const tableNode = lexicalUtils.$findMatchingParent(node, $isTableNode);
3152
+ return $isTableNode(tableNode) ? tableNode : null;
3153
+ }
3154
+ function $getBlockParentIfFirstNode(node) {
3155
+ for (let prevNode = node, currentNode = node; currentNode !== null; prevNode = currentNode, currentNode = currentNode.getParent()) {
3156
+ if (lexical.$isElementNode(currentNode)) {
3157
+ if (currentNode !== prevNode && currentNode.getFirstChild() !== prevNode) {
3158
+ // Not the first child or the initial node
3159
+ return null;
3160
+ } else if (!currentNode.isInline()) {
3161
+ return currentNode;
3162
+ }
3163
+ }
3164
+ }
3165
+ return null;
3166
+ }
3167
+ function $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, isBackward, tableNode, tableObserver) {
3168
+ const initialFocus = lexical.$caretFromPoint(selection.focus, isBackward ? 'previous' : 'next');
3169
+ if (lexical.$isExtendableTextPointCaret(initialFocus)) {
3170
+ return false;
3171
+ }
3172
+ let lastCaret = initialFocus;
3173
+ // TableCellNode is the only shadow root we are interested in piercing so
3174
+ // we find the last internal caret and then check its parent
3175
+ for (const nextCaret of lexical.$extendCaretToRange(initialFocus).iterNodeCarets('shadowRoot')) {
3176
+ if (!(lexical.$isSiblingCaret(nextCaret) && lexical.$isElementNode(nextCaret.origin))) {
3177
+ return false;
3178
+ }
3179
+ lastCaret = nextCaret;
3180
+ }
3181
+ const lastCaretParent = lastCaret.getParentAtCaret();
3182
+ if (!$isTableCellNode(lastCaretParent)) {
3183
+ return false;
3184
+ }
3185
+ const anchorCell = lastCaretParent;
3186
+ const focusCaret = $findNextTableCell(lexical.$getSiblingCaret(anchorCell, lastCaret.direction));
3187
+ const anchorCellTable = lexicalUtils.$findMatchingParent(anchorCell, $isTableNode);
3188
+ if (!(anchorCellTable && anchorCellTable.is(tableNode))) {
3189
+ return false;
3190
+ }
3191
+ const anchorCellDOM = editor.getElementByKey(anchorCell.getKey());
3192
+ const anchorDOMCell = getDOMCellFromTarget(anchorCellDOM);
3193
+ if (!anchorCellDOM || !anchorDOMCell) {
3194
+ return false;
3195
+ }
3196
+ const anchorCellTableElement = $getElementForTableNode(editor, anchorCellTable);
3197
+ tableObserver.table = anchorCellTableElement;
3198
+ if (!focusCaret) {
3199
+ if (alter === 'extend') {
3200
+ // extend the selection from a range inside the cell to a table selection of the cell
3201
+ tableObserver.$setAnchorCellForSelection(anchorDOMCell);
3202
+ tableObserver.$setFocusCellForSelection(anchorDOMCell, true);
3203
+ } else {
3204
+ // exit the table
3205
+ const outerFocusCaret = $getTableExitCaret(lexical.$getSiblingCaret(anchorCellTable, initialFocus.direction));
3206
+ lexical.$setPointFromCaret(selection.anchor, outerFocusCaret);
3207
+ lexical.$setPointFromCaret(selection.focus, outerFocusCaret);
3208
+ }
3209
+ } else if (alter === 'extend') {
3210
+ const focusDOMCell = getDOMCellFromTarget(editor.getElementByKey(focusCaret.origin.getKey()));
3211
+ if (!focusDOMCell) {
3212
+ return false;
3213
+ }
3214
+ tableObserver.$setAnchorCellForSelection(anchorDOMCell);
3215
+ tableObserver.$setFocusCellForSelection(focusDOMCell, true);
3216
+ } else {
3217
+ // alter === 'move'
3218
+ const innerFocusCaret = lexical.$normalizeCaret(focusCaret);
3219
+ lexical.$setPointFromCaret(selection.anchor, innerFocusCaret);
3220
+ lexical.$setPointFromCaret(selection.focus, innerFocusCaret);
3221
+ }
3222
+ stopEvent(event);
3223
+ return true;
3224
+ }
3225
+ function $getTableExitCaret(initialCaret) {
3226
+ const adjacent = lexical.$getAdjacentChildCaret(initialCaret);
3227
+ return lexical.$isChildCaret(adjacent) ? lexical.$normalizeCaret(adjacent) : initialCaret;
3228
+ }
3229
+ function $findNextTableCell(initialCaret) {
3230
+ for (const nextCaret of lexical.$extendCaretToRange(initialCaret).iterNodeCarets('root')) {
3231
+ const {
3232
+ origin
3233
+ } = nextCaret;
3234
+ if ($isTableCellNode(origin)) {
3235
+ // not sure why ts isn't narrowing here (even if the guard is on nextCaret.origin)
3236
+ // but returning a new caret is fine
3237
+ if (lexical.$isChildCaret(nextCaret)) {
3238
+ return lexical.$getChildCaret(origin, initialCaret.direction);
3239
+ }
3240
+ } else if (!$isTableRowNode(origin)) {
3241
+ break;
3242
+ }
3243
+ }
3244
+ return null;
3245
+ }
3246
+ function $handleArrowKey(editor, event, direction, tableNode, tableObserver) {
3247
+ if ((direction === 'up' || direction === 'down') && isTypeaheadMenuInView(editor)) {
3248
+ return false;
3249
+ }
3250
+ const selection = lexical.$getSelection();
3251
+ if (!$isSelectionInTable(selection, tableNode)) {
3252
+ if (lexical.$isRangeSelection(selection)) {
3253
+ if (direction === 'backward') {
3254
+ if (selection.focus.offset > 0) {
3255
+ return false;
3256
+ }
3257
+ const parentNode = $getBlockParentIfFirstNode(selection.focus.getNode());
3258
+ if (!parentNode) {
3259
+ return false;
3260
+ }
3261
+ const siblingNode = parentNode.getPreviousSibling();
3262
+ if (!$isTableNode(siblingNode)) {
3263
+ return false;
3264
+ }
3265
+ stopEvent(event);
3266
+ if (event.shiftKey) {
3267
+ selection.focus.set(siblingNode.getParentOrThrow().getKey(), siblingNode.getIndexWithinParent(), 'element');
3268
+ } else {
3269
+ siblingNode.selectEnd();
3270
+ }
3271
+ return true;
3272
+ } else if (event.shiftKey && (direction === 'up' || direction === 'down')) {
3273
+ const focusNode = selection.focus.getNode();
3274
+ const isTableUnselect = !selection.isCollapsed() && (direction === 'up' && !selection.isBackward() || direction === 'down' && selection.isBackward());
3275
+ if (isTableUnselect) {
3276
+ let focusParentNode = lexicalUtils.$findMatchingParent(focusNode, n => $isTableNode(n));
3277
+ if ($isTableCellNode(focusParentNode)) {
3278
+ focusParentNode = lexicalUtils.$findMatchingParent(focusParentNode, $isTableNode);
3279
+ }
3280
+ if (focusParentNode !== tableNode) {
3281
+ return false;
3282
+ }
3283
+ if (!focusParentNode) {
3284
+ return false;
3285
+ }
3286
+ const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
3287
+ if (!sibling) {
3288
+ return false;
3289
+ }
3290
+ let newOffset = 0;
3291
+ if (direction === 'up') {
3292
+ if (lexical.$isElementNode(sibling)) {
3293
+ newOffset = sibling.getChildrenSize();
3294
+ }
3295
+ }
3296
+ let newFocusNode = sibling;
3297
+ if (direction === 'up') {
3298
+ if (lexical.$isElementNode(sibling)) {
3299
+ const lastCell = sibling.getLastChild();
3300
+ newFocusNode = lastCell ? lastCell : sibling;
3301
+ newOffset = lexical.$isTextNode(newFocusNode) ? newFocusNode.getTextContentSize() : 0;
3302
+ }
3303
+ }
3304
+ const newSelection = selection.clone();
3305
+ newSelection.focus.set(newFocusNode.getKey(), newOffset, lexical.$isTextNode(newFocusNode) ? 'text' : 'element');
3306
+ lexical.$setSelection(newSelection);
3307
+ stopEvent(event);
3308
+ return true;
3309
+ } else if (lexical.$isRootOrShadowRoot(focusNode)) {
3310
+ const selectedNode = direction === 'up' ? selection.getNodes()[selection.getNodes().length - 1] : selection.getNodes()[0];
3311
+ if (selectedNode) {
3312
+ const tableCellNode = $findParentTableCellNodeInTable(tableNode, selectedNode);
3313
+ if (tableCellNode !== null) {
3314
+ const firstDescendant = tableNode.getFirstDescendant();
3315
+ const lastDescendant = tableNode.getLastDescendant();
3316
+ if (!firstDescendant || !lastDescendant) {
3317
+ return false;
3318
+ }
3319
+ const [firstCellNode] = $getNodeTriplet(firstDescendant);
3320
+ const [lastCellNode] = $getNodeTriplet(lastDescendant);
3321
+ const firstCellCoords = tableNode.getCordsFromCellNode(firstCellNode, tableObserver.table);
3322
+ const lastCellCoords = tableNode.getCordsFromCellNode(lastCellNode, tableObserver.table);
3323
+ const firstCellDOM = tableNode.getDOMCellFromCordsOrThrow(firstCellCoords.x, firstCellCoords.y, tableObserver.table);
3324
+ const lastCellDOM = tableNode.getDOMCellFromCordsOrThrow(lastCellCoords.x, lastCellCoords.y, tableObserver.table);
3325
+ tableObserver.$setAnchorCellForSelection(firstCellDOM);
3326
+ tableObserver.$setFocusCellForSelection(lastCellDOM, true);
3327
+ return true;
3328
+ }
3329
+ }
3330
+ return false;
3331
+ } else {
3332
+ let focusParentNode = lexicalUtils.$findMatchingParent(focusNode, n => lexical.$isElementNode(n) && !n.isInline());
3333
+ if ($isTableCellNode(focusParentNode)) {
3334
+ focusParentNode = lexicalUtils.$findMatchingParent(focusParentNode, $isTableNode);
3335
+ }
3336
+ if (!focusParentNode) {
3337
+ return false;
3338
+ }
3339
+ const sibling = direction === 'down' ? focusParentNode.getNextSibling() : focusParentNode.getPreviousSibling();
3340
+ if ($isTableNode(sibling) && tableObserver.tableNodeKey === sibling.getKey()) {
3341
+ const firstDescendant = sibling.getFirstDescendant();
3342
+ const lastDescendant = sibling.getLastDescendant();
3343
+ if (!firstDescendant || !lastDescendant) {
3344
+ return false;
3345
+ }
3346
+ const [firstCellNode] = $getNodeTriplet(firstDescendant);
3347
+ const [lastCellNode] = $getNodeTriplet(lastDescendant);
3348
+ const newSelection = selection.clone();
3349
+ newSelection.focus.set((direction === 'up' ? firstCellNode : lastCellNode).getKey(), direction === 'up' ? 0 : lastCellNode.getChildrenSize(), 'element');
3350
+ stopEvent(event);
3351
+ lexical.$setSelection(newSelection);
3352
+ return true;
3353
+ }
3354
+ }
3355
+ }
3356
+ }
3357
+ if (direction === 'down' && $isScrollableTablesActive(editor)) {
3358
+ // Enable Firefox workaround
3359
+ tableObserver.setShouldCheckSelection();
3360
+ }
3361
+ return false;
3362
+ }
3363
+ if (lexical.$isRangeSelection(selection)) {
3364
+ if (direction === 'backward' || direction === 'forward') {
3365
+ const alter = event.shiftKey ? 'extend' : 'move';
3366
+ return $handleHorizontalArrowKeyRangeSelection(editor, event, selection, alter, direction === 'backward', tableNode, tableObserver);
3367
+ }
3368
+ if (selection.isCollapsed()) {
3369
+ const {
3370
+ anchor,
3371
+ focus
3372
+ } = selection;
3373
+ const anchorCellNode = lexicalUtils.$findMatchingParent(anchor.getNode(), $isTableCellNode);
3374
+ const focusCellNode = lexicalUtils.$findMatchingParent(focus.getNode(), $isTableCellNode);
3375
+ if (!$isTableCellNode(anchorCellNode) || !anchorCellNode.is(focusCellNode)) {
3376
+ return false;
3377
+ }
3378
+ const anchorCellTable = $findTableNode(anchorCellNode);
3379
+ if (anchorCellTable !== tableNode && anchorCellTable != null) {
3380
+ const anchorCellTableElement = getTableElement(anchorCellTable, editor.getElementByKey(anchorCellTable.getKey()));
3381
+ if (anchorCellTableElement != null) {
3382
+ tableObserver.table = getTable(anchorCellTable, anchorCellTableElement);
3383
+ return $handleArrowKey(editor, event, direction, anchorCellTable, tableObserver);
3384
+ }
3385
+ }
3386
+ const anchorCellDom = editor.getElementByKey(anchorCellNode.__key);
3387
+ const anchorDOM = editor.getElementByKey(anchor.key);
3388
+ if (anchorDOM == null || anchorCellDom == null) {
3389
+ return false;
3390
+ }
3391
+ let edgeSelectionRect;
3392
+ if (anchor.type === 'element') {
3393
+ edgeSelectionRect = anchorDOM.getBoundingClientRect();
3394
+ } else {
3395
+ const domSelection = lexical.getDOMSelectionForEditor(editor);
3396
+ if (domSelection === null || domSelection.rangeCount === 0) {
3397
+ return false;
3398
+ }
3399
+ const range = domSelection.getRangeAt(0);
3400
+ edgeSelectionRect = range.getBoundingClientRect();
3401
+ }
3402
+ const edgeChild = direction === 'up' ? anchorCellNode.getFirstChild() : anchorCellNode.getLastChild();
3403
+ if (edgeChild == null) {
3404
+ return false;
3405
+ }
3406
+ const edgeChildDOM = editor.getElementByKey(edgeChild.__key);
3407
+ if (edgeChildDOM == null) {
3408
+ return false;
3409
+ }
3410
+ const edgeRect = edgeChildDOM.getBoundingClientRect();
3411
+ const isExiting = direction === 'up' ? edgeRect.top > edgeSelectionRect.top - edgeSelectionRect.height : edgeSelectionRect.bottom + edgeSelectionRect.height > edgeRect.bottom;
3412
+ if (isExiting) {
3413
+ stopEvent(event);
3414
+ const cords = tableNode.getCordsFromCellNode(anchorCellNode, tableObserver.table);
3415
+ if (event.shiftKey) {
3416
+ const cell = tableNode.getDOMCellFromCordsOrThrow(cords.x, cords.y, tableObserver.table);
3417
+ tableObserver.$setAnchorCellForSelection(cell);
3418
+ tableObserver.$setFocusCellForSelection(cell, true);
3419
+ } else {
3420
+ return selectTableNodeInDirection(tableObserver, tableNode, cords.x, cords.y, direction);
3421
+ }
3422
+ return true;
3423
+ }
3424
+ }
3425
+ } else if ($isTableSelection(selection)) {
3426
+ const {
3427
+ anchor,
3428
+ focus
3429
+ } = selection;
3430
+ const anchorCellNode = lexicalUtils.$findMatchingParent(anchor.getNode(), $isTableCellNode);
3431
+ const focusCellNode = lexicalUtils.$findMatchingParent(focus.getNode(), $isTableCellNode);
3432
+ const [tableNodeFromSelection] = selection.getNodes();
3433
+ if (!$isTableNode(tableNodeFromSelection)) {
3434
+ formatDevErrorMessage(`$handleArrowKey: TableSelection.getNodes()[0] expected to be TableNode`);
3435
+ }
3436
+ const tableElement = getTableElement(tableNodeFromSelection, editor.getElementByKey(tableNodeFromSelection.getKey()));
3437
+ if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableNode(tableNodeFromSelection) || tableElement == null) {
3438
+ return false;
3439
+ }
3440
+ tableObserver.$updateTableTableSelection(selection);
3441
+ const grid = getTable(tableNodeFromSelection, tableElement);
3442
+ const cordsAnchor = tableNode.getCordsFromCellNode(anchorCellNode, grid);
3443
+ const anchorCell = tableNode.getDOMCellFromCordsOrThrow(cordsAnchor.x, cordsAnchor.y, grid);
3444
+ tableObserver.$setAnchorCellForSelection(anchorCell);
3445
+ stopEvent(event);
3446
+ if (event.shiftKey) {
3447
+ const [tableMap, anchorValue, focusValue] = $computeTableMap(tableNode, anchorCellNode, focusCellNode);
3448
+ return $adjustFocusInDirection(tableObserver, tableMap, anchorValue, focusValue, direction);
3449
+ } else {
3450
+ focusCellNode.selectEnd();
3451
+ }
3452
+ return true;
3453
+ }
3454
+ return false;
3455
+ }
3456
+ function stopEvent(event) {
3457
+ event.preventDefault();
3458
+ event.stopImmediatePropagation();
3459
+ event.stopPropagation();
3460
+ }
3461
+ function isTypeaheadMenuInView(editor) {
3462
+ // There is no inbuilt way to check if the component picker is in view
3463
+ // but we can check if the root DOM element has the aria-controls attribute "typeahead-menu".
3464
+ const root = editor.getRootElement();
3465
+ if (!root) {
3466
+ return false;
3467
+ }
3468
+ return root.hasAttribute('aria-controls') && root.getAttribute('aria-controls') === 'typeahead-menu';
3469
+ }
3470
+ function $insertParagraphAtTableEdge(edgePosition, tableNode, children) {
3471
+ const paragraphNode = lexical.$createParagraphNode();
3472
+ if (edgePosition === 'first') {
3473
+ tableNode.insertBefore(paragraphNode);
3474
+ } else {
3475
+ tableNode.insertAfter(paragraphNode);
3476
+ }
3477
+ paragraphNode.append(...(children || []));
3478
+ paragraphNode.selectEnd();
3479
+ }
3480
+ function $getTableEdgeCursorPosition(editor, selection, tableNode) {
3481
+ const tableNodeParent = tableNode.getParent();
3482
+ if (!tableNodeParent) {
3483
+ return undefined;
3484
+ }
3485
+
3486
+ // TODO: Add support for nested tables
3487
+ const domSelection = lexical.getDOMSelectionForEditor(editor);
3488
+ if (!domSelection) {
3489
+ return undefined;
3490
+ }
3491
+ const domAnchorNode = domSelection.anchorNode;
3492
+ const tableNodeParentDOM = editor.getElementByKey(tableNodeParent.getKey());
3493
+ const tableElement = getTableElement(tableNode, editor.getElementByKey(tableNode.getKey()));
3494
+ // We are only interested in the scenario where the
3495
+ // native selection anchor is:
3496
+ // - at or inside the table's parent DOM
3497
+ // - and NOT at or inside the table DOM
3498
+ // It may be adjacent to the table DOM (e.g. in a wrapper)
3499
+ if (!domAnchorNode || !tableNodeParentDOM || !tableElement || !tableNodeParentDOM.contains(domAnchorNode) || tableElement.contains(domAnchorNode)) {
3500
+ return undefined;
3501
+ }
3502
+ const anchorCellNode = lexicalUtils.$findMatchingParent(selection.anchor.getNode(), n => $isTableCellNode(n));
3503
+ if (!anchorCellNode) {
3504
+ return undefined;
3505
+ }
3506
+ const parentTable = lexicalUtils.$findMatchingParent(anchorCellNode, n => $isTableNode(n));
3507
+ if (!$isTableNode(parentTable) || !parentTable.is(tableNode)) {
3508
+ return undefined;
3509
+ }
3510
+ const [tableMap, cellValue] = $computeTableMap(tableNode, anchorCellNode, anchorCellNode);
3511
+ const firstCell = tableMap[0][0];
3512
+ const lastCell = tableMap[tableMap.length - 1][tableMap[0].length - 1];
3513
+ const {
3514
+ startRow,
3515
+ startColumn
3516
+ } = cellValue;
3517
+ const isAtFirstCell = startRow === firstCell.startRow && startColumn === firstCell.startColumn;
3518
+ const isAtLastCell = startRow === lastCell.startRow && startColumn === lastCell.startColumn;
3519
+ if (isAtFirstCell) {
3520
+ return 'first';
3521
+ } else if (isAtLastCell) {
3522
+ return 'last';
3523
+ } else {
3524
+ return undefined;
3525
+ }
3526
+ }
3527
+ function $getObserverCellFromCellNodeOrThrow(tableObserver, tableCellNode) {
3528
+ const {
3529
+ tableNode
3530
+ } = tableObserver.$lookup();
3531
+ const currentCords = tableNode.getCordsFromCellNode(tableCellNode, tableObserver.table);
3532
+ return tableNode.getDOMCellFromCordsOrThrow(currentCords.x, currentCords.y, tableObserver.table);
3533
+ }
3534
+ function $getNearestTableCellInTableFromDOMNode(tableNode, startingDOM, editorState) {
3535
+ return $findParentTableCellNodeInTable(tableNode, lexical.$getNearestNodeFromDOMNode(startingDOM, editorState));
3536
+ }
3537
+
3538
+ function isHTMLDivElement(element) {
3539
+ return lexicalUtils.isHTMLElement(element) && element.nodeName === 'DIV';
3540
+ }
3541
+ function updateColgroup(dom, config, colCount, colWidths) {
3542
+ const colGroup = dom.querySelector('colgroup');
3543
+ if (!colGroup) {
3544
+ return;
3545
+ }
3546
+ const cols = [];
3547
+ for (let i = 0; i < colCount; i++) {
3548
+ const col = document.createElement('col');
3549
+ const width = colWidths && colWidths[i];
3550
+ if (width) {
3551
+ col.style.width = `${width}px`;
3552
+ }
3553
+ cols.push(col);
3554
+ }
3555
+ colGroup.replaceChildren(...cols);
3556
+ }
3557
+ function setRowStriping(dom, config, rowStriping) {
3558
+ if (rowStriping) {
3559
+ lexicalUtils.addClassNamesToElement(dom, config.theme.tableRowStriping);
3560
+ dom.setAttribute('data-lexical-row-striping', 'true');
3561
+ } else {
3562
+ lexicalUtils.removeClassNamesFromElement(dom, config.theme.tableRowStriping);
3563
+ dom.removeAttribute('data-lexical-row-striping');
3564
+ }
3565
+ }
3566
+ function setFrozenColumns(dom, tableElement, config, frozenColumnCount) {
3567
+ if (frozenColumnCount > 0) {
3568
+ lexicalUtils.addClassNamesToElement(dom, config.theme.tableFrozenColumn);
3569
+ tableElement.setAttribute('data-lexical-frozen-column', 'true');
3570
+ } else {
3571
+ lexicalUtils.removeClassNamesFromElement(dom, config.theme.tableFrozenColumn);
3572
+ tableElement.removeAttribute('data-lexical-frozen-column');
3573
+ }
3574
+ }
3575
+ function setFrozenRows(dom, tableElement, config, frozenRowCount) {
3576
+ if (frozenRowCount > 0) {
3577
+ lexicalUtils.addClassNamesToElement(dom, config.theme.tableFrozenRow);
3578
+ tableElement.setAttribute('data-lexical-frozen-row', 'true');
3579
+ } else {
3580
+ lexicalUtils.removeClassNamesFromElement(dom, config.theme.tableFrozenRow);
3581
+ tableElement.removeAttribute('data-lexical-frozen-row');
3582
+ }
3583
+ }
3584
+ function alignTableElement(dom, config, formatType) {
3585
+ if (!config.theme.tableAlignment) {
3586
+ return;
3587
+ }
3588
+ const removeClasses = [];
3589
+ const addClasses = [];
3590
+ for (const format of ['center', 'right']) {
3591
+ const classes = config.theme.tableAlignment[format];
3592
+ if (!classes) {
3593
+ continue;
3594
+ }
3595
+ (format === formatType ? addClasses : removeClasses).push(classes);
3596
+ }
3597
+ lexicalUtils.removeClassNamesFromElement(dom, ...removeClasses);
3598
+ lexicalUtils.addClassNamesToElement(dom, ...addClasses);
3599
+ }
3600
+ const scrollableEditors = new WeakSet();
3601
+ function $isScrollableTablesActive(editor = lexical.$getEditor()) {
3602
+ return scrollableEditors.has(editor);
3603
+ }
3604
+ function setScrollableTablesActive(editor, active) {
3605
+ if (active) {
3606
+ if (!editor._config.theme.tableScrollableWrapper) {
3607
+ console.warn('TableNode: hasHorizontalScroll is active but theme.tableScrollableWrapper is not defined.');
3608
+ }
3609
+ scrollableEditors.add(editor);
3610
+ } else {
3611
+ scrollableEditors.delete(editor);
3612
+ }
3613
+ }
3614
+
3615
+ /** @noInheritDoc */
3616
+ class TableNode extends lexical.ElementNode {
3617
+ /** @internal */
3618
+ __rowStriping;
3619
+ __frozenColumnCount;
3620
+ __frozenRowCount;
3621
+ __colWidths;
3622
+ static getType() {
3623
+ return 'table';
3624
+ }
3625
+ getColWidths() {
3626
+ const self = this.getLatest();
3627
+ return self.__colWidths;
3628
+ }
3629
+ setColWidths(colWidths) {
3630
+ const self = this.getWritable();
3631
+ // NOTE: Node properties should be immutable. Freeze to prevent accidental mutation.
3632
+ self.__colWidths = colWidths !== undefined && true ? Object.freeze(colWidths) : colWidths;
3633
+ return self;
3634
+ }
3635
+ static clone(node) {
3636
+ return new TableNode(node.__key);
3637
+ }
3638
+ afterCloneFrom(prevNode) {
3639
+ super.afterCloneFrom(prevNode);
3640
+ this.__colWidths = prevNode.__colWidths;
3641
+ this.__rowStriping = prevNode.__rowStriping;
3642
+ this.__frozenColumnCount = prevNode.__frozenColumnCount;
3643
+ this.__frozenRowCount = prevNode.__frozenRowCount;
3644
+ }
3645
+ static importDOM() {
3646
+ return {
3647
+ table: _node => ({
3648
+ conversion: $convertTableElement,
3649
+ priority: 1
3650
+ })
3651
+ };
3652
+ }
3653
+ static importJSON(serializedNode) {
3654
+ return $createTableNode().updateFromJSON(serializedNode);
3655
+ }
3656
+ updateFromJSON(serializedNode) {
3657
+ return super.updateFromJSON(serializedNode).setRowStriping(serializedNode.rowStriping || false).setFrozenColumns(serializedNode.frozenColumnCount || 0).setFrozenRows(serializedNode.frozenRowCount || 0).setColWidths(serializedNode.colWidths);
3658
+ }
3659
+ constructor(key) {
3660
+ super(key);
3661
+ this.__rowStriping = false;
3662
+ this.__frozenColumnCount = 0;
3663
+ this.__frozenRowCount = 0;
3664
+ this.__colWidths = undefined;
3665
+ }
3666
+ exportJSON() {
3667
+ return {
3668
+ ...super.exportJSON(),
3669
+ colWidths: this.getColWidths(),
3670
+ frozenColumnCount: this.__frozenColumnCount ? this.__frozenColumnCount : undefined,
3671
+ frozenRowCount: this.__frozenRowCount ? this.__frozenRowCount : undefined,
3672
+ rowStriping: this.__rowStriping ? this.__rowStriping : undefined
3673
+ };
3674
+ }
3675
+ extractWithChild(child, selection, destination) {
3676
+ return destination === 'html';
3677
+ }
3678
+ getDOMSlot(element) {
3679
+ const tableElement = !isHTMLTableElement(element) ? element.querySelector('table') : element;
3680
+ if (!isHTMLTableElement(tableElement)) {
3681
+ formatDevErrorMessage(`TableNode.getDOMSlot: createDOM() did not return a table`);
3682
+ }
3683
+ return super.getDOMSlot(element).withElement(tableElement).withAfter(tableElement.querySelector('colgroup'));
3684
+ }
3685
+ createDOM(config, editor) {
3686
+ const tableElement = document.createElement('table');
3687
+ if (this.__style) {
3688
+ tableElement.style.cssText = this.__style;
3689
+ }
3690
+ const colGroup = document.createElement('colgroup');
3691
+ tableElement.appendChild(colGroup);
3692
+ lexical.setDOMUnmanaged(colGroup);
3693
+ lexicalUtils.addClassNamesToElement(tableElement, config.theme.table);
3694
+ this.updateTableElement(null, tableElement, config);
3695
+ if ($isScrollableTablesActive(editor)) {
3696
+ const wrapperElement = document.createElement('div');
3697
+ const classes = config.theme.tableScrollableWrapper;
3698
+ if (classes) {
3699
+ lexicalUtils.addClassNamesToElement(wrapperElement, classes);
3700
+ } else {
3701
+ wrapperElement.style.cssText = 'overflow-x: auto;';
3702
+ }
3703
+ wrapperElement.appendChild(tableElement);
3704
+ this.updateTableWrapper(null, wrapperElement, tableElement, config);
3705
+ return wrapperElement;
3706
+ }
3707
+ return tableElement;
3708
+ }
3709
+ updateTableWrapper(prevNode, tableWrapper, tableElement, config) {
3710
+ if (this.__frozenColumnCount !== (prevNode ? prevNode.__frozenColumnCount : 0)) {
3711
+ setFrozenColumns(tableWrapper, tableElement, config, this.__frozenColumnCount);
3712
+ }
3713
+ if (this.__frozenRowCount !== (prevNode ? prevNode.__frozenRowCount : 0)) {
3714
+ setFrozenRows(tableWrapper, tableElement, config, this.__frozenRowCount);
3715
+ }
3716
+ }
3717
+ updateTableElement(prevNode, tableElement, config) {
3718
+ if (this.__style !== (prevNode ? prevNode.__style : '')) {
3719
+ tableElement.style.cssText = this.__style;
3720
+ }
3721
+ if (this.__rowStriping !== (prevNode ? prevNode.__rowStriping : false)) {
3722
+ setRowStriping(tableElement, config, this.__rowStriping);
3723
+ }
3724
+ updateColgroup(tableElement, config, this.getColumnCount(), this.getColWidths());
3725
+ alignTableElement(tableElement, config, this.getFormatType());
3726
+ }
3727
+ updateDOM(prevNode, dom, config) {
3728
+ const slot = this.getDOMSlot(dom);
3729
+ const tableElement = slot.element;
3730
+ if (dom === tableElement === $isScrollableTablesActive()) {
3731
+ return true;
3732
+ }
3733
+ if (isHTMLDivElement(dom)) {
3734
+ this.updateTableWrapper(prevNode, dom, tableElement, config);
3735
+ }
3736
+ this.updateTableElement(prevNode, tableElement, config);
3737
+ return false;
3738
+ }
3739
+ exportDOM(editor) {
3740
+ const superExport = super.exportDOM(editor);
3741
+ const {
3742
+ element
3743
+ } = superExport;
3744
+ return {
3745
+ after: tableElement => {
3746
+ if (superExport.after) {
3747
+ tableElement = superExport.after(tableElement);
3748
+ }
3749
+ if (!isHTMLTableElement(tableElement) && lexicalUtils.isHTMLElement(tableElement)) {
3750
+ tableElement = tableElement.querySelector('table');
3751
+ }
3752
+ if (!isHTMLTableElement(tableElement)) {
3753
+ return null;
3754
+ }
3755
+ alignTableElement(tableElement, editor._config, this.getFormatType());
3756
+
3757
+ // Scan the table map to build a map of table cell key to the columns it needs
3758
+ const [tableMap] = $computeTableMapSkipCellCheck(this, null, null);
3759
+ const cellValues = new Map();
3760
+ for (const mapRow of tableMap) {
3761
+ for (const mapValue of mapRow) {
3762
+ const key = mapValue.cell.getKey();
3763
+ if (!cellValues.has(key)) {
3764
+ cellValues.set(key, {
3765
+ colSpan: mapValue.cell.getColSpan(),
3766
+ startColumn: mapValue.startColumn
3767
+ });
3768
+ }
3769
+ }
3770
+ }
3771
+
3772
+ // scan the DOM to find the table cell keys that were used and mark those columns
3773
+ const knownColumns = new Set();
3774
+ for (const cellDOM of tableElement.querySelectorAll(':scope > tr > [data-temporary-table-cell-lexical-key]')) {
3775
+ const key = cellDOM.getAttribute('data-temporary-table-cell-lexical-key');
3776
+ if (key) {
3777
+ const cellSpan = cellValues.get(key);
3778
+ cellDOM.removeAttribute('data-temporary-table-cell-lexical-key');
3779
+ if (cellSpan) {
3780
+ cellValues.delete(key);
3781
+ for (let i = 0; i < cellSpan.colSpan; i++) {
3782
+ knownColumns.add(i + cellSpan.startColumn);
3783
+ }
3784
+ }
3785
+ }
3786
+ }
3787
+
3788
+ // Compute the colgroup and columns in the export
3789
+ const colGroup = tableElement.querySelector(':scope > colgroup');
3790
+ if (colGroup) {
3791
+ // Only include the <col /> for rows that are in the output
3792
+ const cols = Array.from(tableElement.querySelectorAll(':scope > colgroup > col')).filter((dom, i) => knownColumns.has(i));
3793
+ colGroup.replaceChildren(...cols);
3794
+ }
3795
+
3796
+ // Wrap direct descendant rows in a tbody for export
3797
+ const rows = tableElement.querySelectorAll(':scope > tr');
3798
+ if (rows.length > 0) {
3799
+ const tBody = document.createElement('tbody');
3800
+ for (const row of rows) {
3801
+ tBody.appendChild(row);
3802
+ }
3803
+ tableElement.append(tBody);
3804
+ }
3805
+ return tableElement;
3806
+ },
3807
+ element: !isHTMLTableElement(element) && lexicalUtils.isHTMLElement(element) ? element.querySelector('table') : element
3808
+ };
3809
+ }
3810
+ canBeEmpty() {
3811
+ return false;
3812
+ }
3813
+ isShadowRoot() {
3814
+ return true;
3815
+ }
3816
+ getCordsFromCellNode(tableCellNode, table) {
3817
+ const {
3818
+ rows,
3819
+ domRows
3820
+ } = table;
3821
+ for (let y = 0; y < rows; y++) {
3822
+ const row = domRows[y];
3823
+ if (row == null) {
3824
+ continue;
3825
+ }
3826
+ for (let x = 0; x < row.length; x++) {
3827
+ const cell = row[x];
3828
+ if (cell == null) {
3829
+ continue;
3830
+ }
3831
+ const {
3832
+ elem
3833
+ } = cell;
3834
+ const cellNode = $getNearestTableCellInTableFromDOMNode(this, elem);
3835
+ if (cellNode !== null && tableCellNode.is(cellNode)) {
3836
+ return {
3837
+ x,
3838
+ y
3839
+ };
3840
+ }
3841
+ }
3842
+ }
3843
+ throw new Error('Cell not found in table.');
3844
+ }
3845
+ getDOMCellFromCords(x, y, table) {
3846
+ const {
3847
+ domRows
3848
+ } = table;
3849
+ const row = domRows[y];
3850
+ if (row == null) {
3851
+ return null;
3852
+ }
3853
+ const index = x < row.length ? x : row.length - 1;
3854
+ const cell = row[index];
3855
+ if (cell == null) {
3856
+ return null;
3857
+ }
3858
+ return cell;
3859
+ }
3860
+ getDOMCellFromCordsOrThrow(x, y, table) {
3861
+ const cell = this.getDOMCellFromCords(x, y, table);
3862
+ if (!cell) {
3863
+ throw new Error('Cell not found at cords.');
3864
+ }
3865
+ return cell;
3866
+ }
3867
+ getCellNodeFromCords(x, y, table) {
3868
+ const cell = this.getDOMCellFromCords(x, y, table);
3869
+ if (cell == null) {
3870
+ return null;
3871
+ }
3872
+ const node = lexical.$getNearestNodeFromDOMNode(cell.elem);
3873
+ if ($isTableCellNode(node)) {
3874
+ return node;
3875
+ }
3876
+ return null;
3877
+ }
3878
+ getCellNodeFromCordsOrThrow(x, y, table) {
3879
+ const node = this.getCellNodeFromCords(x, y, table);
3880
+ if (!node) {
3881
+ throw new Error('Node at cords not TableCellNode.');
3882
+ }
3883
+ return node;
3884
+ }
3885
+ getRowStriping() {
3886
+ return Boolean(this.getLatest().__rowStriping);
3887
+ }
3888
+ setRowStriping(newRowStriping) {
3889
+ const self = this.getWritable();
3890
+ self.__rowStriping = newRowStriping;
3891
+ return self;
3892
+ }
3893
+ setFrozenColumns(columnCount) {
3894
+ const self = this.getWritable();
3895
+ self.__frozenColumnCount = columnCount;
3896
+ return self;
3897
+ }
3898
+ getFrozenColumns() {
3899
+ return this.getLatest().__frozenColumnCount;
3900
+ }
3901
+ setFrozenRows(rowCount) {
3902
+ const self = this.getWritable();
3903
+ self.__frozenRowCount = rowCount;
3904
+ return self;
3905
+ }
3906
+ getFrozenRows() {
3907
+ return this.getLatest().__frozenRowCount;
3908
+ }
3909
+ canSelectBefore() {
3910
+ return true;
3911
+ }
3912
+ canIndent() {
3913
+ return false;
3914
+ }
3915
+ getColumnCount() {
3916
+ const firstRow = this.getFirstChild();
3917
+ if (!firstRow) {
3918
+ return 0;
3919
+ }
3920
+ let columnCount = 0;
3921
+ firstRow.getChildren().forEach(cell => {
3922
+ if ($isTableCellNode(cell)) {
3923
+ columnCount += cell.getColSpan();
3924
+ }
3925
+ });
3926
+ return columnCount;
3927
+ }
3928
+ }
3929
+ function $getElementForTableNode(editor, tableNode) {
3930
+ const tableElement = editor.getElementByKey(tableNode.getKey());
3931
+ if (!(tableElement !== null)) {
3932
+ formatDevErrorMessage(`$getElementForTableNode: Table Element Not Found`);
3933
+ }
3934
+ return getTable(tableNode, tableElement);
3935
+ }
3936
+ function $convertTableElement(domNode) {
3937
+ const tableNode = $createTableNode();
3938
+ if (domNode.hasAttribute('data-lexical-row-striping')) {
3939
+ tableNode.setRowStriping(true);
3940
+ }
3941
+ if (domNode.hasAttribute('data-lexical-frozen-column')) {
3942
+ tableNode.setFrozenColumns(1);
3943
+ }
3944
+ if (domNode.hasAttribute('data-lexical-frozen-row')) {
3945
+ tableNode.setFrozenRows(1);
3946
+ }
3947
+ const colGroup = domNode.querySelector(':scope > colgroup');
3948
+ if (colGroup) {
3949
+ let columns = [];
3950
+ for (const col of colGroup.querySelectorAll(':scope > col')) {
3951
+ let width = col.style.width || '';
3952
+ if (!PIXEL_VALUE_REG_EXP.test(width)) {
3953
+ // Also support deprecated width attribute for google docs
3954
+ width = col.getAttribute('width') || '';
3955
+ if (!/^\d+$/.test(width)) {
3956
+ columns = undefined;
3957
+ break;
3958
+ }
3959
+ }
3960
+ columns.push(parseFloat(width));
3961
+ }
3962
+ if (columns) {
3963
+ tableNode.setColWidths(columns);
3964
+ }
3965
+ }
3966
+ return {
3967
+ after: children => lexicalUtils.$descendantsMatching(children, $isTableRowNode),
3968
+ node: tableNode
3969
+ };
3970
+ }
3971
+ function $createTableNode() {
3972
+ return lexical.$applyNodeReplacement(new TableNode());
3973
+ }
3974
+ function $isTableNode(node) {
3975
+ return node instanceof TableNode;
3976
+ }
3977
+
3978
+ function $insertTable({
3979
+ rows,
3980
+ columns,
3981
+ includeHeaders
3982
+ }, hasNestedTables) {
3983
+ const selection = lexical.$getSelection() || lexical.$getPreviousSelection();
3984
+ if (!selection || !lexical.$isRangeSelection(selection)) {
3985
+ return false;
3986
+ }
3987
+
3988
+ // Prevent nested tables by checking if we're already inside a table
3989
+ if (!hasNestedTables && $findTableNode(selection.anchor.getNode())) {
3990
+ return false;
3991
+ }
3992
+ const tableNode = $createTableNodeWithDimensions(Number(rows), Number(columns), includeHeaders);
3993
+ lexicalUtils.$insertNodeToNearestRoot(tableNode);
3994
+ const firstDescendant = tableNode.getFirstDescendant();
3995
+ if (lexical.$isTextNode(firstDescendant)) {
3996
+ firstDescendant.select();
3997
+ }
3998
+ return true;
3999
+ }
4000
+ function $tableCellTransform(node) {
4001
+ if (!$isTableRowNode(node.getParent())) {
4002
+ // TableCellNode must be a child of TableRowNode.
4003
+ node.remove();
4004
+ } else if (node.isEmpty()) {
4005
+ // TableCellNode should never be empty
4006
+ node.append(lexical.$createParagraphNode());
4007
+ }
4008
+ }
4009
+ function $tableRowTransform(node) {
4010
+ if (!$isTableNode(node.getParent())) {
4011
+ // TableRowNode must be a child of TableNode.
4012
+ // TODO: Future support of tbody/thead/tfoot may change this
4013
+ node.remove();
4014
+ } else {
4015
+ lexicalUtils.$unwrapAndFilterDescendants(node, $isTableCellNode);
4016
+ }
4017
+ }
4018
+ function $tableTransform(node) {
4019
+ // TableRowNode is the only valid child for TableNode
4020
+ // TODO: Future support of tbody/thead/tfoot/caption may change this
4021
+ lexicalUtils.$unwrapAndFilterDescendants(node, $isTableRowNode);
4022
+ const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
4023
+ const maxRowLength = gridMap.reduce((curLength, row) => {
4024
+ return Math.max(curLength, row.length);
4025
+ }, 0);
4026
+ const rowNodes = node.getChildren();
4027
+ for (let i = 0; i < gridMap.length; ++i) {
4028
+ const rowNode = rowNodes[i];
4029
+ if (!rowNode) {
4030
+ continue;
4031
+ }
4032
+ if (!$isTableRowNode(rowNode)) {
4033
+ formatDevErrorMessage(`TablePlugin: Expecting all children of TableNode to be TableRowNode, found ${rowNode.constructor.name} (type ${rowNode.getType()})`);
4034
+ }
4035
+ const rowLength = gridMap[i].reduce((acc, cell) => cell ? 1 + acc : acc, 0);
4036
+ if (rowLength === maxRowLength) {
4037
+ continue;
4038
+ }
4039
+ for (let j = rowLength; j < maxRowLength; ++j) {
4040
+ // TODO: inherit header state from another header or body
4041
+ const newCell = $createTableCellNode();
4042
+ newCell.append(lexical.$createParagraphNode());
4043
+ rowNode.append(newCell);
4044
+ }
4045
+ }
4046
+ const colWidths = node.getColWidths();
4047
+ const columnCount = node.getColumnCount();
4048
+ if (colWidths && colWidths.length !== columnCount) {
4049
+ let newColWidths = undefined;
4050
+ if (columnCount < colWidths.length) {
4051
+ newColWidths = colWidths.slice(0, columnCount);
4052
+ } else if (colWidths.length > 0) {
4053
+ // Repeat the last column width.
4054
+ const fillWidth = colWidths[colWidths.length - 1];
4055
+ newColWidths = [...colWidths, ...Array(columnCount - colWidths.length).fill(fillWidth)];
4056
+ }
4057
+ node.setColWidths(newColWidths);
4058
+ }
4059
+ }
4060
+ function $tableClickCommand(event) {
4061
+ if (event.detail < 3 || !lexical.isDOMNode(event.target)) {
4062
+ return false;
4063
+ }
4064
+ const startNode = lexical.$getNearestNodeFromDOMNode(event.target);
4065
+ if (startNode === null) {
4066
+ return false;
4067
+ }
4068
+ const blockNode = lexicalUtils.$findMatchingParent(startNode, node => lexical.$isElementNode(node) && !node.isInline());
4069
+ if (blockNode === null) {
4070
+ return false;
4071
+ }
4072
+ const rootNode = blockNode.getParent();
4073
+ if (!$isTableCellNode(rootNode)) {
4074
+ return false;
4075
+ }
4076
+ blockNode.select(0);
4077
+ return true;
4078
+ }
4079
+
4080
+ /**
4081
+ * Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
4082
+ * This should only be registered when you do not want to support merged cells.
4083
+ *
4084
+ * @param editor The editor
4085
+ * @returns An unregister callback
4086
+ */
4087
+ function registerTableCellUnmergeTransform(editor) {
4088
+ return editor.registerNodeTransform(TableCellNode, node => {
4089
+ if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
4090
+ // When we have rowSpan we have to map the entire Table to understand where the new Cells
4091
+ // fit best; let's analyze all Cells at once to save us from further transform iterations
4092
+ const [,, gridNode] = $getNodeTriplet(node);
4093
+ const [gridMap] = $computeTableMap(gridNode, node, node);
4094
+ // TODO this function expects Tables to be normalized. Look into this once it exists
4095
+ const rowsCount = gridMap.length;
4096
+ const columnsCount = gridMap[0].length;
4097
+ let row = gridNode.getFirstChild();
4098
+ if (!$isTableRowNode(row)) {
4099
+ formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
4100
+ }
4101
+ const unmerged = [];
4102
+ for (let i = 0; i < rowsCount; i++) {
4103
+ if (i !== 0) {
4104
+ row = row.getNextSibling();
4105
+ if (!$isTableRowNode(row)) {
4106
+ formatDevErrorMessage(`Expected TableNode first child to be a RowNode`);
4107
+ }
4108
+ }
4109
+ let lastRowCell = null;
4110
+ for (let j = 0; j < columnsCount; j++) {
4111
+ const cellMap = gridMap[i][j];
4112
+ const cell = cellMap.cell;
4113
+ if (cellMap.startRow === i && cellMap.startColumn === j) {
4114
+ lastRowCell = cell;
4115
+ unmerged.push(cell);
4116
+ } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
4117
+ if (!$isTableCellNode(cell)) {
4118
+ formatDevErrorMessage(`Expected TableNode cell to be a TableCellNode`);
4119
+ }
4120
+ const newCell = $createTableCellNode(cell.__headerState);
4121
+ if (lastRowCell !== null) {
4122
+ lastRowCell.insertAfter(newCell);
4123
+ } else {
4124
+ lexicalUtils.$insertFirst(row, newCell);
4125
+ }
4126
+ }
4127
+ }
4128
+ }
4129
+ for (const cell of unmerged) {
4130
+ cell.setColSpan(1);
4131
+ cell.setRowSpan(1);
4132
+ }
4133
+ }
4134
+ });
4135
+ }
4136
+ function registerTableSelectionObserver(editor, hasTabHandler = true) {
4137
+ const tableSelections = new Map();
4138
+ const initializeTableNode = (tableNode, nodeKey, dom) => {
4139
+ const tableElement = getTableElement(tableNode, dom);
4140
+ const tableSelection = applyTableHandlers(tableNode, tableElement, editor, hasTabHandler);
4141
+ tableSelections.set(nodeKey, [tableSelection, tableElement]);
4142
+ };
4143
+ const unregisterMutationListener = editor.registerMutationListener(TableNode, nodeMutations => {
4144
+ editor.getEditorState().read(() => {
4145
+ for (const [nodeKey, mutation] of nodeMutations) {
4146
+ const tableSelection = tableSelections.get(nodeKey);
4147
+ if (mutation === 'created' || mutation === 'updated') {
4148
+ const {
4149
+ tableNode,
4150
+ tableElement
4151
+ } = $getTableAndElementByKey(nodeKey);
4152
+ if (tableSelection === undefined) {
4153
+ initializeTableNode(tableNode, nodeKey, tableElement);
4154
+ } else if (tableElement !== tableSelection[1]) {
4155
+ // The update created a new DOM node, destroy the existing TableObserver
4156
+ tableSelection[0].removeListeners();
4157
+ tableSelections.delete(nodeKey);
4158
+ initializeTableNode(tableNode, nodeKey, tableElement);
4159
+ }
4160
+ } else if (mutation === 'destroyed') {
4161
+ if (tableSelection !== undefined) {
4162
+ tableSelection[0].removeListeners();
4163
+ tableSelections.delete(nodeKey);
4164
+ }
4165
+ }
4166
+ }
4167
+ }, {
4168
+ editor
4169
+ });
4170
+ }, {
4171
+ skipInitialization: false
4172
+ });
4173
+ return () => {
4174
+ unregisterMutationListener();
4175
+ // Hook might be called multiple times so cleaning up tables listeners as well,
4176
+ // as it'll be reinitialized during recurring call
4177
+ for (const [, [tableSelection]] of tableSelections) {
4178
+ tableSelection.removeListeners();
4179
+ }
4180
+ };
4181
+ }
4182
+
4183
+ /**
4184
+ * Register the INSERT_TABLE_COMMAND listener and the table integrity transforms. The
4185
+ * table selection observer should be registered separately after this with
4186
+ * {@link registerTableSelectionObserver}.
4187
+ *
4188
+ * @param editor The editor
4189
+ * @returns An unregister callback
4190
+ */
4191
+ function registerTablePlugin(editor, options) {
4192
+ if (!editor.hasNodes([TableNode])) {
4193
+ {
4194
+ formatDevErrorMessage(`TablePlugin: TableNode is not registered on editor`);
4195
+ }
4196
+ }
4197
+ const {
4198
+ hasNestedTables = lexicalExtension.signal(false)
4199
+ } = options ?? {};
4200
+ return lexicalUtils.mergeRegister(editor.registerCommand(INSERT_TABLE_COMMAND, payload => {
4201
+ return $insertTable(payload, hasNestedTables.peek());
4202
+ }, lexical.COMMAND_PRIORITY_EDITOR), editor.registerCommand(lexical.SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ({
4203
+ nodes,
4204
+ selection
4205
+ }, dispatchEditor) => {
4206
+ if (hasNestedTables.peek() || editor !== dispatchEditor || !lexical.$isRangeSelection(selection)) {
4207
+ return false;
4208
+ }
4209
+ const isInsideTableCell = $findTableNode(selection.anchor.getNode()) !== null;
4210
+ return isInsideTableCell && nodes.some($isTableNode);
4211
+ }, lexical.COMMAND_PRIORITY_EDITOR), editor.registerCommand(lexical.CLICK_COMMAND, $tableClickCommand, lexical.COMMAND_PRIORITY_EDITOR), editor.registerNodeTransform(TableNode, $tableTransform), editor.registerNodeTransform(TableRowNode, $tableRowTransform), editor.registerNodeTransform(TableCellNode, $tableCellTransform));
4212
+ }
4213
+
4214
+ /**
4215
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
4216
+ *
4217
+ * This source code is licensed under the MIT license found in the
4218
+ * LICENSE file in the root directory of this source tree.
4219
+ *
4220
+ */
4221
+
4222
+ /**
4223
+ * Configures {@link TableNode}, {@link TableRowNode}, {@link TableCellNode} and
4224
+ * registers table behaviors (see {@link TableConfig})
4225
+ */
4226
+ const TableExtension = lexical.defineExtension({
4227
+ build(editor, config, state) {
4228
+ return lexicalExtension.namedSignals(config);
4229
+ },
4230
+ config: lexical.safeCast({
4231
+ hasCellBackgroundColor: true,
4232
+ hasCellMerge: true,
4233
+ hasHorizontalScroll: true,
4234
+ hasNestedTables: false,
4235
+ hasTabHandler: true
4236
+ }),
4237
+ name: '@ekz/lexical-table/Table',
4238
+ nodes: () => [TableNode, TableRowNode, TableCellNode],
4239
+ register(editor, config, state) {
4240
+ const stores = state.getOutput();
4241
+ const {
4242
+ hasNestedTables
4243
+ } = stores;
4244
+ return lexicalUtils.mergeRegister(lexicalExtension.effect(() => {
4245
+ const hasHorizontalScroll = stores.hasHorizontalScroll.value;
4246
+ const hadHorizontalScroll = $isScrollableTablesActive(editor);
4247
+ if (hadHorizontalScroll !== hasHorizontalScroll) {
4248
+ setScrollableTablesActive(editor, hasHorizontalScroll);
4249
+ // Registering the transform has the side-effect of marking all existing
4250
+ // TableNodes as dirty. The handler is immediately unregistered.
4251
+ editor.registerNodeTransform(TableNode, () => {})();
4252
+ }
4253
+ }), registerTablePlugin(editor, {
4254
+ hasNestedTables
4255
+ }), lexicalExtension.effect(() => registerTableSelectionObserver(editor, stores.hasTabHandler.value)), lexicalExtension.effect(() => stores.hasCellMerge.value ? undefined : registerTableCellUnmergeTransform(editor)), lexicalExtension.effect(() => stores.hasCellBackgroundColor.value ? undefined : editor.registerNodeTransform(TableCellNode, node => {
4256
+ if (node.getBackgroundColor() !== null) {
4257
+ node.setBackgroundColor(null);
4258
+ }
4259
+ })));
4260
+ }
4261
+ });
4262
+
4263
+ exports.$computeTableMap = $computeTableMap;
4264
+ exports.$computeTableMapSkipCellCheck = $computeTableMapSkipCellCheck;
4265
+ exports.$createTableCellNode = $createTableCellNode;
4266
+ exports.$createTableNode = $createTableNode;
4267
+ exports.$createTableNodeWithDimensions = $createTableNodeWithDimensions;
4268
+ exports.$createTableRowNode = $createTableRowNode;
4269
+ exports.$createTableSelection = $createTableSelection;
4270
+ exports.$createTableSelectionFrom = $createTableSelectionFrom;
4271
+ exports.$deleteTableColumn = $deleteTableColumn;
4272
+ exports.$deleteTableColumnAtSelection = $deleteTableColumnAtSelection;
4273
+ exports.$deleteTableColumn__EXPERIMENTAL = $deleteTableColumn__EXPERIMENTAL;
4274
+ exports.$deleteTableRowAtSelection = $deleteTableRowAtSelection;
4275
+ exports.$deleteTableRow__EXPERIMENTAL = $deleteTableRow__EXPERIMENTAL;
4276
+ exports.$findCellNode = $findCellNode;
4277
+ exports.$findTableNode = $findTableNode;
4278
+ exports.$getElementForTableNode = $getElementForTableNode;
4279
+ exports.$getNodeTriplet = $getNodeTriplet;
4280
+ exports.$getTableAndElementByKey = $getTableAndElementByKey;
4281
+ exports.$getTableCellNodeFromLexicalNode = $getTableCellNodeFromLexicalNode;
4282
+ exports.$getTableCellNodeRect = $getTableCellNodeRect;
4283
+ exports.$getTableColumnIndexFromTableCellNode = $getTableColumnIndexFromTableCellNode;
4284
+ exports.$getTableNodeFromLexicalNodeOrThrow = $getTableNodeFromLexicalNodeOrThrow;
4285
+ exports.$getTableRowIndexFromTableCellNode = $getTableRowIndexFromTableCellNode;
4286
+ exports.$getTableRowNodeFromTableCellNodeOrThrow = $getTableRowNodeFromTableCellNodeOrThrow;
4287
+ exports.$insertTableColumn = $insertTableColumn;
4288
+ exports.$insertTableColumnAtSelection = $insertTableColumnAtSelection;
4289
+ exports.$insertTableColumn__EXPERIMENTAL = $insertTableColumn__EXPERIMENTAL;
4290
+ exports.$insertTableRow = $insertTableRow;
4291
+ exports.$insertTableRowAtSelection = $insertTableRowAtSelection;
4292
+ exports.$insertTableRow__EXPERIMENTAL = $insertTableRow__EXPERIMENTAL;
4293
+ exports.$isScrollableTablesActive = $isScrollableTablesActive;
4294
+ exports.$isTableCellNode = $isTableCellNode;
4295
+ exports.$isTableNode = $isTableNode;
4296
+ exports.$isTableRowNode = $isTableRowNode;
4297
+ exports.$isTableSelection = $isTableSelection;
4298
+ exports.$mergeCells = $mergeCells;
4299
+ exports.$removeTableRowAtIndex = $removeTableRowAtIndex;
4300
+ exports.$unmergeCell = $unmergeCell;
4301
+ exports.INSERT_TABLE_COMMAND = INSERT_TABLE_COMMAND;
4302
+ exports.TableCellHeaderStates = TableCellHeaderStates;
4303
+ exports.TableCellNode = TableCellNode;
4304
+ exports.TableExtension = TableExtension;
4305
+ exports.TableNode = TableNode;
4306
+ exports.TableObserver = TableObserver;
4307
+ exports.TableRowNode = TableRowNode;
4308
+ exports.applyTableHandlers = applyTableHandlers;
4309
+ exports.getDOMCellFromTarget = getDOMCellFromTarget;
4310
+ exports.getTableElement = getTableElement;
4311
+ exports.getTableObserverFromTableElement = getTableObserverFromTableElement;
4312
+ exports.registerTableCellUnmergeTransform = registerTableCellUnmergeTransform;
4313
+ exports.registerTablePlugin = registerTablePlugin;
4314
+ exports.registerTableSelectionObserver = registerTableSelectionObserver;
4315
+ exports.setScrollableTablesActive = setScrollableTablesActive;