]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Created custom table node with col width handling
authorDan Brown <redacted>
Mon, 24 Jun 2024 19:50:17 +0000 (20:50 +0100)
committerDan Brown <redacted>
Mon, 24 Jun 2024 19:50:17 +0000 (20:50 +0100)
resources/js/wysiwyg/nodes/custom-table.ts [new file with mode: 0644]
resources/js/wysiwyg/nodes/index.ts
resources/js/wysiwyg/ui/framework/blocks/table-creator.ts
resources/js/wysiwyg/ui/toolbars.ts

diff --git a/resources/js/wysiwyg/nodes/custom-table.ts b/resources/js/wysiwyg/nodes/custom-table.ts
new file mode 100644 (file)
index 0000000..c070e06
--- /dev/null
@@ -0,0 +1,180 @@
+import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table";
+import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical";
+import {EditorConfig} from "lexical/LexicalEditor";
+import {el} from "../helpers";
+
+export type SerializedCustomTableNode = Spread<{
+    id: string;
+    colWidths: string[];
+}, SerializedTableNode>
+
+export class CustomTableNode extends TableNode {
+    __id: string = '';
+    __colWidths: string[] = [];
+
+    static getType() {
+        return 'custom-table';
+    }
+
+    setId(id: string) {
+        const self = this.getWritable();
+        self.__id = id;
+    }
+
+    getId(): string {
+        const self = this.getLatest();
+        return self.__id;
+    }
+
+    setColWidths(widths: string[]) {
+        const self = this.getWritable();
+        self.__colWidths = widths;
+    }
+
+    getColWidths(): string[] {
+        const self = this.getLatest();
+        return self.__colWidths;
+    }
+
+    static clone(node: CustomTableNode) {
+        const newNode = new CustomTableNode(node.__key);
+        newNode.__id = node.__id;
+        newNode.__colWidths = node.__colWidths;
+        return newNode;
+    }
+
+    createDOM(config: EditorConfig): HTMLElement {
+        const dom = super.createDOM(config);
+        const id = this.getId();
+        if (id) {
+            dom.setAttribute('id', id);
+        }
+
+        const colWidths = this.getColWidths();
+        if (colWidths.length > 0) {
+            const colgroup = el('colgroup');
+            for (const width of colWidths) {
+                const col = el('col');
+                if (width) {
+                    col.style.width = width;
+                }
+                colgroup.append(col);
+            }
+            dom.append(colgroup);
+        }
+
+        return dom;
+    }
+
+    updateDOM(): boolean {
+        return true;
+    }
+
+    exportJSON(): SerializedCustomTableNode {
+        return {
+            ...super.exportJSON(),
+            type: 'custom-table',
+            version: 1,
+            id: this.__id,
+            colWidths: this.__colWidths,
+        };
+    }
+
+    static importJSON(serializedNode: SerializedCustomTableNode): CustomTableNode {
+        const node = $createCustomTableNode();
+        node.setId(serializedNode.id);
+        node.setColWidths(serializedNode.colWidths);
+        return node;
+    }
+
+    static importDOM(): DOMConversionMap|null {
+        return {
+            table(node: HTMLElement): DOMConversion|null {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+                        const node = $createCustomTableNode();
+
+                        if (element.id) {
+                            node.setId(element.id);
+                        }
+
+                        const colWidths = getTableColumnWidths(element as HTMLTableElement);
+                        node.setColWidths(colWidths);
+
+                        return {node};
+                    },
+                    priority: 1,
+                };
+            },
+        };
+    }
+}
+
+function getTableColumnWidths(table: HTMLTableElement): string[] {
+    const rows = table.querySelectorAll('tr');
+    let maxColCount: number = 0;
+    let maxColRow: HTMLTableRowElement|null = null;
+
+    for (const row of rows) {
+        if (row.childElementCount > maxColCount) {
+            maxColRow = row;
+            maxColCount = row.childElementCount;
+        }
+    }
+
+    const colGroup = table.querySelector('colgroup');
+    let widths: string[] = [];
+    if (colGroup && colGroup.childElementCount === maxColCount) {
+        widths = extractWidthsFromRow(colGroup);
+    }
+    if (widths.filter(Boolean).length === 0 && maxColRow) {
+        widths = extractWidthsFromRow(maxColRow);
+    }
+
+    return widths;
+}
+
+function extractWidthsFromRow(row: HTMLTableRowElement|HTMLTableColElement) {
+    return [...row.children].map(child => extractWidthFromElement(child as HTMLElement))
+}
+
+function extractWidthFromElement(element: HTMLElement): string {
+    let width = element.style.width || element.getAttribute('width');
+    if (!Number.isNaN(Number(width))) {
+        width = width + 'px';
+    }
+
+    return width || '';
+}
+
+export function $createCustomTableNode(): CustomTableNode {
+    return new CustomTableNode();
+}
+
+export function $isCustomTableNode(node: LexicalNode | null | undefined): boolean {
+    return node instanceof CustomTableNode;
+}
+
+export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number): void {
+    const rows = node.getChildren() as TableRowNode[];
+    let maxCols = 0;
+    for (const row of rows) {
+        const cellCount = row.getChildren().length;
+        if (cellCount > maxCols) {
+            maxCols = cellCount;
+        }
+    }
+
+    let colWidths = node.getColWidths();
+    if (colWidths.length === 0 || colWidths.length < maxCols) {
+        colWidths = Array(maxCols).fill('');
+    }
+
+    if (columnIndex + 1 > colWidths.length) {
+        console.error(`Attempted to set table column width for column [${columnIndex}] but only ${colWidths.length} columns found`);
+    }
+
+    colWidths[columnIndex] = width + 'px';
+    node.setColWidths(colWidths);
+    console.log('setting col widths', node, colWidths);
+}
\ No newline at end of file
index ea6206ac2bad545e72f9d9ecbd7c884a9fc6cc5d..6b1b66e66785de362fdd307dd879381a301f5448 100644 (file)
@@ -7,6 +7,7 @@ import {ImageNode} from "./image";
 import {DetailsNode, SummaryNode} from "./details";
 import {ListItemNode, ListNode} from "@lexical/list";
 import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
