]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
Add optional OIDC avatar fetching from the “picture” claim
[bookstack] / resources / js / wysiwyg / lexical / table / LexicalTableNode.ts
1 /**
2  * Copyright (c) Meta Platforms, Inc. and affiliates.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE file in the root directory of this source tree.
6  *
7  */
8
9 import type {TableCellNode} from './LexicalTableCellNode';
10 import {
11   DOMConversionMap,
12   DOMConversionOutput,
13   DOMExportOutput,
14   EditorConfig,
15   LexicalEditor,
16   LexicalNode,
17   NodeKey,
18   Spread,
19 } from 'lexical';
20
21 import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
22 import {
23   $applyNodeReplacement,
24   $getNearestNodeFromDOMNode,
25
26 } from 'lexical';
27
28 import {$isTableCellNode} from './LexicalTableCellNode';
29 import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
30 import {getTable} from './LexicalTableSelectionHelpers';
31 import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
32 import {
33   commonPropertiesDifferent, deserializeCommonBlockNode,
34   setCommonBlockPropsFromElement,
35   updateElementWithCommonBlockProps
36 } from "lexical/nodes/common";
37 import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
38 import {getTableColumnWidths} from "../../utils/tables";
39
40 export type SerializedTableNode = Spread<{
41   colWidths: string[];
42   styles: Record<string, string>,
43 }, SerializedCommonBlockNode>
44
45 /** @noInheritDoc */
46 export class TableNode extends CommonBlockNode {
47   __colWidths: string[] = [];
48   __styles: StyleMap = new Map;
49
50   static getType(): string {
51     return 'table';
52   }
53
54   static clone(node: TableNode): TableNode {
55     const newNode = new TableNode(node.__key);
56     copyCommonBlockProperties(node, newNode);
57     newNode.__colWidths = node.__colWidths;
58     newNode.__styles = new Map(node.__styles);
59     return newNode;
60   }
61
62   static importDOM(): DOMConversionMap | null {
63     return {
64       table: (_node: Node) => ({
65         conversion: $convertTableElement,
66         priority: 1,
67       }),
68     };
69   }
70
71   static importJSON(_serializedNode: SerializedTableNode): TableNode {
72     const node = $createTableNode();
73     deserializeCommonBlockNode(_serializedNode, node);
74     node.setColWidths(_serializedNode.colWidths);
75     node.setStyles(new Map(Object.entries(_serializedNode.styles)));
76     return node;
77   }
78
79   constructor(key?: NodeKey) {
80     super(key);
81   }
82
83   exportJSON(): SerializedTableNode {
84     return {
85       ...super.exportJSON(),
86       type: 'table',
87       version: 1,
88       colWidths: this.__colWidths,
89       styles: Object.fromEntries(this.__styles),
90     };
91   }
92
93   createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {
94     const tableElement = document.createElement('table');
95
96     addClassNamesToElement(tableElement, config.theme.table);
97
98     updateElementWithCommonBlockProps(tableElement, this);
99
100     const colWidths = this.getColWidths();
101     if (colWidths.length > 0) {
102       const colgroup = el('colgroup');
103       for (const width of colWidths) {
104         const col = el('col');
105         if (width) {
106           col.style.width = width;
107         }
108         colgroup.append(col);
109       }
110       tableElement.append(colgroup);
111     }
112
113     for (const [name, value] of this.__styles.entries()) {
114       tableElement.style.setProperty(name, value);
115     }
116
117     return tableElement;
118   }
119
120   updateDOM(_prevNode: TableNode): boolean {
121     return commonPropertiesDifferent(_prevNode, this)
122       || this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
123       || this.__styles.size !== _prevNode.__styles.size
124       || (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':')));
125   }
126
127   exportDOM(editor: LexicalEditor): DOMExportOutput {
128     return {
129       ...super.exportDOM(editor),
130       after: (tableElement) => {
131         if (!tableElement) {
132           return;
133         }
134
135         const newElement = tableElement.cloneNode() as ParentNode;
136         const tBody = document.createElement('tbody');
137
138         if (isHTMLElement(tableElement)) {
139           for (const child of Array.from(tableElement.children)) {
140             if (child.nodeName === 'TR') {
141               tBody.append(child);
142             } else {
143               newElement.append(child);
144             }
145           }
146         }
147
148         newElement.append(tBody);
149
150         return newElement as HTMLElement;
151       },
152     };
153   }
154
155   canBeEmpty(): false {
156     return false;
157   }
158
159   isShadowRoot(): boolean {
160     return true;
161   }
162
163   setColWidths(widths: string[]) {
164     const self = this.getWritable();
165     self.__colWidths = widths;
166   }
167
168   getColWidths(): string[] {
169     const self = this.getLatest();
170     return self.__colWidths;
171   }
172
173   getStyles(): StyleMap {
174     const self = this.getLatest();
175     return new Map(self.__styles);
176   }
177
178   setStyles(styles: StyleMap): void {
179     const self = this.getWritable();
180     self.__styles = new Map(styles);
181   }
182
183   getCordsFromCellNode(
184     tableCellNode: TableCellNode,
185     table: TableDOMTable,
186   ): {x: number; y: number} {
187     const {rows, domRows} = table;
188
189     for (let y = 0; y < rows; y++) {
190       const row = domRows[y];
191
192       if (row == null) {
193         continue;
194       }
195
196       const x = row.findIndex((cell) => {
197         if (!cell) {
198           return;
199         }
200         const {elem} = cell;
201         const cellNode = $getNearestNodeFromDOMNode(elem);
202         return cellNode === tableCellNode;
203       });
204
205       if (x !== -1) {
206         return {x, y};
207       }
208     }
209
210     throw new Error('Cell not found in table.');
211   }
212
213   getDOMCellFromCords(
214     x: number,
215     y: number,
216     table: TableDOMTable,
217   ): null | TableDOMCell {
218     const {domRows} = table;
219
220     const row = domRows[y];
221
222     if (row == null) {
223       return null;
224     }
225
226     const index = x < row.length ? x : row.length - 1;
227
228     const cell = row[index];
229
230     if (cell == null) {
231       return null;
232     }
233
234     return cell;
235   }
236
237   getDOMCellFromCordsOrThrow(
238     x: number,
239     y: number,
240     table: TableDOMTable,
241   ): TableDOMCell {
242     const cell = this.getDOMCellFromCords(x, y, table);
243
244     if (!cell) {
245       throw new Error('Cell not found at cords.');
246     }
247
248     return cell;
249   }
250
251   getCellNodeFromCords(
252     x: number,
253     y: number,
254     table: TableDOMTable,
255   ): null | TableCellNode {
256     const cell = this.getDOMCellFromCords(x, y, table);
257
258     if (cell == null) {
259       return null;
260     }
261
262     const node = $getNearestNodeFromDOMNode(cell.elem);
263
264     if ($isTableCellNode(node)) {
265       return node;
266     }
267
268     return null;
269   }
270
271   getCellNodeFromCordsOrThrow(
272     x: number,
273     y: number,
274     table: TableDOMTable,
275   ): TableCellNode {
276     const node = this.getCellNodeFromCords(x, y, table);
277
278     if (!node) {
279       throw new Error('Node at cords not TableCellNode.');
280     }
281
282     return node;
283   }
284
285   canSelectBefore(): true {
286     return true;
287   }
288
289   canIndent(): false {
290     return false;
291   }
292 }
293
294 export function $getElementForTableNode(
295   editor: LexicalEditor,
296   tableNode: TableNode,
297 ): TableDOMTable {
298   const tableElement = editor.getElementByKey(tableNode.getKey());
299
300   if (tableElement == null) {
301     throw new Error('Table Element Not Found');
302   }
303
304   return getTable(tableElement);
305 }
306
307 export function $convertTableElement(element: HTMLElement): DOMConversionOutput {
308   const node = $createTableNode();
309   setCommonBlockPropsFromElement(element, node);
310
311   const colWidths = getTableColumnWidths(element as HTMLTableElement);
312   node.setColWidths(colWidths);
313   node.setStyles(extractStyleMapFromElement(element));
314
315   return {node};
316 }
317
318 export function $createTableNode(): TableNode {
319   return $applyNodeReplacement(new TableNode());
320 }
321
322 export function $isTableNode(
323   node: LexicalNode | null | undefined,
324 ): node is TableNode {
325   return node instanceof TableNode;
326 }