]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-table-cell-node.ts
Lexical: Added table cell node import logic
[bookstack] / resources / js / wysiwyg / nodes / custom-table-cell-node.ts
1 import {
2     $createParagraphNode,
3     $isElementNode,
4     $isLineBreakNode,
5     $isTextNode,
6     DOMConversionMap,
7     DOMConversionOutput,
8     DOMExportOutput,
9     EditorConfig,
10     LexicalEditor,
11     LexicalNode,
12     Spread
13 } from "lexical";
14
15 import {
16     $createTableCellNode,
17     $isTableCellNode,
18     SerializedTableCellNode,
19     TableCellHeaderStates,
20     TableCellNode
21 } from "@lexical/table";
22 import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
23
24 export type SerializedCustomTableCellNode = Spread<{
25     styles: Record<string, string>,
26 }, SerializedTableCellNode>
27
28 export class CustomTableCellNode extends TableCellNode {
29     __styles: Map<string, string> = new Map;
30
31     static getType(): string {
32         return 'custom-table-cell';
33     }
34
35     static clone(node: CustomTableCellNode): CustomTableCellNode {
36         const cellNode = new CustomTableCellNode(
37             node.__headerState,
38             node.__colSpan,
39             node.__width,
40             node.__key,
41         );
42         cellNode.__rowSpan = node.__rowSpan;
43         cellNode.__styles = new Map(node.__styles);
44         return cellNode;
45     }
46
47     getStyles(): Map<string, string> {
48         const self = this.getLatest();
49         return new Map(self.__styles);
50     }
51
52     setStyles(styles: Map<string, string>): void {
53         const self = this.getWritable();
54         self.__styles = new Map(styles);
55     }
56
57     updateTag(tag: string): void {
58         const isHeader = tag.toLowerCase() === 'th';
59         const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
60         const self = this.getWritable();
61         self.__headerState = state;
62     }
63
64     createDOM(config: EditorConfig): HTMLElement {
65         const element = super.createDOM(config);
66
67         for (const [name, value] of this.__styles.entries()) {
68             element.style.setProperty(name, value);
69         }
70
71         return element;
72     }
73
74     updateDOM(prevNode: CustomTableCellNode): boolean {
75         return super.updateDOM(prevNode)
76             || this.__styles !== prevNode.__styles;
77     }
78
79     static importDOM(): DOMConversionMap | null {
80         return {
81             td: (node: Node) => ({
82                 conversion: $convertCustomTableCellNodeElement,
83                 priority: 0,
84             }),
85             th: (node: Node) => ({
86                 conversion: $convertCustomTableCellNodeElement,
87                 priority: 0,
88             }),
89         };
90     }
91
92     exportDOM(editor: LexicalEditor): DOMExportOutput {
93         const element = this.createDOM(editor._config);
94         return {
95             element
96         };
97     }
98
99     static importJSON(serializedNode: SerializedCustomTableCellNode): CustomTableCellNode {
100         const node = $createCustomTableCellNode(
101             serializedNode.headerState,
102             serializedNode.colSpan,
103             serializedNode.width,
104         );
105
106         node.setStyles(new Map<string, string>(Object.entries(serializedNode.styles)));
107
108         return node;
109     }
110
111     exportJSON(): SerializedCustomTableCellNode {
112         return {
113             ...super.exportJSON(),
114             type: 'custom-table-cell',
115             styles: Object.fromEntries(this.__styles),
116         };
117     }
118 }
119
120 function $convertCustomTableCellNodeElement(domNode: Node): DOMConversionOutput {
121     const output =  $convertTableCellNodeElement(domNode);
122
123     if (domNode instanceof HTMLElement && output.node instanceof CustomTableCellNode) {
124         const styleMap = new Map<string, string>();
125         const styleNames = Array.from(domNode.style);
126         for (const style of styleNames) {
127             styleMap.set(style, domNode.style.getPropertyValue(style));
128         }
129         output.node.setStyles(styleMap);
130     }
131
132     return output;
133 }
134
135 /**
136  * Function taken from:
137  * https://github.com/facebook/lexical/blob/e1881a6e409e1541c10dd0b5378f3a38c9dc8c9e/packages/lexical-table/src/LexicalTableCellNode.ts#L289
138  * Copyright (c) Meta Platforms, Inc. and affiliates.
139  * MIT LICENSE
140  * Modified since copy.
141  */
142 export function $convertTableCellNodeElement(
143     domNode: Node,
144 ): DOMConversionOutput {
145     const domNode_ = domNode as HTMLTableCellElement;
146     const nodeName = domNode.nodeName.toLowerCase();
147
148     let width: number | undefined = undefined;
149
150
151     const PIXEL_VALUE_REG_EXP = /^(\d+(?:\.\d+)?)px$/;
152     if (PIXEL_VALUE_REG_EXP.test(domNode_.style.width)) {
153         width = parseFloat(domNode_.style.width);
154     }
155
156     const tableCellNode = $createTableCellNode(
157         nodeName === 'th'
158             ? TableCellHeaderStates.ROW
159             : TableCellHeaderStates.NO_STATUS,
160         domNode_.colSpan,
161         width,
162     );
163
164     tableCellNode.__rowSpan = domNode_.rowSpan;
165
166     const style = domNode_.style;
167     const textDecoration = style.textDecoration.split(' ');
168     const hasBoldFontWeight =
169         style.fontWeight === '700' || style.fontWeight === 'bold';
170     const hasLinethroughTextDecoration = textDecoration.includes('line-through');
171     const hasItalicFontStyle = style.fontStyle === 'italic';
172     const hasUnderlineTextDecoration = textDecoration.includes('underline');
173     return {
174         after: (childLexicalNodes) => {
175             if (childLexicalNodes.length === 0) {
176                 childLexicalNodes.push($createParagraphNode());
177             }
178             return childLexicalNodes;
179         },
180         forChild: (lexicalNode, parentLexicalNode) => {
181             if ($isTableCellNode(parentLexicalNode) && !$isElementNode(lexicalNode)) {
182                 const paragraphNode = $createParagraphNode();
183                 if (
184                     $isLineBreakNode(lexicalNode) &&
185                     lexicalNode.getTextContent() === '\n'
186                 ) {
187                     return null;
188                 }
189                 if ($isTextNode(lexicalNode)) {
190                     if (hasBoldFontWeight) {
191                         lexicalNode.toggleFormat('bold');
192                     }
193                     if (hasLinethroughTextDecoration) {
194                         lexicalNode.toggleFormat('strikethrough');
195                     }
196                     if (hasItalicFontStyle) {
197                         lexicalNode.toggleFormat('italic');
198                     }
199                     if (hasUnderlineTextDecoration) {
200                         lexicalNode.toggleFormat('underline');
201                     }
202                 }
203                 paragraphNode.append(lexicalNode);
204                 return paragraphNode;
205             }
206
207             return lexicalNode;
208         },
209         node: tableCellNode,
210     };
211 }
212
213
214 export function $createCustomTableCellNode(
215     headerState: TableCellHeaderState,
216     colSpan = 1,
217     width?: number,
218 ): CustomTableCellNode {
219     return new CustomTableCellNode(headerState, colSpan, width);
220 }
221
222 export function $isCustomTableCellNode(node: LexicalNode | null | undefined): node is CustomTableCellNode {
223     return node instanceof CustomTableCellNode;
224 }