]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/utils/table-copy-paste.ts
Tests: Updated comment test to account for new editor usage
[bookstack] / resources / js / wysiwyg / utils / table-copy-paste.ts
index ae8ef3d35870b27bccec2a94c754768cbce3cd77..1e024e4c7581199932c03ab108f168e3b44f9292 100644 (file)
@@ -1,22 +1,28 @@
 import {NodeClipboard} from "./node-clipboard";
-import {CustomTableRowNode} from "../nodes/custom-table-row";
-import {$getTableFromSelection, $getTableRowsFromSelection} from "./tables";
-import {$getSelection, LexicalEditor} from "lexical";
-import {$createCustomTableCellNode, $isCustomTableCellNode} from "../nodes/custom-table-cell";
-import {CustomTableNode} from "../nodes/custom-table";
+import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
+import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
 import {TableMap} from "./table-map";
+import {
+    $createTableCellNode,
+    $isTableCellNode,
+    $isTableSelection,
+    TableCellNode,
+    TableNode,
+    TableRowNode
+} from "@lexical/table";
+import {$getNodeFromSelection} from "./selection";
 
-const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
+const rowClipboard: NodeClipboard<TableRowNode> = new NodeClipboard<TableRowNode>();
 
 export function isRowClipboardEmpty(): boolean {
     return rowClipboard.size() === 0;
 }
 
