From b862f12a5072ee1429f3eab6ae3be454f4fab5c4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 28 May 2025 22:47:39 +0100 Subject: [PATCH] Lexical: Further improvements to table selection and captions - Fixed errors with selection and range handling due to captions existing. - Updated TableNode change handling to update existing DOM instead of re-creating, which avoids breaking an attached selection helper. - To support, Added function to handle node change detection and apply relevant dom updates for common properties. --- .../js/wysiwyg/lexical/core/nodes/common.ts | 33 +++++++++++++++ .../wysiwyg/lexical/table/LexicalTableNode.ts | 42 ++++++++++++------- .../table/LexicalTableSelectionHelpers.ts | 6 +-- .../lexical/table/LexicalTableUtils.ts | 3 +- .../helpers/table-selection-handler.ts | 2 +- resources/js/wysiwyg/utils/tables.ts | 19 ++++++++- 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/nodes/common.ts b/resources/js/wysiwyg/lexical/core/nodes/common.ts index eac9c8295..50d884344 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/common.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/common.ts @@ -1,5 +1,6 @@ import {sizeToPixels} from "../../../utils/dom"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; +import {elem} from "../../../../services/dom"; export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | ''; const validAlignments: CommonBlockAlignment[] = ['left', 'right', 'center', 'justify']; @@ -82,6 +83,38 @@ export function commonPropertiesDifferent(nodeA: CommonBlockInterface, nodeB: Co nodeA.__dir !== nodeB.__dir; } +export function applyCommonPropertyChanges(prevNode: CommonBlockInterface, currentNode: CommonBlockInterface, element: HTMLElement): void { + if (prevNode.__id !== currentNode.__id) { + element.setAttribute('id', currentNode.__id); + } + + if (prevNode.__alignment !== currentNode.__alignment) { + for (const alignment of validAlignments) { + element.classList.remove('align-' + alignment); + } + + if (currentNode.__alignment) { + element.classList.add('align-' + currentNode.__alignment); + } + } + + if (prevNode.__inset !== currentNode.__inset) { + if (currentNode.__inset) { + element.style.paddingLeft = `${currentNode.__inset}px`; + } else { + element.style.removeProperty('paddingLeft'); + } + } + + if (prevNode.__dir !== currentNode.__dir) { + if (currentNode.__dir) { + element.dir = currentNode.__dir; + } else { + element.removeAttribute('dir'); + } + } +} + export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void { if (node.__id) { element.setAttribute('id', node.__id); diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts index 105764be2..460223bc9 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableNode.ts @@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver'; import {getTable} from './LexicalTableSelectionHelpers'; import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import { + applyCommonPropertyChanges, commonPropertiesDifferent, deserializeCommonBlockNode, setCommonBlockPropsFromElement, updateElementWithCommonBlockProps } from "lexical/nodes/common"; import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom"; -import {getTableColumnWidths} from "../../utils/tables"; +import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables"; export type SerializedTableNode = Spread<{ colWidths: string[]; @@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode { updateElementWithCommonBlockProps(tableElement, this); 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); - } + const colgroup = buildColgroupFromTableWidths(colWidths); + if (colgroup) { tableElement.append(colgroup); } @@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode { return tableElement; } - updateDOM(_prevNode: TableNode): boolean { - return commonPropertiesDifferent(_prevNode, this) - || this.__colWidths.join(':') !== _prevNode.__colWidths.join(':') - || this.__styles.size !== _prevNode.__styles.size - || (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).join(':'))); + updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean { + applyCommonPropertyChanges(_prevNode, this, dom); + + if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) { + const existingColGroup = Array.from(dom.children).find(child => child.nodeName === 'COLGROUP'); + const newColGroup = buildColgroupFromTableWidths(this.__colWidths); + if (existingColGroup) { + existingColGroup.remove(); + } + + if (newColGroup) { + dom.prepend(newColGroup); + } + } + + if (Array.from(this.__styles.values()).join(':') !== Array.from(_prevNode.__styles.values()).join(':')) { + dom.style.cssText = ''; + for (const [name, value] of this.__styles.entries()) { + dom.style.setProperty(name, value); + } + } + + return false; } exportDOM(editor: LexicalEditor): DOMExportOutput { diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts index 448019669..6e5e5416f 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts @@ -916,14 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable { domRows.length = 0; while (currentNode != null) { - const nodeMame = currentNode.nodeName; + const nodeName = currentNode.nodeName; - if (nodeMame === 'COLGROUP') { + if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') { currentNode = currentNode.nextSibling; continue; } - if (nodeMame === 'TD' || nodeMame === 'TH') { + if (nodeName === 'TD' || nodeName === 'TH') { const elem = currentNode as HTMLElement; const cell = { elem, diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts b/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts index cdbc84658..bd807d7f9 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts @@ -35,6 +35,7 @@ import { TableRowNode, } from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; +import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode"; export function $createTableNodeWithDimensions( rowCount: number, @@ -779,7 +780,7 @@ export function $computeTableMapSkipCellCheck( return tableMap[row] === undefined || tableMap[row][column] === undefined; } - const gridChildren = grid.getChildren(); + const gridChildren = grid.getChildren().filter(node => !$isCaptionNode(node)); for (let i = 0; i < gridChildren.length; i++) { const row = gridChildren[i]; invariant( diff --git a/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts b/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts index d3d892550..c05e448f5 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts @@ -56,7 +56,7 @@ class TableSelectionHandler { tableNode, tableElement, this.editor, - false, + true, ); this.tableSelections.set(nodeKey, tableSelection); } diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts index ed947ddcd..8f4a6599f 100644 --- a/resources/js/wysiwyg/utils/tables.ts +++ b/resources/js/wysiwyg/utils/tables.ts @@ -9,7 +9,7 @@ import { } from "@lexical/table"; import {$getParentOfType} from "./nodes"; import {$getNodeFromSelection} from "./selection"; -import {formatSizeValue} from "./dom"; +import {el, formatSizeValue} from "./dom"; import {TableMap} from "./table-map"; function $getTableFromCell(cell: TableCellNode): TableNode|null { @@ -140,6 +140,23 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellN return (widths.length > index) ? widths[index] : ''; } +export function buildColgroupFromTableWidths(colWidths: string[]): HTMLElement|null { + if (colWidths.length === 0) { + return null + } + + const colgroup = el('colgroup'); + for (const width of colWidths) { + const col = el('col'); + if (width) { + col.style.width = width; + } + colgroup.append(col); + } + + return colgroup; +} + export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] { if ($isTableSelection(selection)) { const nodes = selection.getNodes();