]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-table.ts
1107f0a906b5faad7bb28a02909c41935777a2c7
[bookstack] / resources / js / wysiwyg / nodes / custom-table.ts
1 import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table";
2 import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalEditor, LexicalNode, Spread} from "lexical";
3 import {EditorConfig} from "lexical/LexicalEditor";
4 import {el} from "../helpers";
5
6 export type SerializedCustomTableNode = Spread<{
7     id: string;
8     colWidths: string[];
9 }, SerializedTableNode>
10
11 export class CustomTableNode extends TableNode {
12     __id: string = '';
13     __colWidths: string[] = [];
14
15     static getType() {
16         return 'custom-table';
17     }
18
19     setId(id: string) {
20         const self = this.getWritable();
21         self.__id = id;
22     }
23
24     getId(): string {
25         const self = this.getLatest();
26         return self.__id;
27     }
28
29     setColWidths(widths: string[]) {
30         const self = this.getWritable();
31         self.__colWidths = widths;
32     }
33
34     getColWidths(): string[] {
35         const self = this.getLatest();
36         return self.__colWidths;
37     }
38
39     static clone(node: CustomTableNode) {
40         const newNode = new CustomTableNode(node.__key);
41         newNode.__id = node.__id;
42         newNode.__colWidths = node.__colWidths;
43         return newNode;
44     }
45
46     createDOM(config: EditorConfig): HTMLElement {
47         const dom = super.createDOM(config);
48         const id = this.getId();
49         if (id) {
50             dom.setAttribute('id', id);
51         }
52
53         const colWidths = this.getColWidths();
54         if (colWidths.length > 0) {
55             const colgroup = el('colgroup');
56             for (const width of colWidths) {
57                 const col = el('col');
58                 if (width) {
59                     col.style.width = width;
60                 }
61                 colgroup.append(col);
62             }
63             dom.append(colgroup);
64         }
65
66         return dom;
67     }
68
69     updateDOM(): boolean {
70         return true;
71     }
72
73     exportJSON(): SerializedCustomTableNode {
74         return {
75             ...super.exportJSON(),
76             type: 'custom-table',
77             version: 1,
78             id: this.__id,
79             colWidths: this.__colWidths,
80         };
81     }
82
83     static importJSON(serializedNode: SerializedCustomTableNode): CustomTableNode {
84         const node = $createCustomTableNode();
85         node.setId(serializedNode.id);
86         node.setColWidths(serializedNode.colWidths);
87         return node;
88     }
89
90     static importDOM(): DOMConversionMap|null {
91         return {
92             table(node: HTMLElement): DOMConversion|null {
93                 return {
94                     conversion: (element: HTMLElement): DOMConversionOutput|null => {
95                         const node = $createCustomTableNode();
96
97                         if (element.id) {
98                             node.setId(element.id);
99                         }
100
101                         const colWidths = getTableColumnWidths(element as HTMLTableElement);
102                         node.setColWidths(colWidths);
103
104                         return {node};
105                     },
106                     priority: 1,
107                 };
108             },
109         };
110     }
111 }
112
113 function getTableColumnWidths(table: HTMLTableElement): string[] {
114     const maxColRow = getMaxColRowFromTable(table);
115
116     const colGroup = table.querySelector('colgroup');
117     let widths: string[] = [];
118     if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) {
119         widths = extractWidthsFromRow(colGroup);
120     }
121     if (widths.filter(Boolean).length === 0 && maxColRow) {
122         widths = extractWidthsFromRow(maxColRow);
123     }
124
125     return widths;
126 }
127
128 function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement|null {
129     const rows = table.querySelectorAll('tr');
130     let maxColCount: number = 0;
131     let maxColRow: HTMLTableRowElement|null = null;
132
133     for (const row of rows) {
134         if (row.childElementCount > maxColCount) {
135             maxColRow = row;
136             maxColCount = row.childElementCount;
137         }
138     }
139
140     return maxColRow;
141 }
142
143 function extractWidthsFromRow(row: HTMLTableRowElement|HTMLTableColElement) {
144     return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
145 }
146
147 function extractWidthFromElement(element: HTMLElement): string {
148     let width = element.style.width || element.getAttribute('width');
149     if (width && !Number.isNaN(Number(width))) {
150         width = width + 'px';
151     }
152
153     return width || '';
154 }
155
156 export function $createCustomTableNode(): CustomTableNode {
157     return new CustomTableNode();
158 }
159
160 export function $isCustomTableNode(node: LexicalNode | null | undefined): boolean {
161     return node instanceof CustomTableNode;
162 }
163
164 export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number): void {
165     const rows = node.getChildren() as TableRowNode[];
166     let maxCols = 0;
167     for (const row of rows) {
168         const cellCount = row.getChildren().length;
169         if (cellCount > maxCols) {
170             maxCols = cellCount;
171         }
172     }
173
174     let colWidths = node.getColWidths();
175     if (colWidths.length === 0 || colWidths.length < maxCols) {
176         colWidths = Array(maxCols).fill('');
177     }
178
179     if (columnIndex + 1 > colWidths.length) {
180         console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
181     }
182
183     colWidths[columnIndex] = width + 'px';
184     node.setColWidths(colWidths);
185 }
186
187 export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number {
188     const colWidths = node.getColWidths();
189     if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) {
190         return Number(colWidths[columnIndex].replace('px', ''));
191     }
192
193     // Otherwise, get from table element
194     const table = editor.getElementByKey(node.__key) as HTMLTableElement|null;
195     if (table) {
196         const maxColRow = getMaxColRowFromTable(table);
197         if (maxColRow && maxColRow.children.length > columnIndex) {
198             const cell = maxColRow.children[columnIndex];
199             return cell.clientWidth;
200         }
201     }
202
203     return 0;
204 }