-export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
+export function validateRowsToCopy(rows: TableRowNode[]): void {
     let commonRowSize: number|null = null;
 
     for (const row of rows) {
-        const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
+        const cells = row.getChildren().filter(n => $isTableCellNode(n));
         let rowSize = 0;
         for (const cell of cells) {
             rowSize += cell.getColSpan() || 1;
@@ -33,10 +39,10 @@ export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
     }
 }
 
-export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void {
+export function validateRowsToPaste(rows: TableRowNode[], targetTable: TableNode): void {
     const tableColCount = (new TableMap(targetTable)).columnCount;
     for (const row of rows) {
-        const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
+        const cells = row.getChildren().filter(n => $isTableCellNode(n));
         let rowSize = 0;
         for (const cell of cells) {
             rowSize += cell.getColSpan() || 1;
@@ -47,7 +53,7 @@ export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: Cus
         }
 
         while (rowSize < tableColCount) {
-            row.append($createCustomTableCellNode());
+            row.append($createTableCellNode());
             rowSize++;
         }
     }
@@ -82,7 +88,7 @@ export function $pasteClipboardRowsBefore(editor: LexicalEditor): void {
     }
 }
 
-export function $pasteRowsAfter(editor: LexicalEditor): void {
+export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
     const selection = $getSelection();
     const rows = $getTableRowsFromSelection(selection);
     const table = $getTableFromSelection(selection);
@@ -94,4 +100,177 @@ export function $pasteRowsAfter(editor: LexicalEditor): void {
             lastRow.insertAfter(row);
         }
     }
+}
+
+const columnClipboard: NodeClipboard<TableCellNode>[] = [];
+
+function setColumnClipboard(columns: TableCellNode[][]): void {
+    const newClipboards = columns.map(cells => {
+        const clipboard = new NodeClipboard<TableCellNode>();
+        clipboard.set(...cells);
+        return clipboard;
+    });
+
+    columnClipboard.splice(0, columnClipboard.length, ...newClipboards);
+}
+
+type TableRange = {from: number, to: number};
+
+export function isColumnClipboardEmpty(): boolean {
+    return columnClipboard.length === 0;
+}
+
+function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|null {
+    if ($isTableSelection(selection)) {
+        const shape = selection.getShape()
+        return {from: shape.fromX, to: shape.toX};
+    }
+
+    const cell = $getNodeFromSelection(selection, $isTableCellNode);
+    const table = $getTableFromSelection(selection);
+    if (!$isTableCellNode(cell) || !table) {
+        return null;
+    }
+
+    const map = new TableMap(table);
+    const range = map.getRangeForCell(cell);
+    if (!range) {
+        return null;
+    }
+
+    return {from: range.fromX, to: range.toX};
+}
+
+function $getTableColumnCellsFromSelection(range: TableRange, table: TableNode): TableCellNode[][] {
+    const map = new TableMap(table);
+    const columns = [];
+    for (let x = range.from; x <= range.to; x++) {
+        const cells = map.getCellsInColumn(x);
+        columns.push(cells);
+    }
+
+    return columns;
+}
+
+function validateColumnsToCopy(columns: TableCellNode[][]): void {
+    let commonColSize: number|null = null;
+
+    for (const cells of columns) {
+        let colSize = 0;
+        for (const cell of cells) {
+            colSize += cell.getRowSpan() || 1;
+            if (cell.getColSpan() > 1) {
+                throw Error('Cannot copy columns with merged cells');
+            }
+        }
+
+        if (commonColSize === null) {
+            commonColSize = colSize;
+        } else if (commonColSize !== colSize) {
+            throw Error('Cannot copy columns with inconsistent sizes');
+        }
+    }
+}
+
+export function $cutSelectedColumnsToClipboard(): void {
+    const selection = $getSelection();
+    const range = $getSelectionColumnRange(selection);
+    const table = $getTableFromSelection(selection);
+    if (!range || !table) {
+        return;
+    }
+
+    const colWidths = table.getColWidths();
+    const columns = $getTableColumnCellsFromSelection(range, table);
+    validateColumnsToCopy(columns);
+    setColumnClipboard(columns);
+    for (const cells of columns) {
+        for (const cell of cells) {
+            cell.remove();
+        }
+    }
+
+    const newWidths = [...colWidths].splice(range.from, (range.to - range.from) + 1);
+    table.setColWidths(newWidths);
+}
+
+export function $copySelectedColumnsToClipboard(): void {
+    const selection = $getSelection();
+    const range = $getSelectionColumnRange(selection);
+    const table = $getTableFromSelection(selection);
+    if (!range || !table) {
+        return;
+    }
+
+    const columns = $getTableColumnCellsFromSelection(range, table);
+    validateColumnsToCopy(columns);
+    setColumnClipboard(columns);
+}
+
+function validateColumnsToPaste(columns: TableCellNode[][], targetTable: TableNode) {
+    const tableRowCount = (new TableMap(targetTable)).rowCount;
+    for (const cells of columns) {
+        let colSize = 0;
+        for (const cell of cells) {
+            colSize += cell.getRowSpan() || 1;
+        }
+
+        if (colSize > tableRowCount) {
+            throw Error('Cannot paste columns that are taller than target table');
+        }
+
+        while (colSize < tableRowCount) {
+            cells.push($createTableCellNode());
+            colSize++;
+        }
+    }
+}
+
+function $pasteClipboardColumns(editor: LexicalEditor, isBefore: boolean): void {
+    const selection = $getSelection();
+    const table = $getTableFromSelection(selection);
+    const cells = $getTableCellsFromSelection(selection);
+    const referenceCell = cells[isBefore ? 0 : cells.length - 1];
+    if (!table || !referenceCell) {
+        return;
+    }
+
+    const clipboardCols = columnClipboard.map(cb => cb.get(editor));
+    if (!isBefore) {
+        clipboardCols.reverse();
+    }
+
+    validateColumnsToPaste(clipboardCols, table);
+    const map = new TableMap(table);
+    const cellRange = map.getRangeForCell(referenceCell);
+    if (!cellRange) {
+        return;
+    }
+
+    const colIndex = isBefore ? cellRange.fromX : cellRange.toX;
+    const colWidths = table.getColWidths();
+
+    for (let y = 0; y < map.rowCount; y++) {
+        const relCell = map.getCellAtPosition(colIndex, y);
+        for (const cells of clipboardCols) {
+            const newCell = cells[y];
+            if (isBefore) {
+                relCell.insertBefore(newCell);
+            } else {
+                relCell.insertAfter(newCell);
+            }
+        }
+    }
+
+    const refWidth = colWidths[colIndex];
+    const addedWidths = clipboardCols.map(_ => refWidth);
+    colWidths.splice(isBefore ? colIndex : colIndex + 1, 0, ...addedWidths);
+}
+
+export function $pasteClipboardColumnsBefore(editor: LexicalEditor): void {
+    $pasteClipboardColumns(editor, true);
+}
+
+export function $pasteClipboardColumnsAfter(editor: LexicalEditor): void {
+    $pasteClipboardColumns(editor, false);
 }
\ No newline at end of file