]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Completed out table menu elements, logic pending
authorDan Brown <redacted>
Sat, 3 Aug 2024 17:01:54 +0000 (18:01 +0100)
committerDan Brown <redacted>
Sat, 3 Aug 2024 17:01:54 +0000 (18:01 +0100)
resources/js/wysiwyg/todo.md
resources/js/wysiwyg/ui/defaults/buttons/tables.ts
resources/js/wysiwyg/ui/defaults/forms/tables.ts
resources/js/wysiwyg/ui/defaults/modals.ts
resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts
resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts
resources/js/wysiwyg/ui/toolbars.ts

index 0354b7935ff40e42b4f829fa2c273d9bd9ed29c9..a0ea2e1ebfb13df6d28a65986bfd24012a6444b3 100644 (file)
@@ -3,15 +3,18 @@
 ## In progress
 
 - Table features
-  - Continued table dropdown menu
-  - Connect up cell properties form
+  - Cell properties form logic
   - Merge cell action
+  - Row properties form logic
+  - Table properties form logic
+    - Caption text support 
+  - Resize to contents button
+  - Remove formatting button
 
 ## Main Todo
 
 - Alignments: Use existing classes for blocks
 - Alignments: Handle inline block content (image, video)
-
 - Image paste upload
 - Keyboard shortcuts support
 - Add ID support to all block types
index e3f7bb570e5c0a194d001997bc8575b9ed679f2c..b0f0bf346a999b2348044e2fbdb3e4a83bfced79 100644 (file)
@@ -9,25 +9,93 @@ import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg";
 import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg";
 import {EditorUiContext} from "../../framework/core";
 import {
-    $getNodeFromSelection,
+    $getNodeFromSelection, $getParentOfType,
     $selectionContainsNodeType
 } from "../../../helpers";
-import {$getSelection} from "lexical";
+import {$getSelection, BaseSelection} from "lexical";
 import {$isCustomTableNode} from "../../../nodes/custom-table";
 import {
+    $createTableRowNode,
     $deleteTableColumn__EXPERIMENTAL,
     $deleteTableRow__EXPERIMENTAL,
     $insertTableColumn__EXPERIMENTAL,
     $insertTableRow__EXPERIMENTAL, $isTableCellNode,
-    $isTableNode, $isTableSelection, $unmergeCell, TableCellNode,
+    $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, TableNode,
 } from "@lexical/table";
 
+const neverActive = (): boolean => false;
+const cellNotSelected = (selection: BaseSelection|null) => !$selectionContainsNodeType(selection, $isTableCellNode);
 
 export const table: EditorBasicButtonDefinition = {
     label: 'Table',
     icon: tableIcon,
 };
 
