1 import {NodeClipboard} from "./node-clipboard";
2 import {CustomTableRowNode} from "../nodes/custom-table-row";
3 import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
4 import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
5 import {$createCustomTableCellNode, $isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
6 import {CustomTableNode} from "../nodes/custom-table";
7 import {TableMap} from "./table-map";
8 import {$isTableSelection} from "@lexical/table";
9 import {$getNodeFromSelection} from "./selection";
11 const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>();
13 export function isRowClipboardEmpty(): boolean {
14 return rowClipboard.size() === 0;
17 export function validateRowsToCopy(rows: CustomTableRowNode[]): void {
18 let commonRowSize: number|null = null;
20 for (const row of rows) {
21 const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
23 for (const cell of cells) {
24 rowSize += cell.getColSpan() || 1;
25 if (cell.getRowSpan() > 1) {
26 throw Error('Cannot copy rows with merged cells');
30 if (commonRowSize === null) {
31 commonRowSize = rowSize;
32 } else if (commonRowSize !== rowSize) {
33 throw Error('Cannot copy rows with inconsistent sizes');
38 export function validateRowsToPaste(rows: CustomTableRowNode[], targetTable: CustomTableNode): void {
39 const tableColCount = (new TableMap(targetTable)).columnCount;
40 for (const row of rows) {
41 const cells = row.getChildren().filter(n => $isCustomTableCellNode(n));
43 for (const cell of cells) {
44 rowSize += cell.getColSpan() || 1;
47 if (rowSize > tableColCount) {
48 throw Error('Cannot paste rows that are wider than target table');
51 while (rowSize < tableColCount) {
52 row.append($createCustomTableCellNode());
58 export function $cutSelectedRowsToClipboard(): void {
59 const rows = $getTableRowsFromSelection($getSelection());
60 validateRowsToCopy(rows);
61 rowClipboard.set(...rows);
62 for (const row of rows) {
67 export function $copySelectedRowsToClipboard(): void {
68 const rows = $getTableRowsFromSelection($getSelection());
69 validateRowsToCopy(rows);
70 rowClipboard.set(...rows);
73 export function $pasteClipboardRowsBefore(editor: LexicalEditor): void {
74 const selection = $getSelection();
75 const rows = $getTableRowsFromSelection(selection);
76 const table = $getTableFromSelection(selection);
77 const lastRow = rows[rows.length - 1];
78 if (lastRow && table) {
79 const clipboardRows = rowClipboard.get(editor);
80 validateRowsToPaste(clipboardRows, table);
81 for (const row of clipboardRows) {
82 lastRow.insertBefore(row);
87 export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
88 const selection = $getSelection();
89 const rows = $getTableRowsFromSelection(selection);
90 const table = $getTableFromSelection(selection);
91 const lastRow = rows[rows.length - 1];
92 if (lastRow && table) {
93 const clipboardRows = rowClipboard.get(editor).reverse();
94 validateRowsToPaste(clipboardRows, table);
95 for (const row of clipboardRows) {
96 lastRow.insertAfter(row);
101 const columnClipboard: NodeClipboard<CustomTableCellNode>[] = [];
103 function setColumnClipboard(columns: CustomTableCellNode[][]): void {
104 const newClipboards = columns.map(cells => {
105 const clipboard = new NodeClipboard<CustomTableCellNode>();
106 clipboard.set(...cells);
110 columnClipboard.splice(0, columnClipboard.length, ...newClipboards);
113 type TableRange = {from: number, to: number};
115 export function isColumnClipboardEmpty(): boolean {
116 return columnClipboard.length === 0;
119 function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|null {
120 if ($isTableSelection(selection)) {
121 const shape = selection.getShape()
122 return {from: shape.fromX, to: shape.toX};
125 const cell = $getNodeFromSelection(selection, $isCustomTableCellNode);
126 const table = $getTableFromSelection(selection);
127 if (!$isCustomTableCellNode(cell) || !table) {
131 const map = new TableMap(table);
132 const range = map.getRangeForCell(cell);
137 return {from: range.fromX, to: range.toX};
140 function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTableNode): CustomTableCellNode[][] {
141 const map = new TableMap(table);
143 for (let x = range.from; x <= range.to; x++) {
144 const cells = map.getCellsInColumn(x);
151 function validateColumnsToCopy(columns: CustomTableCellNode[][]): void {
152 let commonColSize: number|null = null;
154 for (const cells of columns) {
156 for (const cell of cells) {
157 colSize += cell.getRowSpan() || 1;
158 if (cell.getColSpan() > 1) {
159 throw Error('Cannot copy columns with merged cells');
163 if (commonColSize === null) {
164 commonColSize = colSize;
165 } else if (commonColSize !== colSize) {
166 throw Error('Cannot copy columns with inconsistent sizes');
171 export function $cutSelectedColumnsToClipboard(): void {
172 const selection = $getSelection();
173 const range = $getSelectionColumnRange(selection);
174 const table = $getTableFromSelection(selection);
175 if (!range || !table) {
179 const colWidths = table.getColWidths();
180 const columns = $getTableColumnCellsFromSelection(range, table);
181 validateColumnsToCopy(columns);
182 setColumnClipboard(columns);
183 for (const cells of columns) {
184 for (const cell of cells) {
189 const newWidths = [...colWidths].splice(range.from, (range.to - range.from) + 1);
190 table.setColWidths(newWidths);
193 export function $copySelectedColumnsToClipboard(): void {
194 const selection = $getSelection();
195 const range = $getSelectionColumnRange(selection);
196 const table = $getTableFromSelection(selection);
197 if (!range || !table) {
201 const columns = $getTableColumnCellsFromSelection(range, table);
202 validateColumnsToCopy(columns);
203 setColumnClipboard(columns);
206 function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: CustomTableNode) {
207 const tableRowCount = (new TableMap(targetTable)).rowCount;
208 for (const cells of columns) {
210 for (const cell of cells) {
211 colSize += cell.getRowSpan() || 1;
214 if (colSize > tableRowCount) {
215 throw Error('Cannot paste columns that are taller than target table');
218 while (colSize < tableRowCount) {
219 cells.push($createCustomTableCellNode());
225 function $pasteClipboardColumns(editor: LexicalEditor, isBefore: boolean): void {
226 const selection = $getSelection();
227 const table = $getTableFromSelection(selection);
228 const cells = $getTableCellsFromSelection(selection);
229 const referenceCell = cells[isBefore ? 0 : cells.length - 1];
230 if (!table || !referenceCell) {
234 const clipboardCols = columnClipboard.map(cb => cb.get(editor));
236 clipboardCols.reverse();
239 validateColumnsToPaste(clipboardCols, table);
240 const map = new TableMap(table);
241 const cellRange = map.getRangeForCell(referenceCell);
246 const colIndex = isBefore ? cellRange.fromX : cellRange.toX;
247 const colWidths = table.getColWidths();
249 for (let y = 0; y < map.rowCount; y++) {
250 const relCell = map.getCellAtPosition(colIndex, y);
251 for (const cells of clipboardCols) {
252 const newCell = cells[y];
254 relCell.insertBefore(newCell);
256 relCell.insertAfter(newCell);
261 const refWidth = colWidths[colIndex];
262 const addedWidths = clipboardCols.map(_ => refWidth);
263 colWidths.splice(isBefore ? colIndex : colIndex + 1, 0, ...addedWidths);
266 export function $pasteClipboardColumnsBefore(editor: LexicalEditor): void {
267 $pasteClipboardColumns(editor, true);
270 export function $pasteClipboardColumnsAfter(editor: LexicalEditor): void {
271 $pasteClipboardColumns(editor, false);