} from "../../../utils/tables";
import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
import {
+ $copySelectedColumnsToClipboard,
$copySelectedRowsToClipboard,
+ $cutSelectedColumnsToClipboard,
$cutSelectedRowsToClipboard,
- $pasteClipboardRowsBefore, $pasteRowsAfter, isRowClipboardEmpty
+ $pasteClipboardRowsBefore,
+ $pasteClipboardRowsAfter,
+ isColumnClipboardEmpty,
+ isRowClipboardEmpty,
+ $pasteClipboardColumnsBefore, $pasteClipboardColumnsAfter
} from "../../../utils/table-copy-paste";
const neverActive = (): boolean => false;
try {
$cutSelectedRowsToClipboard();
} catch (e: any) {
- context.error(e.toString());
+ context.error(e);
}
});
},
try {
$copySelectedRowsToClipboard();
} catch (e: any) {
- context.error(e.toString());
+ context.error(e);
}
});
},
try {
$pasteClipboardRowsBefore(context.editor);
} catch (e: any) {
- context.error(e.toString());
+ context.error(e);
}
});
},
action(context: EditorUiContext) {
context.editor.update(() => {
try {
- $pasteRowsAfter(context.editor);
+ $pasteClipboardRowsAfter(context.editor);
} catch (e: any) {
- context.error(e.toString());
+ context.error(e);
}
});
},
label: 'Cut column',
format: 'long',
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
- // TODO
+ context.editor.update(() => {
+ try {
+ $cutSelectedColumnsToClipboard();
+ } catch (e: any) {
+ context.error(e);
+ }
});
},
isActive: neverActive,
format: 'long',
action(context: EditorUiContext) {
context.editor.getEditorState().read(() => {
- // TODO
+ try {
+ $copySelectedColumnsToClipboard();
+ } catch (e: any) {
+ context.error(e);
+ }
});
},
isActive: neverActive,
label: 'Paste column before',
format: 'long',
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
- // TODO
+ context.editor.update(() => {
+ try {
+ $pasteClipboardColumnsBefore(context.editor);
+ } catch (e: any) {
+ context.error(e);
+ }
});
},
isActive: neverActive,
- isDisabled: cellNotSelected,
+ isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),
};
export const pasteColumnAfter: EditorButtonDefinition = {
label: 'Paste column after',
format: 'long',
action(context: EditorUiContext) {
- context.editor.getEditorState().read(() => {
- // TODO
+ context.editor.update(() => {
+ try {
+ $pasteClipboardColumnsAfter(context.editor);
+ } catch (e: any) {
+ context.error(e);
+ }
});
},
isActive: neverActive,
- isDisabled: cellNotSelected,
+ isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),
};
export const insertColumnBefore: EditorButtonDefinition = {
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 {$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<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
+const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>();
export function isRowClipboardEmpty(): boolean {
return rowClipboard.size() === 0;
}
}
-export function $pasteRowsAfter(editor: LexicalEditor): void {
+export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
const selection = $getSelection();
const rows = $getTableRowsFromSelection(selection);
const table = $getTableFromSelection(selection);
lastRow.insertAfter(row);
}
}
+}
+
+const columnClipboard: NodeClipboard<CustomTableCellNode>[] = [];
+
+function setColumnClipboard(columns: CustomTableCellNode[][]): void {
+ const newClipboards = columns.map(cells => {
+ const clipboard = new NodeClipboard<CustomTableCellNode>();
+ 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);
}
\ No newline at end of file
import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
import {$isTableRowNode} from "@lexical/table";
+export type CellRange = {
+ fromX: number;
+ fromY: number;
+ toX: number;
+ toY: number;
+}
+
export class TableMap {
rowCount: number = 0;
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);
+ public getCellsInRange(range: CellRange): CustomTableCellNode[] {
+ const minX = Math.max(Math.min(range.fromX, range.toX), 0);
+ const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
+ const minY = Math.max(Math.min(range.fromY, range.toY), 0);
+ const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
const cells = new Set<CustomTableCellNode>();
return [...cells.values()];
}
+
+ public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
+ return this.getCellsInRange({
+ fromX: columnIndex,
+ toX: columnIndex,
+ fromY: 0,
+ toY: this.rowCount - 1,
+ });
+ }
+
+ public getRangeForCell(cell: CustomTableCellNode): CellRange|null {
+ let range: CellRange|null = null;
+ const cellKey = cell.getKey();
+
+ for (let y = 0; y < this.rowCount; y++) {
+ for (let x = 0; x < this.columnCount; x++) {
+ const index = (y * this.columnCount) + x;
+ const lCell = this.cells[index];
+ if (lCell.getKey() === cellKey) {
+ if (range === null) {
+ range = {fromX: x, toX: x, fromY: y, toY: y};
+ } else {
+ range.fromX = Math.min(range.fromX, x);
+ range.toX = Math.max(range.toX, x);
+ range.fromY = Math.min(range.fromY, y);
+ range.toY = Math.max(range.toY, y);
+ }
+ }
+ }
+ }
+
+ return range;
+ }
}
\ No newline at end of file