+import {CustomTableNode} from "./custom-table";
 
 /**
  * Load the nodes for lexical.
@@ -18,19 +19,25 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         QuoteNode, // Todo - Create custom
         ListNode, // Todo - Create custom
         ListItemNode,
-        TableNode, // Todo - Create custom,
+        CustomTableNode,
         TableRowNode,
         TableCellNode,
         ImageNode,
         DetailsNode, SummaryNode,
         CustomParagraphNode,
+        LinkNode,
         {
             replace: ParagraphNode,
             with: (node: ParagraphNode) => {
                 return new CustomParagraphNode();
             }
         },
-        LinkNode,
+        {
+            replace: TableNode,
+            with(node: TableNode) {
+                return new CustomTableNode();
+            }
+        },
     ];
 }
 
index c54645856ab806a155ecf958caa897316c81000e..8c28953d566fe7f9e6a81ab24ae3a242820385e4 100644 (file)
@@ -1,6 +1,7 @@
 import {el, insertNewBlockNodeAtSelection} from "../../../helpers";
 import {EditorUiElement} from "../core";
 import {$createTableNodeWithDimensions} from "@lexical/table";
+import {CustomTableNode} from "../../../nodes/custom-table";
 
 
 export class EditorTableCreator extends EditorUiElement {
@@ -73,7 +74,7 @@ export class EditorTableCreator extends EditorUiElement {
         }
 
         this.getContext().editor.update(() => {
-            const table = $createTableNodeWithDimensions(rows, columns, false);
+            const table = $createTableNodeWithDimensions(rows, columns, false) as CustomTableNode;
             insertNewBlockNodeAtSelection(table);
         });
     }
index 821c9f9cfea9e86cd6501758c2989f108b1380b3..7f7e99a785a331044563292b35d5453d2e96ebec 100644 (file)
@@ -9,7 +9,7 @@ import {
     undo,
     warningCallout
 } from "./defaults/button-definitions";
-import {EditorContainerUiElement, EditorSimpleClassContainer} from "./framework/core";
+import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiContext} from "./framework/core";
 import {el} from "../helpers";
 import {EditorFormatMenu} from "./framework/blocks/format-menu";
 import {FormatPreviewButton} from "./framework/blocks/format-preview-button";
@@ -17,6 +17,8 @@ import {EditorDropdownButton} from "./framework/blocks/dropdown-button";
 import {EditorColorPicker} from "./framework/blocks/color-picker";
 import {EditorTableCreator} from "./framework/blocks/table-creator";
 import {EditorColorButton} from "./framework/blocks/color-button";
+import {$isCustomTableNode, $setTableColumnWidth, CustomTableNode} from "../nodes/custom-table";
+import {$getRoot} from "lexical";
 
 export function getMainEditorFullToolbar(): EditorContainerUiElement {
     return new EditorSimpleClassContainer('editor-toolbar-main', [
@@ -69,5 +71,29 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
 
         // Meta elements
         new EditorButton(source),
+
+        // Test
+        new EditorButton({
+            label: 'Expand table col 2',
+            action(context: EditorUiContext) {
+                context.editor.update(() => {
+                    const root = $getRoot();
+                    let table: CustomTableNode|null = null;
+                    for (const child of root.getChildren()) {
+                        if ($isCustomTableNode(child)) {
+                            table = child as CustomTableNode;
+                            break;
+                        }
+                    }
+
+                    if (table) {
+                        $setTableColumnWidth(table, 1, 500);
+                    }
+                });
+            },
+            isActive() {
+                return false;
+            }
+        })
     ]);
 }
\ No newline at end of file