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