]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/table-map.ts
Lexical: Added table column cut/copy/paste support
[bookstack] / resources / js / wysiwyg / utils / table-map.ts
1 import {CustomTableNode} from "../nodes/custom-table";
2 import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
3 import {$isTableRowNode} from "@lexical/table";
4
5 export type CellRange = {
6     fromX: number;
7     fromY: number;
8     toX: number;
9     toY: number;
10 }
11
12 export class TableMap {
13
14     rowCount: number = 0;
15     columnCount: number = 0;
16
17     // Represents an array (rows*columns in length) of cell nodes from top-left to
18     // bottom right. Cells may repeat where merged and covering multiple spaces.
19     cells: CustomTableCellNode[] = [];
20
21     constructor(table: CustomTableNode) {
22         this.buildCellMap(table);
23     }
24
25     protected buildCellMap(table: CustomTableNode) {
26         const rowsAndCells: CustomTableCellNode[][] = [];
27         const setCell = (x: number, y: number, cell: CustomTableCellNode) => {
28             if (typeof rowsAndCells[y] === 'undefined') {
29                 rowsAndCells[y] = [];
30             }
31
32             rowsAndCells[y][x] = cell;
33         };
34         const cellFilled = (x: number, y: number): boolean => !!(rowsAndCells[y] && rowsAndCells[y][x]);
35
36         const rowNodes = table.getChildren().filter(r => $isTableRowNode(r));
37         for (let rowIndex = 0; rowIndex < rowNodes.length; rowIndex++) {
38             const rowNode = rowNodes[rowIndex];
39             const cellNodes = rowNode.getChildren().filter(c => $isCustomTableCellNode(c));
40             let targetColIndex: number = 0;
41             for (let cellIndex = 0; cellIndex < cellNodes.length; cellIndex++) {
42                 const cellNode = cellNodes[cellIndex];
43                 const colspan = cellNode.getColSpan() || 1;
44                 const rowSpan = cellNode.getRowSpan() || 1;
45                 for (let x = targetColIndex; x < targetColIndex + colspan; x++) {
46                     for (let y = rowIndex; y < rowIndex + rowSpan; y++) {
47                         while (cellFilled(x, y)) {
48                             targetColIndex += 1;
49                             x += 1;
50                         }
51
52                         setCell(x, y, cellNode);
53                     }
54                 }
55                 targetColIndex += colspan;
56             }
57         }
58
59         this.rowCount = rowsAndCells.length;
60         this.columnCount = Math.max(...rowsAndCells.map(r => r.length));
61
62         const cells = [];
63         let lastCell: CustomTableCellNode = rowsAndCells[0][0];
64         for (let y = 0; y < this.rowCount; y++) {
65             for (let x = 0; x < this.columnCount; x++) {
66                 if (!rowsAndCells[y] || !rowsAndCells[y][x]) {
67                     cells.push(lastCell);
68                 } else {
69                     cells.push(rowsAndCells[y][x]);
70                     lastCell = rowsAndCells[y][x];
71                 }
72             }
73         }
74
75         this.cells = cells;
76     }
77
78     public getCellAtPosition(x: number, y: number): CustomTableCellNode {
79         const position = (y * this.columnCount) + x;
80         if (position >= this.cells.length) {
81             throw new Error(`TableMap Error: Attempted to get cell ${position+1} of ${this.cells.length}`);
82         }
83
84         return this.cells[position];
85     }
86
87     public getCellsInRange(range: CellRange): CustomTableCellNode[] {
88         const minX = Math.max(Math.min(range.fromX, range.toX), 0);
89         const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
90         const minY = Math.max(Math.min(range.fromY, range.toY), 0);
91         const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
92
93         const cells = new Set<CustomTableCellNode>();
94
95         for (let y = minY; y <= maxY; y++) {
96             for (let x = minX; x <= maxX; x++) {
97                 cells.add(this.getCellAtPosition(x, y));
98             }
99         }
100
101         return [...cells.values()];
102     }
103
104     public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
105         return this.getCellsInRange({
106             fromX: columnIndex,
107             toX: columnIndex,
108             fromY: 0,
109             toY: this.rowCount - 1,
110         });
111     }
112
113     public getRangeForCell(cell: CustomTableCellNode): CellRange|null {
114         let range: CellRange|null = null;
115         const cellKey = cell.getKey();
116
117         for (let y = 0; y < this.rowCount; y++) {
118             for (let x = 0; x < this.columnCount; x++) {
119                 const index = (y * this.columnCount) + x;
120                 const lCell = this.cells[index];
121                 if (lCell.getKey() === cellKey) {
122                     if (range === null) {
123                         range = {fromX: x, toX: x, fromY: y, toY: y};
124                     } else {
125                         range.fromX = Math.min(range.fromX, x);
126                         range.toX = Math.max(range.toX, x);
127                         range.fromY = Math.min(range.fromY, y);
128                         range.toY = Math.max(range.toY, y);
129                     }
130                 }
131             }
132         }
133
134         return range;
135     }
136 }