diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 0354b7935..a0ea2e1eb 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -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 diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts index e3f7bb570..b0f0bf346 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -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) { diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts index a045ba55d..9951bfe7f 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts @@ -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: 'Border style', - name: 'border_style', + 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: '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: 'Border color', - name: 'border_color', + 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: 'Background color', - name: 'background_color', + label: 'Height', + name: 'height', type: 'text', }, + { + label: 'Cell spacing', + name: 'cell_spacing', + type: 'text', + }, + { + 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([ diff --git a/resources/js/wysiwyg/ui/defaults/modals.ts b/resources/js/wysiwyg/ui/defaults/modals.ts index 30351602c..44d4e0360 100644 --- a/resources/js/wysiwyg/ui/defaults/modals.ts +++ b/resources/js/wysiwyg/ui/defaults/modals.ts @@ -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 = { link: { @@ -24,4 +24,12 @@ export const modals: Record = { 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 diff --git a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts index 24659b546..da0d3e5d0 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/dropdown-button.ts @@ -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; diff --git a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts index 45c3f39d1..e8cef3c8d 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index 43f00c001..3346e0a07 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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, [