import {NodeClipboard} from "./node-clipboard"; import {CustomTableRowNode} from "../nodes/custom-table-row"; import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables"; import {$getSelection, BaseSelection, LexicalEditor} from "lexical"; import {$createCustomTableCellNode, $isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell"; import {CustomTableNode} from "../nodes/custom-table"; import {TableMap} from "./table-map"; import {$isTableSelection} from "@lexical/table"; import {$getNodeFromSelection} from "./selection"; const rowClipboard: NodeClipboard = new NodeClipboard(); export function isRowClipboardEmpty(): boolean { return rowClipboard.size() === 0; } export function validateRowsToCopy(rows: CustomTableRowNode[]): void { let commonRowSize: number|null = null; for (const row of rows) { const cells = row.getChildren().filter(n => $isCustomTableCellNode(n)); let rowSize = 0; for (const cell of cells) { rowSize += cell.getColSpan() || 1; if (cell.getRowSpan() > 1) { throw Error('Cannot copy rows with merged cells'); } } if (commonRowSize === null) { commonRowSize = rowSize; } else if (commonRowSize !== rowSize) { throw Error('Cannot copy rows with inconsistent sizes'); } } } export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void { const tableColCount = (new TableMap(targetTable)).columnCount; for (const row of rows) { const cells = row.getChildren().filter(n => $isCustomTableCellNode(n)); let rowSize = 0; for (const cell of cells) { rowSize += cell.getColSpan() || 1; } if (rowSize > tableColCount) { throw Error('Cannot paste rows that are wider than target table'); } while (rowSize < tableColCount) { row.append($createCustomTableCellNode()); rowSize++; } } } export function $cutSelectedRowsToClipboard(): void { const rows = $getTableRowsFromSelection($getSelection()); validateRowsToCopy(rows); rowClipboard.set(...rows); for (const row of rows) { row.remove(); } } export function $copySelectedRowsToClipboard(): void { const rows = $getTableRowsFromSelection($getSelection()); validateRowsToCopy(rows); rowClipboard.set(...rows); } export function $pasteClipboardRowsBefore(editor: LexicalEditor): void { const selection = $getSelection(); const rows = $getTableRowsFromSelection(selection); const table = $getTableFromSelection(selection); const lastRow = rows[rows.length - 1]; if (lastRow && table) { const clipboardRows = rowClipboard.get(editor); validateRowsToPaste(clipboardRows, table); for (const row of clipboardRows) { lastRow.insertBefore(row); } } } export function $pasteClipboardRowsAfter(editor: LexicalEditor): void { const selection = $getSelection(); const rows = $getTableRowsFromSelection(selection); const table = $getTableFromSelection(selection); const lastRow = rows[rows.length - 1]; if (lastRow && table) { const clipboardRows = rowClipboard.get(editor).reverse(); validateRowsToPaste(clipboardRows, table); for (const row of clipboardRows) { lastRow.insertAfter(row); } } } const columnClipboard: NodeClipboard[] = []; function setColumnClipboard(columns: CustomTableCellNode[][]): 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, $isCustomTableCellNode); const table = $getTableFromSelection(selection); if (!$isCustomTableCellNode(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: CustomTableNode): CustomTableCellNode[][] { 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: CustomTableCellNode[][]): 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: CustomTableCellNode[][], targetTable: CustomTableNode) { 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($createCustomTableCellNode()); 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); }