From b3d3b14f79552299ce558083383cf05c2f1a7d90 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 Aug 2024 18:49:17 +0100 Subject: [PATCH] Lexical: Finished off core cell properties functionality --- resources/js/wysiwyg/nodes/custom-table.ts | 90 +----------- resources/js/wysiwyg/todo.md | 2 +- .../js/wysiwyg/ui/defaults/forms/tables.ts | 23 +-- .../ui/framework/helpers/table-resizer.ts | 3 +- resources/js/wysiwyg/utils/dom.ts | 8 ++ resources/js/wysiwyg/utils/tables.ts | 134 ++++++++++++++++++ 6 files changed, 160 insertions(+), 100 deletions(-) create mode 100644 resources/js/wysiwyg/utils/tables.ts diff --git a/resources/js/wysiwyg/nodes/custom-table.ts b/resources/js/wysiwyg/nodes/custom-table.ts index 32f3ec4fa..99351d852 100644 --- a/resources/js/wysiwyg/nodes/custom-table.ts +++ b/resources/js/wysiwyg/nodes/custom-table.ts @@ -1,8 +1,9 @@ -import {SerializedTableNode, TableNode, TableRowNode} from "@lexical/table"; -import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalEditor, LexicalNode, Spread} from "lexical"; +import {SerializedTableNode, TableNode} from "@lexical/table"; +import {DOMConversion, DOMConversionMap, DOMConversionOutput, LexicalNode, Spread} from "lexical"; import {EditorConfig} from "lexical/LexicalEditor"; import {el} from "../utils/dom"; +import {getTableColumnWidths} from "../utils/tables"; export type SerializedCustomTableNode = Spread<{ id: string; @@ -111,49 +112,6 @@ export class CustomTableNode extends TableNode { } } -function getTableColumnWidths(table: HTMLTableElement): string[] { - const maxColRow = getMaxColRowFromTable(table); - - const colGroup = table.querySelector('colgroup'); - let widths: string[] = []; - if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) { - widths = extractWidthsFromRow(colGroup); - } - if (widths.filter(Boolean).length === 0 && maxColRow) { - widths = extractWidthsFromRow(maxColRow); - } - - return widths; -} - -function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement|null { - 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; - } - } - - return maxColRow; -} - -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 (width && !Number.isNaN(Number(width))) { - width = width + 'px'; - } - - return width || ''; -} - export function $createCustomTableNode(): CustomTableNode { return new CustomTableNode(); } @@ -161,45 +119,3 @@ export function $createCustomTableNode(): CustomTableNode { export function $isCustomTableNode(node: LexicalNode | null | undefined): node is CustomTableNode { 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); -} - -export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number { - const colWidths = node.getColWidths(); - if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) { - return Number(colWidths[columnIndex].replace('px', '')); - } - - // Otherwise, get from table element - const table = editor.getElementByKey(node.__key) as HTMLTableElement|null; - if (table) { - const maxColRow = getMaxColRowFromTable(table); - if (maxColRow && maxColRow.children.length > columnIndex) { - const cell = maxColRow.children[columnIndex]; - return cell.clientWidth; - } - } - - return 0; -} \ No newline at end of file diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index d925711e1..086ca1462 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -3,7 +3,7 @@ ## In progress - Table features - - Cell properties form logic + - CustomTableCellNode importDOM logic - Merge cell action - Row properties form logic - Table properties form logic diff --git a/resources/js/wysiwyg/ui/defaults/forms/tables.ts b/resources/js/wysiwyg/ui/defaults/forms/tables.ts index 291b355e7..1d637b0ee 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/tables.ts @@ -5,11 +5,11 @@ import { EditorSelectFormFieldDefinition } from "../../framework/forms"; import {EditorUiContext} from "../../framework/core"; -import {$isCustomTableCellNode, CustomTableCellNode} from "../../../nodes/custom-table-cell-node"; +import {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"; +import {$getTableCellsFromSelection, $setTableCellColumnWidth} from "../../../utils/tables"; +import {formatSizeValue} from "../../../utils/dom"; const borderStyleInput: EditorSelectFormFieldDefinition = { label: 'Border style', @@ -61,7 +61,7 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito width: '', // TODO height: styles.get('height') || '', type: cell.getTag(), - h_align: '', // TODO + h_align: cell.getFormatType(), v_align: styles.get('vertical-align') || '', border_width: styles.get('border-width') || '', border_style: styles.get('border-style') || '', @@ -74,18 +74,19 @@ export function showCellPropertiesForm(cell: CustomTableCellNode, context: Edito export const cellProperties: EditorFormDefinition = { submitText: 'Save', async action(formData, context: EditorUiContext) { - // 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); + const cells = $getTableCellsFromSelection($getSelection()); + for (const cell of cells) { + const width = formData.get('width')?.toString() || ''; + + $setTableCellColumnWidth(cell, width); cell.updateTag(formData.get('type')?.toString() || ''); + cell.setFormat((formData.get('h_align')?.toString() || '') as ElementFormatType); const styles = cell.getStyles(); - styles.set('height', formData.get('height')?.toString() || ''); + styles.set('height', formatSizeValue(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-width', formatSizeValue(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() || ''); diff --git a/resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts b/resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts index f312294c5..37f1b6f01 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/table-resizer.ts @@ -1,8 +1,9 @@ import {$getNearestNodeFromDOMNode, LexicalEditor} from "lexical"; import {MouseDragTracker, MouseDragTrackerDistance} from "./mouse-drag-tracker"; -import {$getTableColumnWidth, $setTableColumnWidth, CustomTableNode} from "../../../nodes/custom-table"; +import {CustomTableNode} from "../../../nodes/custom-table"; import {TableRowNode} from "@lexical/table"; import {el} from "../../../utils/dom"; +import {$getTableColumnWidth, $setTableColumnWidth} from "../../../utils/tables"; type MarkerDomRecord = {x: HTMLElement, y: HTMLElement}; diff --git a/resources/js/wysiwyg/utils/dom.ts b/resources/js/wysiwyg/utils/dom.ts index dc0872e89..7426ac592 100644 --- a/resources/js/wysiwyg/utils/dom.ts +++ b/resources/js/wysiwyg/utils/dom.ts @@ -21,4 +21,12 @@ export function el(tag: string, attrs: Record = {}, child export function htmlToDom(html: string): Document { const parser = new DOMParser(); return parser.parseFromString(html, 'text/html'); +} + +export function formatSizeValue(size: number | string, defaultSuffix: string = 'px'): string { + if (typeof size === 'number' || /^-?\d+$/.test(size)) { + return `${size}${defaultSuffix}`; + } + + return size; } \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts new file mode 100644 index 000000000..959c8a423 --- /dev/null +++ b/resources/js/wysiwyg/utils/tables.ts @@ -0,0 +1,134 @@ +import {BaseSelection, LexicalEditor} from "lexical"; +import {$isTableRowNode, $isTableSelection, TableRowNode} from "@lexical/table"; +import {$isCustomTableNode, CustomTableNode} from "../nodes/custom-table"; +import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell-node"; +import {$getParentOfType} from "./nodes"; +import {$getNodeFromSelection} from "./selection"; +import {formatSizeValue} from "./dom"; + +function $getTableFromCell(cell: CustomTableCellNode): CustomTableNode|null { + return $getParentOfType(cell, $isCustomTableNode) as CustomTableNode|null; +} + +export function getTableColumnWidths(table: HTMLTableElement): string[] { + const maxColRow = getMaxColRowFromTable(table); + + const colGroup = table.querySelector('colgroup'); + let widths: string[] = []; + if (colGroup && (colGroup.childElementCount === maxColRow?.childElementCount || !maxColRow)) { + widths = extractWidthsFromRow(colGroup); + } + if (widths.filter(Boolean).length === 0 && maxColRow) { + widths = extractWidthsFromRow(maxColRow); + } + + return widths; +} + +function getMaxColRowFromTable(table: HTMLTableElement): HTMLTableRowElement | null { + 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; + } + } + + return maxColRow; +} + +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 (width && !Number.isNaN(Number(width))) { + width = width + 'px'; + } + + return width || ''; +} + +export function $setTableColumnWidth(node: CustomTableNode, columnIndex: number, width: number|string): 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] = formatSizeValue(width); + node.setColWidths(colWidths); +} + +export function $getTableColumnWidth(editor: LexicalEditor, node: CustomTableNode, columnIndex: number): number { + const colWidths = node.getColWidths(); + if (colWidths.length > columnIndex && colWidths[columnIndex].endsWith('px')) { + return Number(colWidths[columnIndex].replace('px', '')); + } + + // Otherwise, get from table element + const table = editor.getElementByKey(node.__key) as HTMLTableElement | null; + if (table) { + const maxColRow = getMaxColRowFromTable(table); + if (maxColRow && maxColRow.children.length > columnIndex) { + const cell = maxColRow.children[columnIndex]; + return cell.clientWidth; + } + } + + return 0; +} + +function $getCellColumnIndex(node: CustomTableCellNode): number { + const row = node.getParent(); + if (!$isTableRowNode(row)) { + return -1; + } + + let index = 0; + const cells = row.getChildren(); + for (const cell of cells) { + let colSpan = cell.getColSpan() || 1; + index += colSpan; + if (cell.getKey() === node.getKey()) { + break; + } + } + + return index - 1; +} + +export function $setTableCellColumnWidth(cell: CustomTableCellNode, width: string): void { + const table = $getTableFromCell(cell) + const index = $getCellColumnIndex(cell); + + if (table && index >= 0) { + $setTableColumnWidth(table, index, width); + } +} + +export function $getTableCellsFromSelection(selection: BaseSelection|null): CustomTableCellNode[] { + if ($isTableSelection(selection)) { + const nodes = selection.getNodes(); + return nodes.filter(n => $isCustomTableCellNode(n)); + } + + const cell = $getNodeFromSelection(selection, $isCustomTableCellNode) as CustomTableCellNode; + return cell ? [cell] : []; +} \ No newline at end of file