@lexical/table 0.29.1-nightly.20250401.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.
- package/LexicalTable.dev.js +256 -52
- package/LexicalTable.dev.mjs +259 -56
- package/LexicalTable.mjs +1 -0
- package/LexicalTable.node.mjs +1 -0
- package/LexicalTable.prod.js +1 -1
- package/LexicalTable.prod.mjs +1 -1
- package/LexicalTableUtils.d.ts +14 -0
- package/index.d.ts +1 -1
- package/package.json +4 -4
package/LexicalTable.dev.js
CHANGED
@@ -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 [
|
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 =
|
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 =
|
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 [
|
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
|
721
|
-
|
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
|
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
|
2290
|
-
const
|
2291
|
-
|
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
|
2300
|
-
const
|
2301
|
-
const
|
2302
|
-
const
|
2303
|
-
const
|
2304
|
-
|
2305
|
-
|
2306
|
-
|
2307
|
-
|
2308
|
-
let
|
2309
|
-
|
2310
|
-
|
2311
|
-
|
2312
|
-
|
2313
|
-
|
2314
|
-
const
|
2315
|
-
|
2316
|
-
|
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
|
-
|
2319
|
-
|
2320
|
-
|
2321
|
-
|
2322
|
-
|
2323
|
-
|
2324
|
-
|
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
|
2327
|
-
if (
|
2328
|
-
|
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
|
2331
|
-
|
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
|
-
|
2533
|
+
cell.append(child);
|
2336
2534
|
} else {
|
2337
|
-
|
2535
|
+
cell.append(child);
|
2338
2536
|
}
|
2339
2537
|
});
|
2340
2538
|
originalChildren.forEach(n => n.remove());
|
2341
|
-
newColumnIdx++;
|
2342
2539
|
}
|
2343
|
-
|
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;
|