diff --git a/resources/js/wysiwyg/nodes/custom-paragraph.ts b/resources/js/wysiwyg/nodes/custom-paragraph.ts index f13cef56f..97647bf5e 100644 --- a/resources/js/wysiwyg/nodes/custom-paragraph.ts +++ b/resources/js/wysiwyg/nodes/custom-paragraph.ts @@ -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 index 000000000..693ef5f5b --- /dev/null +++ b/resources/js/wysiwyg/nodes/custom-table-cell-node.ts @@ -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, +}, SerializedTableCellNode> + +export class CustomTableCellNode extends TableCellNode { + __styles: Map = 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 { + const self = this.getLatest(); + return new Map(self.__styles); + } + + setStyles(styles: Map): 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 diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts index f0df08fcb..92f6d2336 100644 --- a/resources/js/wysiwyg/nodes/index.ts +++ b/resources/js/wysiwyg/nodes/index.ts @@ -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 | CustomListItemNode, CustomTableNode, TableRowNode, - TableCellNode, + CustomTableCellNode, ImageNode, HorizontalRuleNode, DetailsNode, SummaryNode, @@ -59,7 +60,19 @@ export function getNodesForPageEditor(): (KlassConstructor | 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; + } + }, ]; } diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index a0ea2e1eb..d925711e1 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -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 diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts index 2cc2e701b..3b431141f 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -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; diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts index 9951bfe7f..291b355e7 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts @@ -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) {