+export const tableProperties: EditorButtonDefinition = {
+    label: 'Table properties',
+    icon: tableIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+            if (!$isTableCellNode(cell)) {
+                return;
+            }
+
+            const table = $getParentOfType(cell, $isTableNode);
+            const modalForm = context.manager.createModal('table_properties');
+            modalForm.show({});
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const clearTableFormatting: EditorButtonDefinition = {
+    label: 'Clear table formatting',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+            if (!$isTableCellNode(cell)) {
+                return;
+            }
+
+            const table = $getParentOfType(cell, $isTableNode);
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const resizeTableToContents: EditorButtonDefinition = {
+    label: 'Resize to contents',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+            if (!$isTableCellNode(cell)) {
+                return;
+            }
+
+            const table = $getParentOfType(cell, $isCustomTableNode);
+            if (!$isCustomTableNode(table)) {
+                return;
+            }
+
+            for (const row of table.getChildren()) {
+                if ($isTableRowNode(row)) {
+                    // TODO - Come back later as this may depend on if we
+                    //   are using a custom table row
+                }
+            }
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
 export const deleteTable: EditorButtonDefinition = {
     label: 'Delete table',
     icon: deleteIcon,
@@ -53,29 +121,27 @@ export const deleteTableMenuAction: EditorButtonDefinition = {
 };
 
 export const insertRowAbove: EditorButtonDefinition = {
-    label: 'Insert row above',
+    label: 'Insert row before',
     icon: insertRowAboveIcon,
     action(context: EditorUiContext) {
         context.editor.update(() => {
             $insertTableRow__EXPERIMENTAL(false);
         });
     },
-    isActive() {
-        return false;
-    }
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
 };
 
 export const insertRowBelow: EditorButtonDefinition = {
-    label: 'Insert row below',
+    label: 'Insert row after',
     icon: insertRowBelowIcon,
     action(context: EditorUiContext) {
         context.editor.update(() => {
             $insertTableRow__EXPERIMENTAL(true);
         });
     },
-    isActive() {
-        return false;
-    }
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
 };
 
 export const deleteRow: EditorButtonDefinition = {
@@ -86,9 +152,124 @@ export const deleteRow: EditorButtonDefinition = {
             $deleteTableRow__EXPERIMENTAL();
         });
     },
-    isActive() {
-        return false;
-    }
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const rowProperties: EditorButtonDefinition = {
+    label: 'Row properties',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const cell = $getNodeFromSelection($getSelection(), $isTableCellNode);
+            if (!$isTableCellNode(cell)) {
+                return;
+            }
+
+            const row = $getParentOfType(cell, $isTableRowNode);
+            const modalForm = context.manager.createModal('row_properties');
+            modalForm.show({});
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const cutRow: EditorButtonDefinition = {
+    label: 'Cut row',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const copyRow: EditorButtonDefinition = {
+    label: 'Copy row',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const pasteRowBefore: EditorButtonDefinition = {
+    label: 'Paste row before',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const pasteRowAfter: EditorButtonDefinition = {
+    label: 'Paste row after',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const cutColumn: EditorButtonDefinition = {
+    label: 'Cut column',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const copyColumn: EditorButtonDefinition = {
+    label: 'Copy column',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const pasteColumnBefore: EditorButtonDefinition = {
+    label: 'Paste column before',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
+};
+
+export const pasteColumnAfter: EditorButtonDefinition = {
+    label: 'Paste column after',
+    format: 'long',
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            // TODO
+        });
+    },
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
 };
 
 export const insertColumnBefore: EditorButtonDefinition = {
@@ -142,12 +323,8 @@ export const cellProperties: EditorButtonDefinition = {
             }
         });
     },
-    isActive() {
-        return false;
-    },
-    isDisabled(selection) {
-        return !$selectionContainsNodeType(selection, $isTableCellNode);
-    }
+    isActive: neverActive,
+    isDisabled: cellNotSelected,
 };
 
 export const mergeCells: EditorButtonDefinition = {
@@ -159,9 +336,7 @@ export const mergeCells: EditorButtonDefinition = {
             // https://github.com/facebook/lexical/blob/f373759a7849f473d34960a6bf4e34b2a011e762/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx#L299
         });
     },
-    isActive() {
-        return false;
-    },
+    isActive: neverActive,
     isDisabled(selection) {
         return !$isTableSelection(selection);
     }
@@ -174,9 +349,7 @@ export const splitCell: EditorButtonDefinition = {
             $unmergeCell();
         });
     },
-    isActive() {
-        return false;
-    },
+    isActive: neverActive,
     isDisabled(selection) {
         const cell = $getNodeFromSelection(selection, $isTableCellNode) as TableCellNode|null;
         if (cell) {
index a045ba55d61f7176c64d3931fb9e82da288537ce..9951bfe7ff6f700b17855026f363449b13b7e1bc 100644 (file)
@@ -5,12 +5,54 @@ import {
     EditorSelectFormFieldDefinition
 } from "../../framework/forms";
 import {EditorUiContext} from "../../framework/core";
-import {setEditorContentFromHtml} from "../../../actions";
+
+const borderStyleInput: EditorSelectFormFieldDefinition = {
+    label: 'Border style',
+    name: 'border_style',
+    type: 'select',
+    valuesByLabel: {
+        'Select...': '',
+        "Solid": 'solid',
+        "Dotted": 'dotted',
+        "Dashed": 'dashed',
+        "Double": 'double',
+        "Groove": 'groove',
+        "Ridge": 'ridge',
+        "Inset": 'inset',
+        "Outset": 'outset',
+        "None": 'none',
+        "Hidden": 'hidden',
+    }
+};
+
+const borderColorInput: EditorFormFieldDefinition = {
+    label: 'Border color',
+    name: 'border_color',
+    type: 'text',
+};
+
+const backgroundColorInput: EditorFormFieldDefinition = {
+    label: 'Background color',
+    name: 'background_color',
+    type: 'text',
+};
+
+const alignmentInput: EditorSelectFormFieldDefinition = {
+    label: 'Alignment',
+    name: 'align',
+    type: 'select',
+    valuesByLabel: {
+        'None': '',
+        'Left': 'left',
+        'Center': 'center',
+        'Right': 'right',
+    }
+};
 
 export const cellProperties: EditorFormDefinition = {
     submitText: 'Save',
     async action(formData, context: EditorUiContext) {
-        setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || '');
+        // TODO
         return true;
     },
     fields: [
@@ -37,16 +79,10 @@ export const cellProperties: EditorFormDefinition = {
                         }
                     } as EditorSelectFormFieldDefinition,
                     {
+                        ...alignmentInput,
                         label: 'Horizontal align',
                         name: 'h_align',
-                        type: 'select',
-                        valuesByLabel: {
-                            'None': '',
-                            'Left': 'left',
-                            'Center': 'center',
-                            'Right': 'right',
-                        }
-                    } as EditorSelectFormFieldDefinition,
+                    },
                     {
                         label: 'Vertical align',
                         name: 'v_align',
@@ -66,34 +102,122 @@ export const cellProperties: EditorFormDefinition = {
                         name: 'border_width',
                         type: 'text',
                     },
+                    borderStyleInput,
+                    borderColorInput,
+                    backgroundColorInput,
+                ];
+
+                return new EditorFormTabs([
+                    {
+                        label: 'General',
+                        contents: generalFields,
+                    },
+                    {
+                        label: 'Advanced',
+                        contents: advancedFields,
+                    }
+                ])
+            }
+        },
+    ],
+};
+
+export const rowProperties: EditorFormDefinition = {
+    submitText: 'Save',
+    async action(formData, context: EditorUiContext) {
+        // TODO
+        return true;
+    },
+    fields: [
+        {
+            build() {
+                const generalFields: EditorFormFieldDefinition[] = [
                     {
-                        label: 'Border style',
-                        name: 'border_style',
+                        label: 'Row type',
+                        name: 'type',
                         type: 'select',
                         valuesByLabel: {
-                            'Select...': '',
-                            "Solid": 'solid',
-                            "Dotted": 'dotted',
-                            "Dashed": 'dashed',
-                            "Double": 'double',
-                            "Groove": 'groove',
-                            "Ridge": 'ridge',
-                            "Inset": 'inset',
-                            "Outset": 'outset',
-                            "None": 'none',
-                            "Hidden": 'hidden',
+                            'Body': 'body',
+                            'Header': 'header',
+                            'Footer': 'footer',
                         }
                     } as EditorSelectFormFieldDefinition,
+                    alignmentInput,
+                    {
+                        label: 'Height',
+                        name: 'height',
+                        type: 'text',
+                    },
+                ];
+
+                const advancedFields: EditorFormFieldDefinition[] = [
+                    borderStyleInput,
+                    borderColorInput,
+                    backgroundColorInput,
+                ];
+
+                return new EditorFormTabs([
+                    {
+                        label: 'General',
+                        contents: generalFields,
+                    },
+                    {
+                        label: 'Advanced',
+                        contents: advancedFields,
+                    }
+                ])
+            }
+        },
+    ],
+};
+
+export const tableProperties: EditorFormDefinition = {
+    submitText: 'Save',
+    async action(formData, context: EditorUiContext) {
+        // TODO
+        return true;
+    },
+    fields: [
+        {
+            build() {
+                const generalFields: EditorFormFieldDefinition[] = [
+                    {
+                        label: 'Width',
+                        name: 'width',
+                        type: 'text',
+                    },
+                    {
+                        label: 'Height',
+                        name: 'height',
+                        type: 'text',
+                    },
                     {
-                        label: 'Border color',
-                        name: 'border_color',
+                        label: 'Cell spacing',
+                        name: 'cell_spacing',
                         type: 'text',
                     },
                     {
-                        label: 'Background color',
-                        name: 'background_color',
+                        label: 'Cell padding',
+                        name: 'cell_padding',
                         type: 'text',
                     },
+                    {
+                        label: 'Border width',
+                        name: 'border_width',
+                        type: 'text',
+                    },
+                    {
+                        label: 'caption',
+                        name: 'height',
+                        type: 'text', // TODO -
+                    },
+                    alignmentInput,
+                ];
+
+                const advancedFields: EditorFormFieldDefinition[] = [
+                    borderStyleInput,
+                    borderColorInput,
+                    backgroundColorInput,
                 ];
 
                 return new EditorFormTabs([
index 30351602cc4eb9104b95d5d6c3a6ab4c774e2893..44d4e0360947973d0af6f7ae7e164ac7177f3b82 100644 (file)
@@ -1,7 +1,7 @@
 import {EditorFormModalDefinition} from "../framework/modals";
 import {image, link, media} from "./forms/objects";
 import {source} from "./forms/controls";
-import {cellProperties} from "./forms/tables";
+import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
 
 export const modals: Record<string, EditorFormModalDefinition> = {
     link: {
@@ -24,4 +24,12 @@ export const modals: Record<string, EditorFormModalDefinition> = {
         title: 'Cell Properties',
         form: cellProperties,
     },
+    row_properties: {
+        title: 'Row Properties',
+        form: rowProperties,
+    },
+    table_properties: {
+        title: 'Table Properties',
+        form: tableProperties,
+    },
 };
\ No newline at end of file
index 24659b5469cd0da30fb180b057c0b7053e2d7cfe..da0d3e5d0cf1fc63546580eac64b7d25fae0e259 100644 (file)
@@ -24,7 +24,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
     constructor(options: EditorDropdownButtonOptions, children: EditorUiElement[]) {
         super(children);
         this.childItems = children;
-        this.options = Object.assign(defaultOptions, options);
+        this.options = Object.assign({}, defaultOptions, options);
 
         if (options.button instanceof EditorButton) {
             this.button = options.button;
@@ -61,7 +61,7 @@ export class EditorDropdownButton extends EditorContainerUiElement {
             class: 'editor-dropdown-menu-container',
         }, [button, menu]);
 
-        handleDropdown({toggle : button, menu : menu,
+        handleDropdown({toggle: button, menu : menu,
             showOnHover: this.options.showOnHover,
             onOpen : () => {
             this.open = true;
index 45c3f39d190d3aa003c11cca1d95bdee9c9e8754..e8cef3c8d2014e3fdab9d51f64b9c31a6f0a3a83 100644 (file)
@@ -44,5 +44,5 @@ export function handleDropdown(options: HandleDropdownParams) {
         toggle.addEventListener('mouseenter', toggleShowing);
     }
 
-    menu.addEventListener('mouseleave', hide);
+    menu.parentElement?.addEventListener('mouseleave', hide);
 }
\ No newline at end of file
index 43f00c0014be7143262e17d35d0417971231326a..3346e0a0724ca78cfaf3fa73bde56838fdb8e288 100644 (file)
@@ -9,14 +9,27 @@ import {EditorTableCreator} from "./framework/blocks/table-creator";
 import {EditorColorButton} from "./framework/blocks/color-button";
 import {EditorOverflowContainer} from "./framework/blocks/overflow-container";
 import {
-    cellProperties,
+    cellProperties, clearTableFormatting,
+    copyColumn,
+    copyRow,
+    cutColumn,
+    cutRow,
     deleteColumn,
     deleteRow,
-    deleteTable, deleteTableMenuAction, insertColumnAfter,
+    deleteTable,
+    deleteTableMenuAction,
+    insertColumnAfter,
     insertColumnBefore,
     insertRowAbove,
-    insertRowBelow, mergeCells, splitCell,
-    table
+    insertRowBelow,
+    mergeCells,
+    pasteColumnAfter,
+    pasteColumnBefore,
+    pasteRowAfter,
+    pasteRowBefore, resizeTableToContents,
+    rowProperties,
+    splitCell,
+    table, tableProperties
 } from "./defaults/buttons/tables";
 import {fullscreen, redo, source, undo} from "./defaults/buttons/controls";
 import {
@@ -119,11 +132,33 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
                 new EditorDropdownButton({button: {...table, format: 'long'}, showOnHover: true}, [
                     new EditorTableCreator(),
                 ]),
-                new EditorDropdownButton({button: {label: 'Cell'}}, [
+                new EditorDropdownButton({button: {label: 'Cell'}, direction: 'vertical', showOnHover: true}, [
                     new EditorButton(cellProperties),
                     new EditorButton(mergeCells),
                     new EditorButton(splitCell),
                 ]),
+                new EditorDropdownButton({button: {label: 'Row'}, direction: 'vertical', showOnHover: true}, [
+                    new EditorButton({...insertRowAbove, format: 'long'}),
+                    new EditorButton({...insertRowBelow, format: 'long'}),
+                    new EditorButton({...deleteRow, format: 'long'}),
+                    new EditorButton(rowProperties),
+                    new EditorButton(cutRow),
+                    new EditorButton(copyRow),
+                    new EditorButton(pasteRowBefore),
+                    new EditorButton(pasteRowAfter),
+                ]),
+                new EditorDropdownButton({button: {label: 'Column'}, direction: 'vertical', showOnHover: true}, [
+                    new EditorButton({...insertColumnBefore, format: 'long'}),
+                    new EditorButton({...insertColumnAfter, format: 'long'}),
+                    new EditorButton({...deleteColumn, format: 'long'}),
+                    new EditorButton(cutColumn),
+                    new EditorButton(copyColumn),
+                    new EditorButton(pasteColumnBefore),
+                    new EditorButton(pasteColumnAfter),
+                ]),
+                new EditorButton({...tableProperties, format: 'long'}),
+                new EditorButton(clearTableFormatting),
+                new EditorButton(resizeTableToContents),
                 new EditorButton(deleteTableMenuAction),
             ]),
 
@@ -176,7 +211,7 @@ export function getCodeToolbarContent(): EditorUiElement[] {
 export function getTableToolbarContent(): EditorUiElement[] {
     return [
         new EditorOverflowContainer(2, [
-            // Todo - Table properties
+            new EditorButton(tableProperties),
             new EditorButton(deleteTable),
         ]),
         new EditorOverflowContainer(3, [