import {$getParentOfType} from "../../../utils/nodes";
import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell-node";
import {showCellPropertiesForm} from "../forms/tables";
+import {$mergeTableCellsInSelection} from "../../../utils/tables";
const neverActive = (): boolean => false;
const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
label: 'Merge cells',
action(context: EditorUiContext) {
context.editor.update(() => {
- // Todo - Needs to be done manually
- // Playground reference:
- // https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
+ const selection = $getSelection();
+ if ($isTableSelection(selection)) {
+ $mergeTableCellsInSelection(selection);
+ }
});
},
isActive: neverActive,
--- /dev/null
+import {CustomTableNode} from "../nodes/custom-table";
+import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
+import {$isTableRowNode} from "@lexical/table";
+
+export class TableMap {
+
+ rowCount: number = 0;
+ columnCount: number = 0;
+
+ // Represents an array (rows*columns in length) of cell nodes from top-left to
+ // bottom right. Cells may repeat where merged and covering multiple spaces.
+ cells: CustomTableCellNode[] = [];
+
+ constructor(table: CustomTableNode) {
+ this.buildCellMap(table);
+ }
+
+ protected buildCellMap(table: CustomTableNode) {
+ const rowsAndCells: CustomTableCellNode[][] = [];
+ const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
+ if (typeof rowsAndCells[y] === 'undefined') {
+ rowsAndCells[y] = [];
+ }
+
+ rowsAndCells[y][x] = cell;
+ };
+ const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]);
+
+ const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
+ for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
+ const rowNode = rowNodes[rowIndex];
+ const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c));
+ let targetColIndex: number = 0;
+ for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
+ const cellNode = cellNodes[cellIndex];
+ const colspan = cellNode.getColSpan() || 1;
+ const rowSpan = cellNode.getRowSpan() || 1;
+ for (let x = targetColIndex; x < targetColIndex + colspan; x++) {
+ for (let y = rowIndex; y < rowIndex + rowSpan; y++) {
+ while (cellFilled(x, y)) {
+ targetColIndex += 1;
+ x += 1;
+ }
+
+ setCell(x, y, cellNode);
+ }
+ }
+ targetColIndex += colspan;
+ }
+ }
+
+ this.rowCount = rowsAndCells.length;
+ this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
+
+ const cells = [];
+ let lastCell: CustomTableCellNode = rowsAndCells[0][0];
+ for (let y = 0; y < this.rowCount; y++) {
+ for (let x = 0; x < this.columnCount; x++) {
+ if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
+ cells.push(lastCell);
+ } else {
+ cells.push(rowsAndCells[y][x]);
+ lastCell = rowsAndCells[y][x];
+ }
+ }
+ }
+
+ this.cells = cells;
+ }
+
+ public getCellAtPosition(x: number, y: number): CustomTableCellNode {
+ const position = (y * this.columnCount) + x;
+ if (position >= this.cells.length) {
+ throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
+ }
+
+ return this.cells[position];
+ }
+
+ public getCellsInRange(fromX: number, fromY: number, toX: number, toY: number): CustomTableCellNode[] {
+ const minX = Math.max(Math.min(fromX, toX), 0);
+ const maxX = Math.min(Math.max(fromX, toX), this.columnCount - 1);
+ const minY = Math.max(Math.min(fromY, toY), 0);
+ const maxY = Math.min(Math.max(fromY, toY), this.rowCount - 1);
+
+ const cells = new Set<CustomTableCellNode>();
+
+ for (let y = minY; y <= maxY; y++) {
+ for (let x = minX; x <= maxX; x++) {
+ cells.add(this.getCellAtPosition(x, y));
+ }
+ }
+
+ return [...cells.values()];
+ }
+}
import {BaseSelection, LexicalEditor} from "lexical";
-import {$isTableRowNode, $isTableSelection, TableRowNode} from "@lexical/table";
+import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table";
import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
import {$getParentOfType} from "./nodes";
import {$getNodeFromSelection} from "./selection";
import {formatSizeValue} from "./dom";
+import {TableMap} from "./table-map";
function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
return cell ? [cell] : [];
-}
\ No newline at end of file
+}
+
+export function $mergeTableCellsInSelection(selection: TableSelection): void {
+ const selectionShape = selection.getShape();
+ const cells = $getTableCellsFromSelection(selection);
+ if (cells.length === 0) {
+ return;
+ }
+
+ const table = $getTableFromCell(cells[0]);
+ if (!table) {
+ return;
+ }
+
+ const tableMap = new TableMap(table);
+ const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
+ if (!headCell) {
+ return;
+ }
+
+ // We have to adjust the shape since it won't take into account spans for the head corner position.
+ const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
+ const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
+
+ const mergeCells = tableMap.getCellsInRange(
+ selectionShape.fromX,
+ selectionShape.fromY,
+ fixedToX,
+ fixedToY,
+ );
+
+ if (mergeCells.length === 0) {
+ return;
+ }
+
+ const firstCell = mergeCells[0];
+ const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
+ const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
+
+ for (let i = 1; i < mergeCells.length; i++) {
+ const mergeCell = mergeCells[i];
+ firstCell.append(...mergeCell.getChildren());
+ mergeCell.remove();
+ }
+
+ firstCell.setColSpan(newWidth);
+ firstCell.setRowSpan(newHeight);
+}
+
+
+
+
+
+
+
+
+
+
+