@lexical/table 0.44.1-nightly.20260519.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/{LexicalTable.dev.js → dist/LexicalTable.dev.js} +243 -33
  2. package/{LexicalTable.dev.mjs → dist/LexicalTable.dev.mjs} +239 -33
  3. package/{LexicalTable.mjs → dist/LexicalTable.mjs} +4 -0
  4. package/{LexicalTable.node.mjs → dist/LexicalTable.node.mjs} +4 -0
  5. package/dist/LexicalTable.prod.js +9 -0
  6. package/dist/LexicalTable.prod.mjs +9 -0
  7. package/dist/TableImportExtension.d.ts +38 -0
  8. package/{index.d.ts → dist/index.d.ts} +1 -0
  9. package/package.json +34 -18
  10. package/src/LexicalTableCellNode.ts +479 -0
  11. package/src/LexicalTableCommands.ts +27 -0
  12. package/src/LexicalTableExtension.ts +104 -0
  13. package/src/LexicalTableNode.ts +678 -0
  14. package/src/LexicalTableObserver.ts +575 -0
  15. package/src/LexicalTablePluginHelpers.ts +694 -0
  16. package/src/LexicalTableRowNode.ts +154 -0
  17. package/src/LexicalTableSelection.ts +460 -0
  18. package/src/LexicalTableSelectionHelpers.ts +2409 -0
  19. package/src/LexicalTableUtils.ts +1386 -0
  20. package/src/TableImportExtension.ts +302 -0
  21. package/src/constants.ts +13 -0
  22. package/src/index.ts +97 -0
  23. package/LexicalTable.prod.js +0 -9
  24. package/LexicalTable.prod.mjs +0 -9
  25. /package/{LexicalTable.js → dist/LexicalTable.js} +0 -0
  26. /package/{LexicalTable.js.flow → dist/LexicalTable.js.flow} +0 -0
  27. /package/{LexicalTableCellNode.d.ts → dist/LexicalTableCellNode.d.ts} +0 -0
  28. /package/{LexicalTableCommands.d.ts → dist/LexicalTableCommands.d.ts} +0 -0
  29. /package/{LexicalTableExtension.d.ts → dist/LexicalTableExtension.d.ts} +0 -0
  30. /package/{LexicalTableNode.d.ts → dist/LexicalTableNode.d.ts} +0 -0
  31. /package/{LexicalTableObserver.d.ts → dist/LexicalTableObserver.d.ts} +0 -0
  32. /package/{LexicalTablePluginHelpers.d.ts → dist/LexicalTablePluginHelpers.d.ts} +0 -0
  33. /package/{LexicalTableRowNode.d.ts → dist/LexicalTableRowNode.d.ts} +0 -0
  34. /package/{LexicalTableSelection.d.ts → dist/LexicalTableSelection.d.ts} +0 -0
  35. /package/{LexicalTableSelectionHelpers.d.ts → dist/LexicalTableSelectionHelpers.d.ts} +0 -0
  36. /package/{LexicalTableUtils.d.ts → dist/LexicalTableUtils.d.ts} +0 -0
  37. /package/{constants.d.ts → dist/constants.d.ts} +0 -0
