X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/8a13a9df8092d1f7aad84fd960705380c181763e..refs/pull/5689/head:/resources/js/wysiwyg/utils/table-copy-paste.ts diff --git a/resources/js/wysiwyg/utils/table-copy-paste.ts b/resources/js/wysiwyg/utils/table-copy-paste.ts index ae8ef3d35..1e024e4c7 100644 --- a/resources/js/wysiwyg/utils/table-copy-paste.ts +++ b/resources/js/wysiwyg/utils/table-copy-paste.ts @@ -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 = new NodeClipboard(CustomTableRowNode); +const rowClipboard: NodeClipboard = new NodeClipboard(); 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[] = []; + +function setColumnClipboard(columns: TableCellNode[][]): void { + const newClipboards = columns.map(cells => { + const clipboard = new NodeClipboard(); + 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