]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
respective book and chapter structure added.
[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           return;
88         }
89
90         const newElement = tableElement.cloneNode() as ParentNode;
91         const tBody = document.createElement('tbody');
92
93         if (isHTMLElement(tableElement)) {
94           for (const child of Array.from(tableElement.children)) {
95             if (child.nodeName === 'TR') {
96               tBody.append(child);
97             } else {
98               newElement.append(child);
99             }
100           }
101         }
102
103         newElement.append(tBody);
104
105         return newElement as HTMLElement;
106       },
107     };
108   }
109
110   canBeEmpty(): false {
111     return false;
112   }
113
114   isShadowRoot(): boolean {
115     return true;
116   }
117
118   getCordsFromCellNode(
119     tableCellNode: TableCellNode,
120     table: TableDOMTable,
121   ): {x: number; y: number} {
122     const {rows, domRows} = table;
123
124     for (let y = 0; y < rows; y++) {
125       const row = domRows[y];
126
127       if (row == null) {
128         continue;
129       }
130
131       const x = row.findIndex((cell) => {
132         if (!cell) {
133           return;
134         }
135         const {elem} = cell;
136         const cellNode = $getNearestNodeFromDOMNode(elem);
137         return cellNode === tableCellNode;
138       });
139
140       if (x !== -1) {
141         return {x, y};
142       }
143     }
144
145     throw new Error('Cell not found in table.');
146   }
147
148   getDOMCellFromCords(
149     x: number,
150     y: number,
151     table: TableDOMTable,
152   ): null | TableDOMCell {
153     const {domRows} = table;
154
155     const row = domRows[y];
156
157     if (row == null) {
158       return null;
159     }
160
161     const index = x < row.length ? x : row.length - 1;
162
163     const cell = row[index];
164
165     if (cell == null) {
166       return null;
167     }
168
169     return cell;
170   }
171
172   getDOMCellFromCordsOrThrow(
173     x: number,
174     y: number,
175     table: TableDOMTable,
176   ): TableDOMCell {
177     const cell = this.getDOMCellFromCords(x, y, table);
178
179     if (!cell) {
180       throw new Error('Cell not found at cords.');
181     }
182
183     return cell;
184   }
185
186   getCellNodeFromCords(
187     x: number,
188     y: number,
189     table: TableDOMTable,
190   ): null | TableCellNode {
191     const cell = this.getDOMCellFromCords(x, y, table);
192
193     if (cell == null) {
194       return null;
195     }
196
197     const node = $getNearestNodeFromDOMNode(cell.elem);
198
199     if ($isTableCellNode(node)) {
200       return node;
201     }
202
203     return null;
204   }
205
206   getCellNodeFromCordsOrThrow(
207     x: number,
208     y: number,
209     table: TableDOMTable,
210   ): TableCellNode {
211     const node = this.getCellNodeFromCords(x, y, table);
212
213     if (!node) {
214       throw new Error('Node at cords not TableCellNode.');
215     }
216
217     return node;
218   }
219
220   canSelectBefore(): true {
221     return true;
222   }
223
224   canIndent(): false {
225     return false;
226   }
227 }
228
229 export function $getElementForTableNode(
230   editor: LexicalEditor,
231   tableNode: TableNode,
232 ): TableDOMTable {
233   const tableElement = editor.getElementByKey(tableNode.getKey());
234
235   if (tableElement == null) {
236     throw new Error('Table Element Not Found');
237   }
238
239   return getTable(tableElement);
240 }
241
242 export function $convertTableElement(_domNode: Node): DOMConversionOutput {
243   return {node: $createTableNode()};
244 }
245
246 export function $createTableNode(): TableNode {
247   return $applyNodeReplacement(new TableNode());
248 }
249
250 export function $isTableNode(
251   node: LexicalNode | null | undefined,
252 ): node is TableNode {
253   return node instanceof TableNode;
254 }