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