]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/utils/tables.ts
Opensearch: Fixed XML declaration when php short tags enabled
[bookstack] / resources / js / wysiwyg / utils / tables.ts
1 import {BaseSelection, LexicalEditor} from "lexical";
2 import {
3     $isTableCellNode,
4     $isTableNode,
5     $isTableRowNode,
6     $isTableSelection, TableCellNode, TableNode,
7     TableRowNode,
8     TableSelection,
9 } from "@lexical/table";
10 import {$getParentOfType} from "./nodes";
11 import {$getNodeFromSelection} from "./selection";
12 import {el, formatSizeValue} from "./dom";
13 import {TableMap} from "./table-map";
14
15 function $getTableFromCell(cell: TableCellNode): TableNode|null {
16     return $getParentOfType(cell, $isTableNode) as TableNode|null;
17 }
18
19 export function getTableColumnWidths(table: HTMLTableElement): string[] {
20     const maxColRow = getMaxColRowFromTable(table);
21
22     const colGroup = table.querySelector('colgroup');
23     let widths: string[] = [];
24     if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
25         widths = extractWidthsFromRow(colGroup);
26     }
27     if (widths.filter(Boolean).length === 0 && maxColRow) {
28         widths = extractWidthsFromRow(maxColRow);
29     }
30
31     return widths;
32 }
33
34 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null {
35     const rows = table.querySelectorAll('tr');
36     let maxColCount: number = 0;
37     let maxColRow: HTMLTableRowElement | null = null;
38
39     for (const row of rows) {
40         if (row.childElementCount > maxColCount) {
41             maxColRow = row;
42             maxColCount = row.childElementCount;
43         }
44     }
45
46     return maxColRow;
47 }
48
49 function extractWidthsFromRow(row: HTMLTableRowElement | HTMLTableColElement) {
50     return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
51 }
52
53 function extractWidthFromElement(element: HTMLElement): string {
54     let width = element.style.width || element.getAttribute('width');
55     if (width && !Number.isNaN(Number(width))) {
56         width = width + 'px';
57     }
58
59     return width || '';
60 }
61
62 export function $setTableColumnWidth(node: TableNode, columnIndex: number, width: number|string): void {
63     const rows = node.getChildren() as TableRowNode[];
64     let maxCols = 0;
65     for (const row of rows) {
66         const cellCount = row.getChildren().length;
67         if (cellCount > maxCols) {
68             maxCols = cellCount;
69         }
70     }
71
72     let colWidths = node.getColWidths();
73     if (colWidths.length === 0 || colWidths.length < maxCols) {
74         colWidths = Array(maxCols).fill('');
75     }
76
77     if (columnIndex + 1 > colWidths.length) {
78         console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
79     }
80
81     colWidths[columnIndex] = formatSizeValue(width);
82     node.setColWidths(colWidths);
83 }
84
85 export function $getTableColumnWidth(editor: LexicalEditor, node: TableNode, columnIndex: number): number {
86     const colWidths = node.getColWidths();
87     if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
88         return Number(colWidths[columnIndex].replace('px', ''));
89     }
90
91     // Otherwise, get from table element
92     const table = editor.getElementByKey(node.__key) as HTMLTableElement | null;
93     if (table) {
94         const maxColRow = getMaxColRowFromTable(table);
95         if (maxColRow && maxColRow.children.length > columnIndex) {
96             const cell = maxColRow.children[columnIndex];
97             return cell.clientWidth;
98         }
99     }
100
101     return 0;
102 }
103
104 function $getCellColumnIndex(node: TableCellNode): number {
105     const row = node.getParent();
106     if (!$isTableRowNode(row)) {
107         return -1;
108     }
109
110     let index = 0;
111     const cells = row.getChildren<TableCellNode>();
112     for (const cell of cells) {
113         let colSpan = cell.getColSpan() || 1;
114         index += colSpan;
115         if (cell.getKey() === node.getKey()) {
116             break;
117         }
118     }
119
120     return index - 1;
121 }
122
123 export function $setTableCellColumnWidth(cell: TableCellNode, width: string): void {
124     const table = $getTableFromCell(cell)
125     const index = $getCellColumnIndex(cell);
126
127     if (table && index >= 0) {
128         $setTableColumnWidth(table, index, width);
129     }
130 }
131
132 export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellNode): string {
133     const table = $getTableFromCell(cell)
134     const index = $getCellColumnIndex(cell);
135     if (!table) {
136         return '';
137     }
138
139     const widths = table.getColWidths();
140     return (widths.length > index) ? widths[index] : '';
141 }
142
143 export function buildColgroupFromTableWidths(colWidths: string[]): HTMLElement|null {
144     if (colWidths.length === 0) {
145         return null
146     }
147
148     const colgroup = el('colgroup');
149     for (const width of colWidths) {
150         const col = el('col');
151         if (width) {
152             col.style.width = width;
153         }
154         colgroup.append(col);
155     }
156
157     return colgroup;
158 }
159
160 export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[]  {
161     if ($isTableSelection(selection)) {
162         const nodes = selection.getNodes();
163         return nodes.filter(n => $isTableCellNode(n));
164     }
165
166     const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode;
167     return cell ? [cell] : [];
168 }
169
170 export function $mergeTableCellsInSelection(selection: TableSelection): void {
171     const selectionShape = selection.getShape();
172     const cells = $getTableCellsFromSelection(selection);
173     if (cells.length === 0) {
174         return;
175     }
176
177     const table = $getTableFromCell(cells[0]);
178     if (!table) {
179         return;
180     }
181
182     const tableMap = new TableMap(table);
183     const headCell = tableMap.getCellAtPosition(selectionShape.toX, selectionShape.toY);
184     if (!headCell) {
185         return;
186     }
187
188     // We have to adjust the shape since it won't take into account spans for the head corner position.
189     const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
190     const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
191
192     const mergeCells = tableMap.getCellsInRange({
193         fromX: selectionShape.fromX,
194         fromY: selectionShape.fromY,
195         toX: fixedToX,
196         toY: fixedToY,
197     });
198
199     if (mergeCells.length === 0) {
200         return;
201     }
202
203     const firstCell = mergeCells[0];
204     const newWidth = Math.abs(selectionShape.fromX - fixedToX) + 1;
205     const newHeight = Math.abs(selectionShape.fromY - fixedToY) + 1;
206
207     for (let i = 1; i < mergeCells.length; i++) {
208         const mergeCell = mergeCells[i];
209         firstCell.append(...mergeCell.getChildren());
210         mergeCell.remove();
211     }
212
213     firstCell.setColSpan(newWidth);
214     firstCell.setRowSpan(newHeight);
215 }
216
217 export function $getTableRowsFromSelection(selection: BaseSelection|null): TableRowNode[] {
218     const cells = $getTableCellsFromSelection(selection);
219     const rowsByKey: Record<string, TableRowNode> = {};
220     for (const cell of cells) {
221         const row = cell.getParent();
222         if ($isTableRowNode(row)) {
223             rowsByKey[row.getKey()] = row;
224         }
225     }
226
227     return Object.values(rowsByKey);
228 }
229
230 export function $getTableFromSelection(selection: BaseSelection|null): TableNode|null {
231     const cells = $getTableCellsFromSelection(selection);
232     if (cells.length === 0) {
233         return null;
234     }
235
236     const table = $getParentOfType(cells[0], $isTableNode);
237     if ($isTableNode(table)) {
238         return table;
239     }
240
241     return null;
242 }
243
244 export function $clearTableSizes(table: TableNode): void {
245     table.setColWidths([]);
246
247     // TODO - Extra form things once table properties and extra things
248     //   are supported
249
250     for (const row of table.getChildren()) {
251         if (!$isTableRowNode(row)) {
252             continue;
253         }
254
255         const rowStyles = row.getStyles();
256         rowStyles.delete('height');
257         rowStyles.delete('width');
258         row.setStyles(rowStyles);
259
260         const cells = row.getChildren().filter(c => $isTableCellNode(c));
261         for (const cell of cells) {
262             const cellStyles = cell.getStyles();
263             cellStyles.delete('height');
264             cellStyles.delete('width');
265             cell.setStyles(cellStyles);
266             cell.clearWidth();
267         }
268     }
269 }
270
271 export function $clearTableFormatting(table: TableNode): void {
272     table.setColWidths([]);
273     table.setStyles(new Map);
274
275     for (const row of table.getChildren()) {
276         if (!$isTableRowNode(row)) {
277             continue;
278         }
279
280         row.setStyles(new Map);
281
282         const cells = row.getChildren().filter(c => $isTableCellNode(c));
283         for (const cell of cells) {
284             cell.setStyles(new Map);
285             cell.clearWidth();
286         }
287     }
288 }
289
290 /**
291  * Perform the given callback for each cell in the given table.
292  * Returning false from the callback stops the function early.
293  */
294 export function $forEachTableCell(table: TableNode, callback: (c: TableCellNode) => void|false): void {
295     outer: for (const row of table.getChildren()) {
296         if (!$isTableRowNode(row)) {
297             continue;
298         }
299         const cells = row.getChildren();
300         for (const cell of cells) {
301             if (!$isTableCellNode(cell)) {
302                 return;
303             }
304             const result = callback(cell);
305             if (result === false) {
306                 break outer;
307             }
308         }
309     }
310 }
311
312 export function $getCellPaddingForTable(table: TableNode): string {
313     let padding: string|null = null;
314
315     $forEachTableCell(table, (cell: TableCellNode) => {
316         const cellPadding = cell.getStyles().get('padding') || ''
317         if (padding === null) {
318             padding = cellPadding;
319         }
320
321         if (cellPadding !== padding) {
322             padding = null;
323             return false;
324         }
325     });
326
327     return padding || '';
328 }
329
330
331
332
333
334
335
336