]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started linking up cell properties form
authorDan Brown <redacted>
Mon, 5 Aug 2024 14:08:52 +0000 (15:08 +0100)
committerDan Brown <redacted>
Mon, 5 Aug 2024 14:08:52 +0000 (15:08 +0100)
resources/js/wysiwyg/nodes/custom-paragraph.ts
resources/js/wysiwyg/nodes/custom-table-cell-node.ts [new file with mode: 0644]
resources/js/wysiwyg/nodes/index.ts
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/buttons/tables.ts
resources/js/wysiwyg/ui/defaults/forms/tables.ts

index f13cef56f31b3742f8a21d3203423432a3f4d7f7..97647bf5e8d4a6bc34b04962b709cde6ccbf3d2a 100644 (file)
@@ -93,6 +93,6 @@ export function $createCustomParagraphNode() {
     return new CustomParagraphNode();
 }
 
-export function $isCustomParagraphNode(node: LexicalNode | null | undefined) {
+export function $isCustomParagraphNode(node: LexicalNode | null | undefined): node is CustomParagraphNode {
     return node instanceof CustomParagraphNode;
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/nodes/custom-table-cell-node.ts b/resources/js/wysiwyg/nodes/custom-table-cell-node.ts
new file mode 100644 (file)
index 0000000..693ef5f
--- /dev/null
@@ -0,0 +1,90 @@
+import {EditorConfig} from "lexical/LexicalEditor";
+import {DOMExportOutput, LexicalEditor, LexicalNode, Spread} from "lexical";
+
+import {SerializedTableCellNode, TableCellHeaderStates, TableCellNode} from "@lexical/table";
+import {TableCellHeaderState} from "@lexical/table/LexicalTableCellNode";
+
+export type SerializedCustomTableCellNode = Spread<{
+    styles: Record<string, string>,
+}, SerializedTableCellNode>
+
+export class CustomTableCellNode extends TableCellNode {
+    __styles: Map<string, string> = new Map;
+
+    static getType(): string {
+        return 'custom-table-cell';
+    }
+
+    static clone(node: CustomTableCellNode): CustomTableCellNode {
+        const cellNode = new CustomTableCellNode(
+            node.__headerState,
+            node.__colSpan,
+            node.__width,
+            node.__key,
+        );
+        cellNode.__rowSpan = node.__rowSpan;
+        cellNode.__styles = new Map(node.__styles);
+        return cellNode;
+    }
+
+    getStyles(): Map<string, string> {
+        const self = this.getLatest();
+        return new Map(self.__styles);
+    }
+
+    setStyles(styles: Map<string, string>): void {
+        const self = this.getWritable();
+        self.__styles = new Map(styles);
+    }
+
+    updateTag(tag: string): void {
+        const isHeader = tag.toLowerCase() === 'th';
+        const state = isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS;
+        const self = this.getWritable();
+        self.__headerState = state;
+    }
+
+    createDOM(config: EditorConfig): HTMLElement {
+        const element = super.createDOM(config);
+
+        for (const [name, value] of this.__styles.entries()) {
+            element.style.setProperty(name, value);
+        }
+
+        return element;
+    }
+
+    // TODO - Import DOM
+
+    updateDOM(prevNode: CustomTableCellNode): boolean {
+        return super.updateDOM(prevNode)
+            || this.__styles !== prevNode.__styles;
+    }
+
+    exportDOM(editor: LexicalEditor): DOMExportOutput {
+        const element = this.createDOM(editor._config);
+        return {
+            element
+        };
+    }
+
+    exportJSON(): SerializedCustomTableCellNode {
+        return {
+            ...super.exportJSON(),
+            type: 'custom-table-cell',
+            styles: Object.fromEntries(this.__styles),
+        };
+    }
+}
+
+export function $createCustomTableCellNode(
+    headerState: TableCellHeaderState,
+    colSpan = 1,
+    width?: number,
+): CustomTableCellNode {
+    return new CustomTableCellNode(headerState, colSpan, width);
+}
+
+export function $isCustomTableCellNode(node: LexicalNode | null | undefined): node is CustomTableCellNode {
+    return node instanceof CustomTableCellNode;
+}
\ No newline at end of file
index f0df08fcbb4f18be70baa1f3a72e96b7d8db3f39..92f6d2336cf0b8e545bc99b10f854050e5cb0abe 100644 (file)
@@ -20,6 +20,7 @@ import {DiagramNode} from "./diagram";
 import {EditorUiContext} from "../ui/framework/core";
 import {MediaNode} from "./media";
 import {CustomListItemNode} from "./custom-list-item";
+import {CustomTableCellNode} from "./custom-table-cell-node";
 
 /**
  * Load the nodes for lexical.
@@ -33,7 +34,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         CustomListItemNode,
         CustomTableNode,
         TableRowNode,
-        TableCellNode,
+        CustomTableCellNode,
         ImageNode,
         HorizontalRuleNode,
         DetailsNode, SummaryNode,
@@ -59,7 +60,19 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
             with: (node: ListItemNode) => {
                 return new CustomListItemNode(node.__value, node.__checked);
             }
-        }
+        },
+        {
+            replace: TableCellNode,
+            with: (node: TableCellNode) => {
+                const cell = new CustomTableCellNode(
+                    node.__headerState,
+                    node.__colSpan,
+                    node.__width,
+                );
+                cell.__rowSpan = node.__rowSpan;
+                return cell;
+            }
+        },
     ];
 }
 
index a0ea2e1ebfb13df6d28a65986bfd24012a6444b3..d925711e17b5692a063c8942ec7d92839e1c73f4 100644 (file)
@@ -13,7 +13,7 @@
 
 ## Main Todo
 
-- Alignments: Use existing classes for blocks
+- Alignments: Use existing classes for blocks (including table cells)
 - Alignments: Handle inline block content (image, video)
 - Image paste upload
 - Keyboard shortcuts support
index 2cc2e701b69ad5dbb8c7e3ee3783ec37fcfe14af..3b431141f4b768b3dc255c376ef69a34032ecd3f 100644 (file)
@@ -11,18 +11,19 @@ import {EditorUiContext} from "../../framework/core";
 import {$getSelection, BaseSelection} from "lexical";
 import {$isCustomTableNode} from "../../../nodes/custom-table";
 import {
-    $createTableRowNode,
     $deleteTableColumn__EXPERIMENTAL,
     $deleteTableRow__EXPERIMENTAL,
     $insertTableColumn__EXPERIMENTAL,
-    $insertTableRow__EXPERIMENTAL, $isTableCellNode,
-    $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, TableNode,
+    $insertTableRow__EXPERIMENTAL,
+    $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode,
 } from "@lexical/table";
 import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection";
 import {$getParentOfType} from "../../../utils/nodes";
+import {$isCustomTableCellNode} from "../../../nodes/custom-table-cell-node";
+import {showCellPropertiesForm} from "../forms/tables";
 
 const neverActive = (): boolean => false;
-const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
+const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isCustomTableCellNode);
 
 export const table: EditorBasicButtonDefinition = {
     label: 'Table',
@@ -34,8 +35,8 @@ export const tableProperties: EditorButtonDefinition = {
     icon: tableIcon,
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
-            if (!$isTableCellNode(cell)) {
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if (!$isCustomTableCellNode(cell)) {
                 return;
             }
 
@@ -54,8 +55,8 @@ export const clearTableFormatting: EditorButtonDefinition = {
     format: 'long',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
-            if (!$isTableCellNode(cell)) {
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if (!$isCustomTableCellNode(cell)) {
                 return;
             }
 
@@ -72,8 +73,8 @@ export const resizeTableToContents: EditorButtonDefinition = {
     format: 'long',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
-            if (!$isTableCellNode(cell)) {
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if (!$isCustomTableCellNode(cell)) {
                 return;
             }
 
@@ -159,8 +160,8 @@ export const rowProperties: EditorButtonDefinition = {
     format: 'long',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
-            if (!$isTableCellNode(cell)) {
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if (!$isCustomTableCellNode(cell)) {
                 return;
             }
 
@@ -313,11 +314,9 @@ export const cellProperties: EditorButtonDefinition = {
     label: 'Cell properties',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
-            if ($isTableCellNode(cell)) {
-
-                const modalForm = context.manager.createModal('cell_properties');
-                modalForm.show({});
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if ($isCustomTableCellNode(cell)) {
+                showCellPropertiesForm(cell, context);
             }
         });
     },
@@ -349,7 +348,7 @@ export const splitCell: EditorButtonDefinition = {
     },
     isActive: neverActive,
     isDisabled(selection) {
-        const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
+        const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as TableCellNode|null;
         if (cell) {
             const merged = cell.getRowSpan() > 1 || cell.getColSpan() > 1;
             return !merged;
index 9951bfe7ff6f700b17855026f363449b13b7e1bc..291b355e7084d0b8bc97c639cfd07931500cde9e 100644 (file)
@@ -5,6 +5,11 @@ import {
     EditorSelectFormFieldDefinition
 } from "../../framework/forms";
 import {EditorUiContext} from "../../framework/core";
+import {$isCustomTableCellNode, CustomTableCellNode} from "../../../nodes/custom-table-cell-node";
+import {EditorFormModal} from "../../framework/modals";
+import {$getNodeFromSelection} from "../../../utils/selection";
+import {$getSelection, ElementFormatType} from "lexical";
+import {TableCellHeaderStates} from "@lexical/table";
 
 const borderStyleInput: EditorSelectFormFieldDefinition = {
     label: 'Border style',
@@ -49,10 +54,46 @@ const alignmentInput: EditorSelectFormFieldDefinition = {
     }
 };
 
+export function showCellPropertiesForm(cell: CustomTableCellNode, context: EditorUiContext): EditorFormModal {
+    const styles = cell.getStyles();
+    const modalForm = context.manager.createModal('cell_properties');
+    modalForm.show({
+        width: '', // TODO
+        height: styles.get('height') || '',
+        type: cell.getTag(),
+        h_align: '', // TODO
+        v_align: styles.get('vertical-align') || '',
+        border_width: styles.get('border-width') || '',
+        border_style: styles.get('border-style') || '',
+        border_color: styles.get('border-color') || '',
+        background_color: styles.get('background-color') || '',
+    });
+    return modalForm;
+}
+
 export const cellProperties: EditorFormDefinition = {
     submitText: 'Save',
     async action(formData, context: EditorUiContext) {
-        // TODO
+        // TODO - Set for cell selection range
+        context.editor.update(() => {
+            const cell = $getNodeFromSelection($getSelection(), $isCustomTableCellNode);
+            if ($isCustomTableCellNode(cell)) {
+                // TODO - Set width
+                cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType);
+                cell.updateTag(formData.get('type')?.toString() || '');
+
+                const styles = cell.getStyles();
+                styles.set('height', formData.get('height')?.toString() || '');
+                styles.set('vertical-align', formData.get('v_align')?.toString() || '');
+                styles.set('border-width', formData.get('border_width')?.toString() || '');
+                styles.set('border-style', formData.get('border_style')?.toString() || '');
+                styles.set('border-color', formData.get('border_color')?.toString() || '');
+                styles.set('background-color', formData.get('background_color')?.toString() || '');
+
+                cell.setStyles(styles);
+            }
+        });
+
         return true;
     },
     fields: [
@@ -60,31 +101,31 @@ export const cellProperties: EditorFormDefinition = {
             build() {
                 const generalFields: EditorFormFieldDefinition[] = [
                     {
-                        label: 'Width',
+                        label: 'Width', // Colgroup width
                         name: 'width',
                         type: 'text',
                     },
                     {
-                        label: 'Height',
+                        label: 'Height', // inline-style: height
                         name: 'height',
                         type: 'text',
                     },
                     {
-                        label: 'Cell type',
+                        label: 'Cell type', // element
                         name: 'type',
                         type: 'select',
                         valuesByLabel: {
-                            'Cell': 'cell',
-                            'Header cell': 'header',
+                            'Cell': 'td',
+                            'Header cell': 'th',
                         }
                     } as EditorSelectFormFieldDefinition,
                     {
-                        ...alignmentInput,
+                        ...alignmentInput, // class: 'align-right/left/center'
                         label: 'Horizontal align',
                         name: 'h_align',
                     },
                     {
-                        label: 'Vertical align',
+                        label: 'Vertical align', // inline-style: vertical-align
                         name: 'v_align',
                         type: 'select',
                         valuesByLabel: {
@@ -98,13 +139,13 @@ export const cellProperties: EditorFormDefinition = {
 
                 const advancedFields: EditorFormFieldDefinition[] = [
                     {
-                        label: 'Border width',
+                        label: 'Border width', // inline-style: border-width
                         name: 'border_width',
                         type: 'text',
                     },
-                    borderStyleInput,
-                    borderColorInput,
-                    backgroundColorInput,
+                    borderStyleInput, // inline-style: border-style
+                    borderColorInput, // inline-style: border-color
+                    backgroundColorInput, // inline-style: background-color
                 ];
 
                 return new EditorFormTabs([
@@ -170,7 +211,6 @@ export const rowProperties: EditorFormDefinition = {
         },
     ],
 };
-
 export const tableProperties: EditorFormDefinition = {
     submitText: 'Save',
     async action(formData, context: EditorUiContext) {