]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/nodes/custom-table.ts
c070e06b51d29be0d0e1fe091876fd1759c256b5
[bookstack] / resources / js / wysiwyg / nodes / custom-table.ts
1 import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table";
2 import {DOMConversion, DOMConversionMap, DOMConversionOutput, 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 rows = table.querySelectorAll('tr');
115     let maxColCount: number = 0;
116     let maxColRow: HTMLTableRowElement|null = null;
117
118     for (const row of rows) {
119         if (row.childElementCount > maxColCount) {
120             maxColRow = row;
121             maxColCount = row.childElementCount;
122         }
123     }
124
125     const colGroup = table.querySelector('colgroup');
126     let widths: string[] = [];
127     if (colGroup && colGroup.childElementCount === maxColCount) {
128         widths = extractWidthsFromRow(colGroup);
129     }
130     if (widths.filter(Boolean).length === 0 && maxColRow) {
131         widths = extractWidthsFromRow(maxColRow);
132     }
133
134     return widths;
135 }
136
137 function extractWidthsFromRow(row: HTMLTableRowElement|HTMLTableColElement) {
138     return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
139 }
140
141 function extractWidthFromElement(element: HTMLElement): string {
142     let width = element.style.width || element.getAttribute('width');
143     if (!Number.isNaN(Number(width))) {
144         width = width + 'px';
145     }
146
147     return width || '';
148 }
149
150 export function $createCustomTableNode(): CustomTableNode {
151     return new CustomTableNode();
152 }
153
154 export function $isCustomTableNode(node: LexicalNode | null | undefined): boolean {
155     return node instanceof CustomTableNode;
156 }
157
158 export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number): void {
159     const rows = node.getChildren() as TableRowNode[];
160     let maxCols = 0;
161     for (const row of rows) {
162         const cellCount = row.getChildren().length;
163         if (cellCount > maxCols) {
164             maxCols = cellCount;
165         }
166     }
167
168     let colWidths = node.getColWidths();
169     if (colWidths.length === 0 || colWidths.length < maxCols) {
170         colWidths = Array(maxCols).fill('');
171     }
172
173     if (columnIndex + 1 > colWidths.length) {
174         console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
175     }
176
177     colWidths[columnIndex] = width + 'px';
178     node.setColWidths(colWidths);
179     console.log('setting col widths', node, colWidths);
180 }