diff --git a/resources/js/wysiwyg/nodes/custom-table.ts b/resources/js/wysiwyg/nodes/custom-table.ts new file mode 100644 index 000000000..c070e06b5 --- /dev/null +++ b/resources/js/wysiwyg/nodes/custom-table.ts @@ -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 diff --git a/resources/js/wysiwyg/nodes/index.ts b/resources/js/wysiwyg/nodes/index.ts index ea6206ac2..6b1b66e66 100644 --- a/resources/js/wysiwyg/nodes/index.ts +++ b/resources/js/wysiwyg/nodes/index.ts @@ -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 | 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(); + } + }, ]; } diff --git a/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts b/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts index c54645856..8c28953d5 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts @@ -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); }); } diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index 821c9f9cf..7f7e99a78 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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