]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
Lexical: Updated toolbar & text node exporting
[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 type {
11   DOMConversionMap,
12   DOMConversionOutput,
13   DOMExportOutput,
14   EditorConfig,
15   LexicalEditor,
16   LexicalNode,
17   NodeKey,
18   SerializedElementNode,
19 } from 'lexical';
20
21 import {addClassNamesToElement, isHTMLElement} from '@lexical/utils';
22 import {
23   $applyNodeReplacement,
24   $getNearestNodeFromDOMNode,
25   ElementNode,
26 } from 'lexical';
27
28 import {$isTableCellNode} from './LexicalTableCellNode';
29 import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
30 import {$isTableRowNode, TableRowNode} from './LexicalTableRowNode';
31 import {getTable} from './LexicalTableSelectionHelpers';
32
33 export type SerializedTableNode = SerializedElementNode;
34
35 /** @noInheritDoc */
36 export class TableNode extends ElementNode {
37   static getType(): string {
38     return 'table';
39   }
40
41   static clone(node: TableNode): TableNode {
42     return new TableNode(node.__key);
43   }
44
45   static importDOM(): DOMConversionMap | null {
46     return {
47       table: (_node: Node) => ({
48         conversion: $convertTableElement,
49         priority: 1,
50       }),
51     };
52   }
53
54   static importJSON(_serializedNode: SerializedTableNode): TableNode {
55     return $createTableNode();
56   }
57
58   constructor(key?: NodeKey) {
59     super(key);
60   }
61
62   exportJSON(): SerializedElementNode {
63     return {
64       ...super.exportJSON(),
65       type: 'table',
66       version: 1,
67     };
68   }
69
70   createDOM(config: EditorConfig, editor?: LexicalEditor): HTMLElement {
71     const tableElement = document.createElement('table');
72
73     addClassNamesToElement(tableElement, config.theme.table);
74
75     return tableElement;
76   }
77
78   updateDOM(): boolean {
79     return false;
80   }
81
82   exportDOM(editor: LexicalEditor): DOMExportOutput {
83     return {
84       ...super.exportDOM(editor),
85       after: (tableElement) => {
86         if (tableElement) {
87           const newElement = tableElement.cloneNode() as ParentNode;
88           const colGroup = document.createElement('colgroup');
89           const tBody = document.createElement('tbody');
90           if (isHTMLElement(tableElement)) {
91             tBody.append(...tableElement.children);
92           }
93           const firstRow = this.getFirstChildOrThrow<TableRowNode>();
94
95           if (!$isTableRowNode(firstRow)) {
96             throw new Error('Expected to find row node.');
97           }
98
99           const colCount = firstRow.getChildrenSize();
100
101           for (let i = 0; i < colCount; i++) {
102             const col = document.createElement('col');
103             colGroup.append(col);
104           }
105
106           newElement.replaceChildren(colGroup, tBody);
107
108           return newElement as HTMLElement;
109         }
110       },
111     };
112   }
113
114   canBeEmpty(): false {
115     return false;
116   }
117
118   isShadowRoot(): boolean {
119     return true;
120   }
121
122   getCordsFromCellNode(
123     tableCellNode: TableCellNode,
124     table: TableDOMTable,
125   ): {x: number; y: number} {
126     const {rows, domRows} = table;
127
128     for (let y = 0; y < rows; y++) {
129       const row = domRows[y];
130
131       if (row == null) {
132         continue;
133       }
134
135       const x = row.findIndex((cell) => {
136         if (!cell) {
137           return;
138         }
139         const {elem} = cell;
140         const cellNode = $getNearestNodeFromDOMNode(elem);
141         return cellNode === tableCellNode;
142       });
143
144       if (x !== -1) {
145         return {x, y};
146       }
147     }
148
149     throw new Error('Cell not found in table.');
150   }
151
152   getDOMCellFromCords(
153     x: number,
154     y: number,
155     table: TableDOMTable,
156   ): null | TableDOMCell {
157     const {domRows} = table;
158
159     const row = domRows[y];
160
161     if (row == null) {
162       return null;
163     }
164
165     const index = x < row.length ? x : row.length - 1;
166
167     const cell = row[index];
168
169     if (cell == null) {
170       return null;
171     }
172
173     return cell;
174   }
175
176   getDOMCellFromCordsOrThrow(
177     x: number,
178     y: number,
179     table: TableDOMTable,
180   ): TableDOMCell {
181     const cell = this.getDOMCellFromCords(x, y, table);
182
183     if (!cell) {
184       throw new Error('Cell not found at cords.');
185     }
186
187     return cell;
188   }
189
190   getCellNodeFromCords(
191     x: number,
192     y: number,
193     table: TableDOMTable,
194   ): null | TableCellNode {
195     const cell = this.getDOMCellFromCords(x, y, table);
196
197     if (cell == null) {
198       return null;
199     }
200
201     const node = $getNearestNodeFromDOMNode(cell.elem);
202
203     if ($isTableCellNode(node)) {
204       return node;
205     }
206
207     return null;
208   }
209
210   getCellNodeFromCordsOrThrow(
211     x: number,
212     y: number,
213     table: TableDOMTable,
214   ): TableCellNode {
215     const node = this.getCellNodeFromCords(x, y, table);
216
217     if (!node) {
218       throw new Error('Node at cords not TableCellNode.');
219     }
220
221     return node;
222   }
223
224   canSelectBefore(): true {
225     return true;
226   }
227
228   canIndent(): false {
229     return false;
230   }
231 }
232
233 export function $getElementForTableNode(
234   editor: LexicalEditor,
235   tableNode: TableNode,
236 ): TableDOMTable {
237   const tableElement = editor.getElementByKey(tableNode.getKey());
238
239   if (tableElement == null) {
240     throw new Error('Table Element Not Found');
241   }
242
243   return getTable(tableElement);
244 }
245
246 export function $convertTableElement(_domNode: Node): DOMConversionOutput {
247   return {node: $createTableNode()};
248 }
249
250 export function $createTableNode(): TableNode {
251   return $applyNodeReplacement(new TableNode());
252 }
253
254 export function $isTableNode(
255   node: LexicalNode | null | undefined,
256 ): node is TableNode {
257   return node instanceof TableNode;
258 }