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