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 {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);

View File

@ -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 {

View File

@ -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,

View File

@ -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(

View File

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

View File

@ -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();