]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/tables.ts
Lexical: Added merge cell logic
[bookstack] / resources / js / wysiwyg / utils / tables.ts
1 import {BaseSelection, LexicalEditor} from "lexical";
2 import {$isTableRowNode, $isTableSelection, TableRowNode, TableSelection, TableSelectionShape} from "@lexical/table";
3 import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table";
4 import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node";
5 import {$getParentOfType} from "./nodes";
6 import {$getNodeFromSelection} from "./selection";
7 import {formatSizeValue} from "./dom";
8 import {TableMap} from "./table-map";
9
10 function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null {
11     return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null;
12 }
13
14 export function getTableColumnWidths(table: HTMLTableElement): string[] {
15     const maxColRow = getMaxColRowFromTable(table);
16
17     const colGroup = table.querySelector('colgroup');
18     let widths: string[] = [];
19     if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
20         widths = extractWidthsFromRow(colGroup);
21     }
22     if (widths.filter(Boolean).length === 0 && maxColRow) {
23         widths = extractWidthsFromRow(maxColRow);
24     }
25
26     return widths;
27 }
28
29 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
30     const rows = table.querySelectorAll('tr');
31     let maxColCount: number = 0;
32     let maxColRow: HTMLTableRowElement | null = null;
33
34     for (const row of rows) {
35         if (row.childElementCount > maxColCount) {
36             maxColRow = row;
37             maxColCount = row.childElementCount;
38         }
39     }
40
41     return maxColRow;
42 }
43
44 function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
45     return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
46 }
47
48 function extractWidthFromElement(element: HTMLElement): string {
49     let width = element.style.width || element.getAttribute('width');
50     if (width && !Number.isNaN(Number(width))) {
51         width = width + 'px';
52     }
53
54     return width || '';
55 }
56
57 export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): void {
58     const rows = node.getChildren() as TableRowNode[];
59     let maxCols = 0;
60     for (const row of rows) {
61         const cellCount = row.getChildren().length;
62         if (cellCount > maxCols) {
63             maxCols = cellCount;
64         }
65     }
66
67     let colWidths = node.getColWidths();
68     if (colWidths.length === 0 || colWidths.length < maxCols) {
69         colWidths = Array(maxCols).fill('');
70     }
71
72     if (columnIndex + 1 > colWidths.length) {
73         console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
74     }
75
76     colWidths[columnIndex] = formatSizeValue(width);
77     node.setColWidths(colWidths);
78 }
79
80 export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
81     const colWidths = node.getColWidths();
82     if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
83         return Number(colWidths[columnIndex].replace('px', ''));
84     }
85
86     // Otherwise, get from table element
87     const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
88     if (table) {
89         const maxColRow = getMaxColRowFromTable(table);
90         if (maxColRow && maxColRow.children.length > columnIndex) {
91             const cell = maxColRow.children[columnIndex];
92             return cell.clientWidth;
93         }
94     }
95
96     return 0;
97 }
98
99 function $getCellColumnIndex(node: CustomTableCellNode): number {
100     const row = node.getParent();
101     if (!$isTableRowNode(row)) {
102         return -1;
103     }
104
105     let index = 0;
106     const cells = row.getChildren<CustomTableCellNode>();
107     for (const cell of cells) {
108         let colSpan = cell.getColSpan() || 1;
109         index += colSpan;
110         if (cell.getKey() === node.getKey()) {
111             break;
112         }
113     }
114
115     return index - 1;
116 }
117
118 export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void {
119     const table = $getTableFromCell(cell)
120     const index = $getCellColumnIndex(cell);
121
122     if (table && index >= 0) {
123         $setTableColumnWidth(table, index, width);
124     }
125 }
126
127 export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[]  {
128     if ($isTableSelection(selection)) {
129         const nodes = selection.getNodes();
130         return nodes.filter(n => $isCustomTableCellNode(n));
131     }
132
133     const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode;
134     return cell ? [cell] : [];
135 }
136
137 export function $mergeTableCellsInSelection(selection: TableSelection): void {
138     const selectionShape = selection.getShape();
139     const cells = $getTableCellsFromSelection(selection);
140     if (cells.length === 0) {
141         return;
142     }
143
144     const table = $getTableFromCell(cells[0]);
145     if (!table) {
146         return;
147     }
148
149     const tableMap = new TableMap(table);
150     const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
151     if (!headCell) {
152         return;
153     }
154
155     // We have to adjust the shape since it won't take into account spans for the head corner position.
156     const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
157     const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
158
159     const mergeCells = tableMap.getCellsInRange(
160         selectionShape.fromX,
161         selectionShape.fromY,
162         fixedToX,
163         fixedToY,
164     );
165
166     if (mergeCells.length === 0) {
167         return;
168     }
169
170     const firstCell = mergeCells[0];
171     const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
172     const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
173
174     for (let i = 1; i < mergeCells.length; i++) {
175         const mergeCell = mergeCells[i];
176         firstCell.append(...mergeCell.getChildren());
177         mergeCell.remove();
178     }
179
180     firstCell.setColSpan(newWidth);
181     firstCell.setRowSpan(newHeight);
182 }
183
184
185
186
187
188
189
190
191
192
193