@lexical/table 0.29.1-nightly.20250331.0 → 0.29.1-nightly.20250402.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.
@@ -596,17 +596,35 @@ function $insertTableRow__EXPERIMENTAL(insertAfter = true) {
596
596
  const focus = selection.focus.getNode();
597
597
  const [anchorCell] = $getNodeTriplet(anchor);
598
598
  const [focusCell,, grid] = $getNodeTriplet(focus);
599
- const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
600
- const columnCount = gridMap[0].length;
599
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
601
600
  const {
602
601
  startRow: anchorStartRow
603
602
  } = anchorCellMap;
604
603
  const {
605
604
  startRow: focusStartRow
606
605
  } = focusCellMap;
606
+ if (insertAfter) {
607
+ return $insertTableRowAtNode(anchorStartRow + anchorCell.__rowSpan > focusStartRow + focusCell.__rowSpan ? anchorCell : focusCell, true);
608
+ } else {
609
+ return $insertTableRowAtNode(focusStartRow < anchorStartRow ? focusCell : anchorCell, false);
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Inserts a table row before or after the given cell node,
615
+ * taking into account any spans. If successful, returns the
616
+ * inserted table row node.
617
+ */
618
+ function $insertTableRowAtNode(cellNode, insertAfter = true) {
619
+ const [,, grid] = $getNodeTriplet(cellNode);
620
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
621
+ const columnCount = gridMap[0].length;
622
+ const {
623
+ startRow: cellStartRow
624
+ } = cellMap;
607
625
  let insertedRow = null;
608
626
  if (insertAfter) {
609
- const insertAfterEndRow = Math.max(focusStartRow + focusCell.__rowSpan, anchorStartRow + anchorCell.__rowSpan) - 1;
627
+ const insertAfterEndRow = cellStartRow + cellNode.__rowSpan - 1;
610
628
  const insertAfterEndRowMap = gridMap[insertAfterEndRow];
611
629
  const newRow = $createTableRowNode();
612
630
  for (let i = 0; i < columnCount; i++) {
@@ -630,7 +648,7 @@ function $insertTableRow__EXPERIMENTAL(insertAfter = true) {
630
648
  insertAfterEndRowNode.insertAfter(newRow);
631
649
  insertedRow = newRow;
632
650
  } else {
633
- const insertBeforeStartRow = Math.min(focusStartRow, anchorStartRow);
651
+ const insertBeforeStartRow = cellStartRow;
634
652
  const insertBeforeStartRowMap = gridMap[insertBeforeStartRow];
635
653
  const newRow = $createTableRowNode();
636
654
  for (let i = 0; i < columnCount; i++) {
@@ -715,10 +733,33 @@ function $insertTableColumn__EXPERIMENTAL(insertAfter = true) {
715
733
  const focus = selection.focus.getNode();
716
734
  const [anchorCell] = $getNodeTriplet(anchor);
717
735
  const [focusCell,, grid] = $getNodeTriplet(focus);
718
- const [gridMap, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
736
+ const [, focusCellMap, anchorCellMap] = $computeTableMap(grid, focusCell, anchorCell);
737
+ const {
738
+ startColumn: anchorStartColumn
739
+ } = anchorCellMap;
740
+ const {
741
+ startColumn: focusStartColumn
742
+ } = focusCellMap;
743
+ if (insertAfter) {
744
+ return $insertTableColumnAtNode(anchorStartColumn + anchorCell.__colSpan > focusStartColumn + focusCell.__colSpan ? anchorCell : focusCell, true);
745
+ } else {
746
+ return $insertTableColumnAtNode(focusStartColumn < anchorStartColumn ? focusCell : anchorCell, false);
747
+ }
748
+ }
749
+
750
+ /**
751
+ * Inserts a column before or after the given cell node,
752
+ * taking into account any spans. If successful, returns the
753
+ * first inserted cell node.
754
+ */
755
+ function $insertTableColumnAtNode(cellNode, insertAfter = true, shouldSetSelection = true) {
756
+ const [,, grid] = $getNodeTriplet(cellNode);
757
+ const [gridMap, cellMap] = $computeTableMap(grid, cellNode, cellNode);
719
758
  const rowCount = gridMap.length;
720
- const startColumn = insertAfter ? Math.max(focusCellMap.startColumn, anchorCellMap.startColumn) : Math.min(focusCellMap.startColumn, anchorCellMap.startColumn);
721
- const insertAfterColumn = insertAfter ? startColumn + focusCell.__colSpan - 1 : startColumn - 1;
759
+ const {
760
+ startColumn
761
+ } = cellMap;
762
+ const insertAfterColumn = insertAfter ? startColumn + cellNode.__colSpan - 1 : startColumn - 1;
722
763
  const gridFirstChild = grid.getFirstChild();
723
764
  if (!$isTableRowNode(gridFirstChild)) {
724
765
  formatDevErrorMessage(`Expected firstTable child to be a row`);
@@ -775,7 +816,7 @@ function $insertTableColumn__EXPERIMENTAL(insertAfter = true) {
775
816
  currentCell.setColSpan(currentCell.__colSpan + 1);
776
817
  }
777
818
  }
778
- if (firstInsertedCell !== null) {
819
+ if (firstInsertedCell !== null && shouldSetSelection) {
779
820
  $moveSelectionToCell(firstInsertedCell);
780
821
  }
781
822
  const colWidths = grid.getColWidths();
@@ -977,13 +1018,122 @@ function $insertFirst(parent, node) {
977
1018
  parent.append(node);
978
1019
  }
979
1020
  }
1021
+ function $mergeCells(cellNodes) {
1022
+ if (cellNodes.length === 0) {
1023
+ return null;
1024
+ }
1025
+
1026
+ // Find the table node
1027
+ const tableNode = $getTableNodeFromLexicalNodeOrThrow(cellNodes[0]);
1028
+ const [gridMap] = $computeTableMapSkipCellCheck(tableNode, null, null);
1029
+
1030
+ // Find the boundaries of the selection including merged cells
1031
+ let minRow = Infinity;
1032
+ let maxRow = -Infinity;
1033
+ let minCol = Infinity;
1034
+ let maxCol = -Infinity;
1035
+
1036
+ // First pass: find the actual boundaries considering merged cells
1037
+ const processedCells = new Set();
1038
+ for (const row of gridMap) {
1039
+ for (const mapCell of row) {
1040
+ if (!mapCell || !mapCell.cell) {
1041
+ continue;
1042
+ }
1043
+ const cellKey = mapCell.cell.getKey();
1044
+ if (processedCells.has(cellKey)) {
1045
+ continue;
1046
+ }
1047
+ if (cellNodes.some(cell => cell.is(mapCell.cell))) {
1048
+ processedCells.add(cellKey);
1049
+ // Get the actual position of this cell in the grid
1050
+ const cellStartRow = mapCell.startRow;
1051
+ const cellStartCol = mapCell.startColumn;
1052
+ const cellRowSpan = mapCell.cell.__rowSpan || 1;
1053
+ const cellColSpan = mapCell.cell.__colSpan || 1;
1054
+
1055
+ // Update boundaries considering the cell's actual position and span
1056
+ minRow = Math.min(minRow, cellStartRow);
1057
+ maxRow = Math.max(maxRow, cellStartRow + cellRowSpan - 1);
1058
+ minCol = Math.min(minCol, cellStartCol);
1059
+ maxCol = Math.max(maxCol, cellStartCol + cellColSpan - 1);
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ // Validate boundaries
1065
+ if (minRow === Infinity || minCol === Infinity) {
1066
+ return null;
1067
+ }
1068
+
1069
+ // The total span of the merged cell
1070
+ const totalRowSpan = maxRow - minRow + 1;
1071
+ const totalColSpan = maxCol - minCol + 1;
1072
+
1073
+ // Use the top-left cell as the target cell
1074
+ const targetCellMap = gridMap[minRow][minCol];
1075
+ if (!targetCellMap.cell) {
1076
+ return null;
1077
+ }
1078
+ const targetCell = targetCellMap.cell;
1079
+
1080
+ // Set the spans for the target cell
1081
+ targetCell.setColSpan(totalColSpan);
1082
+ targetCell.setRowSpan(totalRowSpan);
1083
+
1084
+ // Move content from other cells to the target cell
1085
+ const seenCells = new Set([targetCell.getKey()]);
1086
+
1087
+ // Second pass: merge content and remove other cells
1088
+ for (let row = minRow; row <= maxRow; row++) {
1089
+ for (let col = minCol; col <= maxCol; col++) {
1090
+ const mapCell = gridMap[row][col];
1091
+ if (!mapCell.cell) {
1092
+ continue;
1093
+ }
1094
+ const currentCell = mapCell.cell;
1095
+ const key = currentCell.getKey();
1096
+ if (!seenCells.has(key)) {
1097
+ seenCells.add(key);
1098
+ const isEmpty = $cellContainsEmptyParagraph(currentCell);
1099
+ if (!isEmpty) {
1100
+ targetCell.append(...currentCell.getChildren());
1101
+ }
1102
+ currentCell.remove();
1103
+ }
1104
+ }
1105
+ }
1106
+
1107
+ // Ensure target cell has content
1108
+ if (targetCell.getChildrenSize() === 0) {
1109
+ targetCell.append(lexical.$createParagraphNode());
1110
+ }
1111
+ return targetCell;
1112
+ }
1113
+ function $cellContainsEmptyParagraph(cell) {
1114
+ if (cell.getChildrenSize() !== 1) {
1115
+ return false;
1116
+ }
1117
+ const firstChild = cell.getFirstChildOrThrow();
1118
+ if (!lexical.$isParagraphNode(firstChild) || !firstChild.isEmpty()) {
1119
+ return false;
1120
+ }
1121
+ return true;
1122
+ }
980
1123
  function $unmergeCell() {
981
1124
  const selection = lexical.$getSelection();
982
1125
  if (!(lexical.$isRangeSelection(selection) || $isTableSelection(selection))) {
983
1126
  formatDevErrorMessage(`Expected a RangeSelection or TableSelection`);
984
1127
  }
985
1128
  const anchor = selection.anchor.getNode();
986
- const [cell, row, grid] = $getNodeTriplet(anchor);
1129
+ const cellNode = utils.$findMatchingParent(anchor, $isTableCellNode);
1130
+ if (!$isTableCellNode(cellNode)) {
1131
+ formatDevErrorMessage(`Expected to find a parent TableCellNode`);
1132
+ }
1133
+ return $unmergeCellNode(cellNode);
1134
+ }
1135
+ function $unmergeCellNode(cellNode) {
1136
+ const [cell, row, grid] = $getNodeTriplet(cellNode);
987
1137
  const colSpan = cell.__colSpan;
988
1138
  const rowSpan = cell.__rowSpan;
989
1139
  if (colSpan === 1 && rowSpan === 1) {
@@ -2285,62 +2435,115 @@ function applyTableHandlers(tableNode, element, editor, hasTabHandler) {
2285
2435
  if (nodes.length !== 1 || !$isTableNode(nodes[0]) || !isSelectionInsideOfGrid || anchorAndFocus === null) {
2286
2436
  return false;
2287
2437
  }
2288
- const [anchor] = anchorAndFocus;
2289
- const newGrid = nodes[0];
2290
- const newGridRows = newGrid.getChildren();
2291
- const newColumnCount = newGrid.getFirstChildOrThrow().getChildrenSize();
2292
- const newRowCount = newGrid.getChildrenSize();
2293
- const gridCellNode = utils.$findMatchingParent(anchor.getNode(), n => $isTableCellNode(n));
2294
- const gridRowNode = gridCellNode && utils.$findMatchingParent(gridCellNode, n => $isTableRowNode(n));
2295
- const gridNode = gridRowNode && utils.$findMatchingParent(gridRowNode, n => $isTableNode(n));
2296
- if (!$isTableCellNode(gridCellNode) || !$isTableRowNode(gridRowNode) || !$isTableNode(gridNode)) {
2438
+ const [anchor, focus] = anchorAndFocus;
2439
+ const [anchorCellNode, anchorRowNode, gridNode] = $getNodeTriplet(anchor);
2440
+ const focusCellNode = utils.$findMatchingParent(focus.getNode(), n => $isTableCellNode(n));
2441
+ if (!$isTableCellNode(anchorCellNode) || !$isTableCellNode(focusCellNode) || !$isTableRowNode(anchorRowNode) || !$isTableNode(gridNode)) {
2297
2442
  return false;
2298
2443
  }
2299
- const startY = gridRowNode.getIndexWithinParent();
2300
- const stopY = Math.min(gridNode.getChildrenSize() - 1, startY + newRowCount - 1);
2301
- const startX = gridCellNode.getIndexWithinParent();
2302
- const stopX = Math.min(gridRowNode.getChildrenSize() - 1, startX + newColumnCount - 1);
2303
- const fromX = Math.min(startX, stopX);
2304
- const fromY = Math.min(startY, stopY);
2305
- const toX = Math.max(startX, stopX);
2306
- const toY = Math.max(startY, stopY);
2307
- const gridRowNodes = gridNode.getChildren();
2308
- let newRowIdx = 0;
2309
- for (let r = fromY; r <= toY; r++) {
2310
- const currentGridRowNode = gridRowNodes[r];
2311
- if (!$isTableRowNode(currentGridRowNode)) {
2312
- return false;
2313
- }
2314
- const newGridRowNode = newGridRows[newRowIdx];
2315
- if (!$isTableRowNode(newGridRowNode)) {
2316
- return false;
2444
+ const templateGrid = nodes[0];
2445
+ const [initialGridMap, anchorCellMap, focusCellMap] = $computeTableMap(gridNode, anchorCellNode, focusCellNode);
2446
+ const [templateGridMap] = $computeTableMapSkipCellCheck(templateGrid, null, null);
2447
+ const initialRowCount = initialGridMap.length;
2448
+ const initialColCount = initialRowCount > 0 ? initialGridMap[0].length : 0;
2449
+
2450
+ // If we have a range selection, we'll fit the template grid into the
2451
+ // table, growing the table if necessary.
2452
+ let startRow = anchorCellMap.startRow;
2453
+ let startCol = anchorCellMap.startColumn;
2454
+ let affectedRowCount = templateGridMap.length;
2455
+ let affectedColCount = affectedRowCount > 0 ? templateGridMap[0].length : 0;
2456
+ if (isTableSelection) {
2457
+ // If we have a table selection, we'll only modify the cells within
2458
+ // the selection boundary.
2459
+ const selectionBoundary = $computeTableCellRectBoundary(initialGridMap, anchorCellMap, focusCellMap);
2460
+ const selectionRowCount = selectionBoundary.maxRow - selectionBoundary.minRow + 1;
2461
+ const selectionColCount = selectionBoundary.maxColumn - selectionBoundary.minColumn + 1;
2462
+ startRow = selectionBoundary.minRow;
2463
+ startCol = selectionBoundary.minColumn;
2464
+ affectedRowCount = Math.min(affectedRowCount, selectionRowCount);
2465
+ affectedColCount = Math.min(affectedColCount, selectionColCount);
2466
+ }
2467
+
2468
+ // Step 1: Unmerge all merged cells within the affected area
2469
+ let didPerformMergeOperations = false;
2470
+ const lastRowForUnmerge = Math.min(initialRowCount, startRow + affectedRowCount) - 1;
2471
+ const lastColForUnmerge = Math.min(initialColCount, startCol + affectedColCount) - 1;
2472
+ const unmergedKeys = new Set();
2473
+ for (let row = startRow; row <= lastRowForUnmerge; row++) {
2474
+ for (let col = startCol; col <= lastColForUnmerge; col++) {
2475
+ const cellMap = initialGridMap[row][col];
2476
+ if (unmergedKeys.has(cellMap.cell.getKey())) {
2477
+ continue; // cell was a merged cell that was already handled
2478
+ }
2479
+ if (cellMap.cell.__rowSpan === 1 && cellMap.cell.__colSpan === 1) {
2480
+ continue; // cell is not a merged cell
2481
+ }
2482
+ $unmergeCellNode(cellMap.cell);
2483
+ unmergedKeys.add(cellMap.cell.getKey());
2484
+ didPerformMergeOperations = true;
2317
2485
  }
2318
- const gridCellNodes = currentGridRowNode.getChildren();
2319
- const newGridCellNodes = newGridRowNode.getChildren();
2320
- let newColumnIdx = 0;
2321
- for (let c = fromX; c <= toX; c++) {
2322
- const currentGridCellNode = gridCellNodes[c];
2323
- if (!$isTableCellNode(currentGridCellNode)) {
2324
- return false;
2486
+ }
2487
+ let [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2488
+
2489
+ // Step 2: Expand current table (if needed)
2490
+ const rowsToInsert = affectedRowCount - initialRowCount + startRow;
2491
+ for (let i = 0; i < rowsToInsert; i++) {
2492
+ const cellMap = interimGridMap[initialRowCount - 1][0];
2493
+ $insertTableRowAtNode(cellMap.cell);
2494
+ }
2495
+ const colsToInsert = affectedColCount - initialColCount + startCol;
2496
+ for (let i = 0; i < colsToInsert; i++) {
2497
+ const cellMap = interimGridMap[0][initialColCount - 1];
2498
+ $insertTableColumnAtNode(cellMap.cell, true, false);
2499
+ }
2500
+ [interimGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2501
+
2502
+ // Step 3: Merge cells and set cell content, to match template grid
2503
+ for (let row = startRow; row < startRow + affectedRowCount; row++) {
2504
+ for (let col = startCol; col < startCol + affectedColCount; col++) {
2505
+ const templateRow = row - startRow;
2506
+ const templateCol = col - startCol;
2507
+ const templateCellMap = templateGridMap[templateRow][templateCol];
2508
+ if (templateCellMap.startRow !== templateRow || templateCellMap.startColumn !== templateCol) {
2509
+ continue; // cell is a merged cell that was already handled
2325
2510
  }
2326
- const newGridCellNode = newGridCellNodes[newColumnIdx];
2327
- if (!$isTableCellNode(newGridCellNode)) {
2328
- return false;
2511
+ const templateCell = templateCellMap.cell;
2512
+ if (templateCell.__rowSpan !== 1 || templateCell.__colSpan !== 1) {
2513
+ const cellsToMerge = [];
2514
+ const lastRowForMerge = Math.min(row + templateCell.__rowSpan, startRow + affectedRowCount) - 1;
2515
+ const lastColForMerge = Math.min(col + templateCell.__colSpan, startCol + affectedColCount) - 1;
2516
+ for (let r = row; r <= lastRowForMerge; r++) {
2517
+ for (let c = col; c <= lastColForMerge; c++) {
2518
+ const cellMap = interimGridMap[r][c];
2519
+ cellsToMerge.push(cellMap.cell);
2520
+ }
2521
+ }
2522
+ $mergeCells(cellsToMerge);
2523
+ didPerformMergeOperations = true;
2329
2524
  }
2330
- const originalChildren = currentGridCellNode.getChildren();
2331
- newGridCellNode.getChildren().forEach(child => {
2525
+ const {
2526
+ cell
2527
+ } = interimGridMap[row][col];
2528
+ const originalChildren = cell.getChildren();
2529
+ templateCell.getChildren().forEach(child => {
2332
2530
  if (lexical.$isTextNode(child)) {
2333
2531
  const paragraphNode = lexical.$createParagraphNode();
2334
2532
  paragraphNode.append(child);
2335
- currentGridCellNode.append(child);
2533
+ cell.append(child);
2336
2534
  } else {
2337
- currentGridCellNode.append(child);
2535
+ cell.append(child);
2338
2536
  }
2339
2537
  });
2340
2538
  originalChildren.forEach(n => n.remove());
2341
- newColumnIdx++;
2342
2539
  }
2343
- newRowIdx++;
2540
+ }
2541
+ if (isTableSelection && didPerformMergeOperations) {
2542
+ // reset the table selection in case the anchor or focus cell was
2543
+ // removed via merge operations
2544
+ const [finalGridMap] = $computeTableMapSkipCellCheck(gridNode.getWritable(), null, null);
2545
+ const newAnchorCellMap = finalGridMap[anchorCellMap.startRow][anchorCellMap.startColumn];
2546
+ newAnchorCellMap.cell.selectEnd();
2344
2547
  }
2345
2548
  return true;
2346
2549
  }, lexical.COMMAND_PRIORITY_CRITICAL));
@@ -3922,6 +4125,7 @@ exports.$isTableCellNode = $isTableCellNode;
3922
4125
  exports.$isTableNode = $isTableNode;
3923
4126
  exports.$isTableRowNode = $isTableRowNode;
3924
4127
  exports.$isTableSelection = $isTableSelection;
4128
+ exports.$mergeCells = $mergeCells;
3925
4129
  exports.$removeTableRowAtIndex = $removeTableRowAtIndex;
3926
4130
  exports.$unmergeCell = $unmergeCell;
3927
4131
  exports.INSERT_TABLE_COMMAND = INSERT_TABLE_COMMAND;