@@ -0,0 +1,694 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import {NamedSignalsOutput, Signal, signal} from '@lexical/extension';
10
+ import invariant from '@lexical/internal/invariant';
11
+ import {
12
+ $dfs,
13
+ $findMatchingParent,
14
+ $insertFirst,
15
+ $insertNodeToNearestRoot,
16
+ $unwrapAndFilterDescendants,
17
+ mergeRegister,
18
+ } from '@lexical/utils';
19
+ import {
20
+ $createParagraphNode,
21
+ $getNearestNodeFromDOMNode,
22
+ $getPreviousSelection,
23
+ $getRoot,
24
+ $getSelection,
25
+ $isElementNode,
26
+ $isRangeSelection,
27
+ $isTextNode,
28
+ $setSelection,
29
+ CLICK_COMMAND,
30
+ COMMAND_PRIORITY_EDITOR,
31
+ COMMAND_PRIORITY_HIGH,
32
+ COMMAND_PRIORITY_LOW,
33
+ CommandPayloadType,
34
+ ElementNode,
35
+ isDOMNode,
36
+ LexicalEditor,
37
+ NodeKey,
38
+ RangeSelection,
39
+ SELECT_ALL_COMMAND,
40
+ SELECTION_CHANGE_COMMAND,
41
+ SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
42
+ } from 'lexical';
43
+
44
+ import {
45
+ $createTableCellNode,
46
+ $isTableCellNode,
47
+ TableCellNode,
48
+ } from './LexicalTableCellNode';
49
+ import {
50
+ INSERT_TABLE_COMMAND,
51
+ InsertTableCommandPayload,
52
+ } from './LexicalTableCommands';
53
+ import {TableConfig} from './LexicalTableExtension';
54
+ import {$isTableNode, TableNode} from './LexicalTableNode';
55
+ import {$getTableAndElementByKey, TableObservers} from './LexicalTableObserver';
56
+ import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
57
+ import {
58
+ $createTableSelectionFrom,
59
+ $isTableSelection,
60
+ TableSelection,
61
+ } from './LexicalTableSelection';
62
+ import {
63
+ $findTableNode,
64
+ $handleTableSelectionChangeCommand,
65
+ applyTableHandlers,
66
+ getTableElement,
67
+ registerTableWindowHandlers,
68
+ } from './LexicalTableSelectionHelpers';
69
+ import {
70
+ $computeTableCellRectBoundary,
71
+ $computeTableMap,
72
+ $computeTableMapSkipCellCheck,
73
+ $createTableNodeWithDimensions,
74
+ $getNodeTriplet,
75
+ $insertTableColumnAtNode,
76
+ $insertTableRowAtNode,
77
+ $mergeCells,
78
+ $unmergeCellNode,
79
+ } from './LexicalTableUtils';
80
+
81
+ function $insertTable(
82
+ {rows, columns, includeHeaders}: InsertTableCommandPayload,
83
+ hasNestedTables: boolean,
84
+ ): boolean {
85
+ const selection = $getSelection() || $getPreviousSelection();
86
+ if (!selection || !$isRangeSelection(selection)) {
87
+ return false;
88
+ }
89
+
90
+ // Prevent nested tables by checking if we're already inside a table
91
+ if (!hasNestedTables && $findTableNode(selection.anchor.getNode())) {
92
+ return false;
93
+ }
94
+
95
+ const tableNode = $createTableNodeWithDimensions(
96
+ Number(rows),
97
+ Number(columns),
98
+ includeHeaders,
99
+ );
100
+ $insertNodeToNearestRoot(tableNode);
101
+
102
+ const firstDescendant = tableNode.getFirstDescendant();
103
+ if ($isTextNode(firstDescendant)) {
104
+ firstDescendant.select();
105
+ }
106
+
107
+ return true;
108
+ }
109
+
110
+ function $tableCellTransform(node: TableCellNode) {
111
+ if (!$isTableRowNode(node.getParent())) {
112
+ // TableCellNode must be a child of TableRowNode.
113
+ node.remove();
114
+ } else if (node.isEmpty()) {
115
+ // TableCellNode should never be empty
116
+ node.append($createParagraphNode());
117
+ }
118
+ }
119
+
120
+ function $tableRowTransform(node: TableRowNode) {
121
+ if (!$isTableNode(node.getParent())) {
122
+ // TableRowNode must be a child of TableNode.
123
+ // TODO: Future support of tbody/thead/tfoot may change this
124
+ node.remove();
125
+ } else {
126
+ $unwrapAndFilterDescendants(node, $isTableCellNode);
127
+ }
128
+ }
129
+
130
+ function $tableTransform(node: TableNode) {
131
+ // TableRowNode is the only valid child for TableNode
132
+ // TODO: Future support of tbody/thead/tfoot/caption may change this
133
+ $unwrapAndFilterDescendants(node, $isTableRowNode);
134
+
135
+ const [gridMap] = $computeTableMapSkipCellCheck(node, null, null);
136
+ const maxRowLength = gridMap.reduce((curLength, row) => {
137
+ return Math.max(curLength, row.length);
138
+ }, 0);
139
+ const rowNodes = node.getChildren();
140
+ for (let i = 0; i < gridMap.length; ++i) {
141
+ const rowNode = rowNodes[i];
142
+ if (!rowNode) {
143
+ continue;
144
+ }
145
+ invariant(
146
+ $isTableRowNode(rowNode),
147
+ 'TablePlugin: Expecting all children of TableNode to be TableRowNode, found %s (type %s)',
148
+ rowNode.constructor.name,
149
+ rowNode.getType(),
150
+ );
151
+ const rowLength = gridMap[i].reduce(
152
+ (acc, cell) => (cell ? 1 + acc : acc),
153
+ 0,
154
+ );
155
+ if (rowLength === maxRowLength) {
156
+ continue;
157
+ }
158
+ for (let j = rowLength; j < maxRowLength; ++j) {
159
+ // TODO: inherit header state from another header or body
160
+ const newCell = $createTableCellNode();
161
+ newCell.append($createParagraphNode());
162
+ rowNode.append(newCell);
163
+ }
164
+ }
165
+ const colWidths = node.getColWidths();
166
+ const columnCount = node.getColumnCount();
167
+ if (colWidths && colWidths.length !== columnCount) {
168
+ let newColWidths: number[] | undefined = undefined;
169
+ if (columnCount < colWidths.length) {
170
+ newColWidths = colWidths.slice(0, columnCount);
171
+ } else if (colWidths.length > 0) {
172
+ // Repeat the last column width.
173
+ const fillWidth = colWidths[colWidths.length - 1];
174
+ newColWidths = [
175
+ ...colWidths,
176
+ ...Array(columnCount - colWidths.length).fill(fillWidth),
177
+ ];
178
+ }
179
+ node.setColWidths(newColWidths);
180
+ }
181
+ }
182
+
183
+ function $tableClickCommand(event: MouseEvent): boolean {
184
+ if (event.detail < 3 || !isDOMNode(event.target)) {
185
+ return false;
186
+ }
187
+ const startNode = $getNearestNodeFromDOMNode(event.target);
188
+ if (startNode === null) {
189
+ return false;
190
+ }
191
+ const blockNode = $findMatchingParent(
192
+ startNode,
193
+ (node): node is ElementNode => $isElementNode(node) && !node.isInline(),
194
+ );
195
+ if (blockNode === null) {
196
+ return false;
197
+ }
198
+ const rootNode = blockNode.getParent();
199
+ if (!$isTableCellNode(rootNode)) {
200
+ return false;
201
+ }
202
+ blockNode.select(0);
203
+ return true;
204
+ }
205
+
206
+ function $tableSelectAllCommand(): boolean {
207
+ const selection = $getSelection();
208
+ if (!$isRangeSelection(selection)) {
209
+ return false;
210
+ }
211
+
212
+ // Check if the selection is inside a table
213
+ const anchorNode = selection.anchor.getNode();
214
+ const tableNode = $findTableNode(anchorNode);
215
+ if (tableNode === null) {
216
+ return false;
217
+ }
218
+
219
+ // CRITICAL: Only intercept if table is the ONLY child of root
220
+ // This is required to reproduce the bug: table must be the only content, no empty paragraphs
221
+ // This prevents breaking other tests that expect RangeSelection when there's content outside table
222
+ const root = $getRoot();
223
+ if (!root.is(tableNode.getParent()) || root.getChildrenSize() !== 1) {
224
+ return false;
225
+ }
226
+
227
+ // At this point, table is the only child
228
+ // This is the exact scenario from issue #8074: table is the only content in editor
229
+
230
+ // Get the table map to find first and last cells (handles merged cells correctly)
231
+ const [tableMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
232
+ if (tableMap.length === 0 || tableMap[0].length === 0) {
233
+ return false;
234
+ }
235
+
236
+ // Get the first cell (top-left)
237
+ const firstCellMap = tableMap[0][0];
238
+ if (!firstCellMap || !firstCellMap.cell) {
239
+ return false;
240
+ }
241
+
242
+ // Get the last cell (bottom-right)
243
+ const lastRow = tableMap[tableMap.length - 1];
244
+ const lastCellMap = lastRow[lastRow.length - 1];
245
+ if (!lastCellMap || !lastCellMap.cell) {
246
+ return false;
247
+ }
248
+
249
+ // Create a TableSelection that selects all cells
250
+ const tableSelection = $createTableSelectionFrom(
251
+ tableNode,
252
+ firstCellMap.cell,
253
+ lastCellMap.cell,
254
+ );
255
+ $setSelection(tableSelection);
256
+
257
+ return true;
258
+ }
259
+
260
+ /**
261
+ * Register a transform to ensure that all TableCellNode have a colSpan and rowSpan of 1.
262
+ * This should only be registered when you do not want to support merged cells.
263
+ *
264
+ * @param editor The editor
265
+ * @returns An unregister callback
266
+ */
267
+ export function registerTableCellUnmergeTransform(
268
+ editor: LexicalEditor,
269
+ ): () => void {
270
+ return editor.registerNodeTransform(TableCellNode, node => {
271
+ if (node.getColSpan() > 1 || node.getRowSpan() > 1) {
272
+ // When we have rowSpan we have to map the entire Table to understand where the new Cells
273
+ // fit best; let's analyze all Cells at once to save us from further transform iterations
274
+ const [, , gridNode] = $getNodeTriplet(node);
275
+ const [gridMap] = $computeTableMap(gridNode, node, node);
276
+ // TODO this function expects Tables to be normalized. Look into this once it exists
277
+ const rowsCount = gridMap.length;
278
+ const columnsCount = gridMap[0].length;
279
+ let row = gridNode.getFirstChild();
280
+ invariant(
281
+ $isTableRowNode(row),
282
+ 'Expected TableNode first child to be a RowNode',
283
+ );
284
+ const unmerged = [];
285
+ for (let i = 0; i < rowsCount; i++) {
286
+ if (i !== 0) {
287
+ row = row.getNextSibling();
288
+ invariant(
289
+ $isTableRowNode(row),
290
+ 'Expected TableNode first child to be a RowNode',
291
+ );
292
+ }
293
+ let lastRowCell: null | TableCellNode = null;
294
+ for (let j = 0; j < columnsCount; j++) {
295
+ const cellMap = gridMap[i][j];
296
+ const cell = cellMap.cell;
297
+ if (cellMap.startRow === i && cellMap.startColumn === j) {
298
+ lastRowCell = cell;
299
+ unmerged.push(cell);
300
+ } else if (cell.getColSpan() > 1 || cell.getRowSpan() > 1) {
301
+ invariant(
302
+ $isTableCellNode(cell),
303
+ 'Expected TableNode cell to be a TableCellNode',
304
+ );
305
+ const newCell = $createTableCellNode(cell.__headerState);
306
+ if (lastRowCell !== null) {
307
+ lastRowCell.insertAfter(newCell);
308
+ } else {
309
+ $insertFirst(row, newCell);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ for (const cell of unmerged) {
315
+ cell.setColSpan(1);
316
+ cell.setRowSpan(1);
317
+ }
318
+ }
319
+ });
320
+ }
321
+
322
+ export function registerTableSelectionObserver(
323
+ editor: LexicalEditor,
324
+ hasTabHandler: boolean = true,
325
+ ): () => void {
326
+ const tableObservers = new TableObservers();
327
+
328
+ const initializeTableNode = (
329
+ tableNode: TableNode,
330
+ nodeKey: NodeKey,
331
+ dom: HTMLElement,
332
+ ) => {
333
+ const tableElement = getTableElement(tableNode, dom);
334
+ const tableSelection = applyTableHandlers(
335
+ tableNode,
336
+ tableElement,
337
+ editor,
338
+ hasTabHandler,
339
+ tableObservers,
340
+ );
341
+ tableObservers.observers.set(nodeKey, [tableSelection, tableElement]);
342
+ };
343
+
344
+ return mergeRegister(
345
+ registerTableWindowHandlers(editor, tableObservers),
346
+ editor.registerCommand(
347
+ SELECTION_CHANGE_COMMAND,
348
+ () => {
349
+ return $handleTableSelectionChangeCommand(tableObservers, editor);
350
+ },
351
+ COMMAND_PRIORITY_HIGH,
352
+ ),
353
+ editor.registerMutationListener(
354
+ TableNode,
355
+ nodeMutations => {
356
+ editor.getEditorState().read(
357
+ () => {
358
+ for (const [nodeKey, mutation] of nodeMutations) {
359
+ const tableSelection = tableObservers.observers.get(nodeKey);
360
+ if (mutation === 'created' || mutation === 'updated') {
361
+ const {tableNode, tableElement} =
362
+ $getTableAndElementByKey(nodeKey);
363
+ if (tableSelection === undefined) {
364
+ initializeTableNode(tableNode, nodeKey, tableElement);
365
+ } else if (tableElement !== tableSelection[1]) {
366
+ // The update created a new DOM node, destroy the existing TableObserver
367
+ tableSelection[0].removeListeners();
368
+ tableObservers.observers.delete(nodeKey);
369
+ initializeTableNode(tableNode, nodeKey, tableElement);
370
+ }
371
+ } else if (mutation === 'destroyed') {
372
+ if (tableSelection !== undefined) {
373
+ tableSelection[0].removeListeners();
374
+ tableObservers.observers.delete(nodeKey);
375
+ }
376
+ }
377
+ }
378
+ },
379
+ {editor},
380
+ );
381
+ },
382
+ {skipInitialization: false},
383
+ ),
384
+ () => {
385
+ // Hook might be called multiple times so cleaning up tables listeners as well,
386
+ // as it'll be reinitialized during recurring call
387
+ for (const [, [tableSelection]] of tableObservers.observers) {
388
+ tableSelection.removeListeners();
389
+ }
390
+ },
391
+ );
392
+ }
393
+
394
+ /**
395
+ * Register table command listeners and the table integrity transforms. The
396
+ * table selection observer should be registered separately after this with
397
+ * {@link registerTableSelectionObserver}.
398
+ *
399
+ * @param editor The editor
400
+ * @returns An unregister callback
401
+ */
402
+ export function registerTablePlugin(
403
+ editor: LexicalEditor,
404
+ options?: Pick<NamedSignalsOutput<TableConfig>, 'hasNestedTables'>,
405
+ ): () => void {
406
+ if (!editor.hasNodes([TableNode])) {
407
+ invariant(false, 'TablePlugin: TableNode is not registered on editor');
408
+ }
409
+
410
+ const {hasNestedTables = signal(false)} = options ?? {};
411
+
412
+ return mergeRegister(
413
+ editor.registerCommand(
414
+ INSERT_TABLE_COMMAND,
415
+ payload => {
416
+ return $insertTable(payload, hasNestedTables.peek());
417
+ },
418
+ COMMAND_PRIORITY_EDITOR,
419
+ ),
420
+ editor.registerCommand(
421
+ SELECTION_INSERT_CLIPBOARD_NODES_COMMAND,
422
+ (payload, dispatchEditor) => {
423
+ if (editor !== dispatchEditor) {
424
+ return false;
425
+ }
426
+ return $tableSelectionInsertClipboardNodesCommand(
427
+ payload,
428
+ hasNestedTables,
429
+ );
430
+ },
431
+ COMMAND_PRIORITY_EDITOR,
432
+ ),
433
+ editor.registerCommand(
434
+ SELECT_ALL_COMMAND,
435
+ $tableSelectAllCommand,
436
+ COMMAND_PRIORITY_LOW,
437
+ ),
438
+ editor.registerCommand(
439
+ CLICK_COMMAND,
440
+ $tableClickCommand,
441
+ COMMAND_PRIORITY_EDITOR,
442
+ ),
443
+ editor.registerNodeTransform(TableNode, $tableTransform),
444
+ editor.registerNodeTransform(TableRowNode, $tableRowTransform),
445
+ editor.registerNodeTransform(TableCellNode, $tableCellTransform),
446
+ );
447
+ }
448
+
449
+ function $tableSelectionInsertClipboardNodesCommand(
450
+ selectionPayload: CommandPayloadType<
451
+ typeof SELECTION_INSERT_CLIPBOARD_NODES_COMMAND
452
+ >,
453
+ hasNestedTables: Signal<boolean>,
454
+ ) {
455
+ const {nodes, selection} = selectionPayload;
456
+
457
+ const hasTables = nodes.some(
458
+ n => $isTableNode(n) || $dfs(n).some(d => $isTableNode(d.node)),
459
+ );
460
+ if (!hasTables) {
461
+ // Not pasting a table - no special handling required.
462
+ return false;
463
+ }
464
+
465
+ const isTableSelection = $isTableSelection(selection);
466
+ const isRangeSelection = $isRangeSelection(selection);
467
+ const isSelectionInsideOfGrid =
468
+ (isRangeSelection &&
469
+ $findMatchingParent(selection.anchor.getNode(), n =>
470
+ $isTableCellNode(n),
471
+ ) !== null &&
472
+ $findMatchingParent(selection.focus.getNode(), n =>
473
+ $isTableCellNode(n),
474
+ ) !== null) ||
475
+ isTableSelection;
476
+
477
+ if (!isSelectionInsideOfGrid) {
478
+ // Not pasting in a grid - no special handling required.
479
+ return false;
480
+ }
481
+
482
+ // When pasting just a table, flatten the table on the destination table, even when nested tables are allowed.
483
+ if (nodes.length === 1 && $isTableNode(nodes[0])) {
484
+ return $insertTableIntoGrid(nodes[0], selection);
485
+ }
486
+
487
+ // If nested tables are enabled, allow pasting a table into a single cell.
488
+ if (
489
+ isRangeSelection &&
490
+ hasNestedTables.peek() &&
491
+ !$isMultiCellTableSelection(selection)
492
+ ) {
493
+ return false;
494
+ }
495
+
496
+ // If we reached this point, there's a table in the selection and nested tables are not allowed - reject the paste.
497
+ return true;
498
+ }
499
+
500
+ function $insertTableIntoGrid(
501
+ tableNode: TableNode,
502
+ selection: RangeSelection | TableSelection,
503
+ ) {
504
+ const anchorAndFocus = selection.getStartEndPoints();
505
+ const isTableSelection = $isTableSelection(selection);
506
+
507
+ if (anchorAndFocus === null) {
508
+ return false;
509
+ }
510
+
511
+ const [anchor, focus] = anchorAndFocus;
512
+ const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
513
+ const focusCellNode = $findMatchingParent(focus.getNode(), n =>
514
+ $isTableCellNode(n),
515
+ );
516
+
517
+ if (
518
+ !$isTableCellNode(anchorCellNode) ||
519
+ !$isTableCellNode(focusCellNode) ||
520
+ !$isTableRowNode(anchorRowNode) ||
521
+ !$isTableNode(gridNode)
522
+ ) {
523
+ return false;
524
+ }
525
+
526
+ const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(
527
+ gridNode,
528
+ anchorCellNode,
529
+ focusCellNode,
530
+ );
531
+ const [templateGridMap] = $computeTableMapSkipCellCheck(
532
+ tableNode,
533
+ null,
534
+ null,
535
+ );
536
+ const initialRowCount = initialGridMap.length;
537
+ const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
538
+
539
+ // If we have a range selection, we'll fit the template grid into the
540
+ // table, growing the table if necessary.
541
+ let startRow = anchorCellMap.startRow;
542
+ let startCol = anchorCellMap.startColumn;
543
+ let affectedRowCount = templateGridMap.length;
544
+ let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
545
+
546
+ if (isTableSelection) {
547
+ // If we have a table selection, we'll only modify the cells within
548
+ // the selection boundary.
549
+ const selectionBoundary = $computeTableCellRectBoundary(
550
+ initialGridMap,
551
+ anchorCellMap,
552
+ focusCellMap,
553
+ );
554
+ const selectionRowCount =
555
+ selectionBoundary.maxRow - selectionBoundary.minRow + 1;
556
+ const selectionColCount =
557
+ selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
558
+ startRow = selectionBoundary.minRow;
559
+ startCol = selectionBoundary.minColumn;
560
+ affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
561
+ affectedColCount = Math.min(affectedColCount, selectionColCount);
562
+ }
563
+
564
+ // Step 1: Unmerge all merged cells within the affected area
565
+ let didPerformMergeOperations = false;
566
+ const lastRowForUnmerge =
567
+ Math.min(initialRowCount, startRow + affectedRowCount) - 1;
568
+ const lastColForUnmerge =
569
+ Math.min(initialColCount, startCol + affectedColCount) - 1;
570
+ const unmergedKeys = new Set<NodeKey>();
571
+ for (let row = startRow; row <= lastRowForUnmerge; row++) {
572
+ for (let col = startCol; col <= lastColForUnmerge; col++) {
573
+ const cellMap = initialGridMap[row][col];
574
+ if (unmergedKeys.has(cellMap.cell.getKey())) {
575
+ continue; // cell was a merged cell that was already handled
576
+ }
577
+ if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
578
+ continue; // cell is not a merged cell
579
+ }
580
+ $unmergeCellNode(cellMap.cell);
581
+ unmergedKeys.add(cellMap.cell.getKey());
582
+ didPerformMergeOperations = true;
583
+ }
584
+ }
585
+
586
+ let [interimGridMap] = $computeTableMapSkipCellCheck(
587
+ gridNode.getWritable(),
588
+ null,
589
+ null,
590
+ );
591
+
592
+ // Step 2: Expand current table (if needed)
593
+ const rowsToInsert = affectedRowCount - initialRowCount + startRow;
594
+ for (let i = 0; i < rowsToInsert; i++) {
595
+ const cellMap = interimGridMap[initialRowCount - 1][0];
596
+ $insertTableRowAtNode(cellMap.cell);
597
+ }
598
+ const colsToInsert = affectedColCount - initialColCount + startCol;
599
+ for (let i = 0; i < colsToInsert; i++) {
600
+ const cellMap = interimGridMap[0][initialColCount - 1];
601
+ $insertTableColumnAtNode(cellMap.cell, true, false);
602
+ }
603
+
604
+ [interimGridMap] = $computeTableMapSkipCellCheck(
605
+ gridNode.getWritable(),
606
+ null,
607
+ null,
608
+ );
609
+
610
+ // Step 3: Merge cells and set cell content, to match template grid
611
+ for (let row = startRow; row < startRow + affectedRowCount; row++) {
612
+ for (let col = startCol; col < startCol + affectedColCount; col++) {
613
+ const templateRow = row - startRow;
614
+ const templateCol = col - startCol;
615
+ const templateCellMap = templateGridMap[templateRow][templateCol];
616
+ if (
617
+ templateCellMap.startRow !== templateRow ||
618
+ templateCellMap.startColumn !== templateCol
619
+ ) {
620
+ continue; // cell is a merged cell that was already handled
621
+ }
622
+
623
+ const templateCell = templateCellMap.cell;
624
+ if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
625
+ const cellsToMerge = [];
626
+ const lastRowForMerge =
627
+ Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) -
628
+ 1;
629
+ const lastColForMerge =
630
+ Math.min(col + templateCell.__colSpan, startCol + affectedColCount) -
631
+ 1;
632
+ for (let r = row; r <= lastRowForMerge; r++) {
633
+ for (let c = col; c <= lastColForMerge; c++) {
634
+ const cellMap = interimGridMap[r][c];
635
+ cellsToMerge.push(cellMap.cell);
636
+ }
637
+ }
638
+ $mergeCells(cellsToMerge);
639
+ didPerformMergeOperations = true;
640
+ }
641
+
642
+ const {cell} = interimGridMap[row][col];
643
+ const backgroundColor = templateCell.getBackgroundColor();
644
+ if (backgroundColor !== null && backgroundColor !== undefined) {
645
+ cell.setBackgroundColor(backgroundColor);
646
+ }
647
+ const originalChildren = cell.getChildren();
648
+ templateCell.getChildren().forEach(child => {
649
+ if ($isTextNode(child)) {
650
+ const paragraphNode = $createParagraphNode();
651
+ paragraphNode.append(child);
652
+ cell.append(child);
653
+ } else {
654
+ cell.append(child);
655
+ }
656
+ });
657
+ originalChildren.forEach(n => n.remove());
658
+ }
659
+ }
660
+
661
+ if (isTableSelection && didPerformMergeOperations) {
662
+ // reset the table selection in case the anchor or focus cell was
663
+ // removed via merge operations
664
+ const [finalGridMap] = $computeTableMapSkipCellCheck(
665
+ gridNode.getWritable(),
666
+ null,
667
+ null,
668
+ );
669
+ const newAnchorCellMap =
670
+ finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
671
+ newAnchorCellMap.cell.selectEnd();
672
+ }
673
+
674
+ return true;
675
+ }
676
+
677
+ function $isMultiCellTableSelection(
678
+ selection: TableSelection | RangeSelection,
679
+ ) {
680
+ if (
681
+ $isTableSelection(selection) &&
682
+ !selection.focus.getNode().is(selection.anchor.getNode())
683
+ ) {
684
+ return true;
685
+ }
686
+ if (
687
+ $isRangeSelection(selection) &&
688
+ $isTableCellNode(selection.anchor.getNode()) &&
689
+ !selection.anchor.getNode().is(selection.focus.getNode())
690
+ ) {
691
+ return true;
692
+ }
693
+ return false;
694
+ }