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.
This commit is contained in:
Dan Brown 2025-05-28 22:47:39 +01:00
parent d9ea52522e
commit b862f12a50
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 84 additions and 21 deletions

View File

@ -1,5 +1,6 @@
import {sizeToPixels} from "../../../utils/dom"; import {sizeToPixels} from "../../../utils/dom";
import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import {elem} from "../../../../services/dom";
export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | ''; export type CommonBlockAlignment = 'left' | 'right' | 'center' | 'justify' | '';
const validAlignments: 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; 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 { export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
if (node.__id) { if (node.__id) {
element.setAttribute('id', node.__id); element.setAttribute('id', node.__id);

View File

@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
import {getTable} from './LexicalTableSelectionHelpers'; import {getTable} from './LexicalTableSelectionHelpers';
import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode"; import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
import { import {
applyCommonPropertyChanges,
commonPropertiesDifferent, deserializeCommonBlockNode, commonPropertiesDifferent, deserializeCommonBlockNode,
setCommonBlockPropsFromElement, setCommonBlockPropsFromElement,
updateElementWithCommonBlockProps updateElementWithCommonBlockProps
} from "lexical/nodes/common"; } from "lexical/nodes/common";
import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom"; import {el, extractStyleMapFromElement, StyleMap} from "../../utils/dom";
import {getTableColumnWidths} from "../../utils/tables"; import {buildColgroupFromTableWidths, getTableColumnWidths} from "../../utils/tables";
export type SerializedTableNode = Spread<{ export type SerializedTableNode = Spread<{
colWidths: string[]; colWidths: string[];
@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode {
updateElementWithCommonBlockProps(tableElement, this); updateElementWithCommonBlockProps(tableElement, this);
const colWidths = this.getColWidths(); const colWidths = this.getColWidths();
if (colWidths.length > 0) { const colgroup = buildColgroupFromTableWidths(colWidths);
const colgroup = el('colgroup'); if (colgroup) {
for (const width of colWidths) {
const col = el('col');
if (width) {
col.style.width = width;
}
colgroup.append(col);
}
tableElement.append(colgroup); tableElement.append(colgroup);
} }
@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode {
return tableElement; return tableElement;
} }
updateDOM(_prevNode: TableNode): boolean { updateDOM(_prevNode: TableNode, dom: HTMLElement): boolean {
return commonPropertiesDifferent(_prevNode, this) applyCommonPropertyChanges(_prevNode, this, dom);
|| this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')
|| this.__styles.size !== _prevNode.__styles.size if (this.__colWidths.join(':') !== _prevNode.__colWidths.join(':')) {
|| (Array.from(this.__styles.values()).join(':') !== (Array.from(_prevNode.__styles.values()).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 { exportDOM(editor: LexicalEditor): DOMExportOutput {

View File

@ -916,14 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable {
domRows.length = 0; domRows.length = 0;
while (currentNode != null) { while (currentNode != null) {
const nodeMame = currentNode.nodeName; const nodeName = currentNode.nodeName;
if (nodeMame === 'COLGROUP') { if (nodeName === 'COLGROUP' || nodeName === 'CAPTION') {
currentNode = currentNode.nextSibling; currentNode = currentNode.nextSibling;
continue; continue;
} }
if (nodeMame === 'TD' || nodeMame === 'TH') { if (nodeName === 'TD' || nodeName === 'TH') {
const elem = currentNode as HTMLElement; const elem = currentNode as HTMLElement;
const cell = { const cell = {
elem, elem,

View File

@ -35,6 +35,7 @@ import {
TableRowNode, TableRowNode,
} from './LexicalTableRowNode'; } from './LexicalTableRowNode';
import {$isTableSelection} from './LexicalTableSelection'; import {$isTableSelection} from './LexicalTableSelection';
import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
export function $createTableNodeWithDimensions( export function $createTableNodeWithDimensions(
rowCount: number, rowCount: number,
@ -779,7 +780,7 @@ export function $computeTableMapSkipCellCheck(
return tableMap[row] === undefined || tableMap[row][column] === undefined; 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++) { for (let i = 0; i < gridChildren.length; i++) {
const row = gridChildren[i]; const row = gridChildren[i];
invariant( invariant(

View File

@ -56,7 +56,7 @@ class TableSelectionHandler {
tableNode, tableNode,
tableElement, tableElement,
this.editor, this.editor,
false, true,
); );
this.tableSelections.set(nodeKey, tableSelection); this.tableSelections.set(nodeKey, tableSelection);
} }

View File

@ -9,7 +9,7 @@ import {
} from "@lexical/table"; } from "@lexical/table";
import {$getParentOfType} from "./nodes"; import {$getParentOfType} from "./nodes";
import {$getNodeFromSelection} from "./selection"; import {$getNodeFromSelection} from "./selection";
import {formatSizeValue} from "./dom"; import {el, formatSizeValue} from "./dom";
import {TableMap} from "./table-map"; import {TableMap} from "./table-map";
function $getTableFromCell(cell: TableCellNode): TableNode|null { function $getTableFromCell(cell: TableCellNode): TableNode|null {
@ -140,6 +140,23 @@ export function $getTableCellColumnWidth(editor: LexicalEditor, cell: TableCellN
return (widths.length > index) ? widths[index] : ''; 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[] { export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[] {
if ($isTableSelection(selection)) { if ($isTableSelection(selection)) {
const nodes = selection.getNodes(); const nodes = selection.getNodes();