]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Further improvements to table selection and captions
authorDan Brown <redacted>
Wed, 28 May 2025 21:47:39 +0000 (22:47 +0100)
committerDan Brown <redacted>
Wed, 28 May 2025 21:47:39 +0000 (22:47 +0100)
- 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.

resources/js/wysiwyg/lexical/core/nodes/common.ts
resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts
resources/js/wysiwyg/lexical/table/LexicalTableUtils.ts
resources/js/wysiwyg/ui/framework/helpers/table-selection-handler.ts
resources/js/wysiwyg/utils/tables.ts

index eac9c82959573ee8d74b5f7d1ee17e18cae05492..50d8843440c87f2b0a66eed69a4dfb66d3ff455c 100644 (file)
@@ -1,5 +1,6 @@
 import {sizeToPixels} from "../../../utils/dom";
 import {SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
 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'];
 
 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;
 }
 
         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);
 export function updateElementWithCommonBlockProps(element: HTMLElement, node: CommonBlockInterface): void {
     if (node.__id) {
         element.setAttribute('id', node.__id);
index 105764be2f0a3b6b30e76495ca006492ca1b8771..460223bc9ce9dc54d4b56d7c64b8e916ad94e24a 100644 (file)
@@ -30,12 +30,13 @@ import {TableDOMCell, TableDOMTable} from './LexicalTableObserver';
 import {getTable} from './LexicalTableSelectionHelpers';
 import {CommonBlockNode, copyCommonBlockProperties, SerializedCommonBlockNode} from "lexical/nodes/CommonBlockNode";
 import {
 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";
   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[];
 
 export type SerializedTableNode = Spread<{
   colWidths: string[];
@@ -98,15 +99,8 @@ export class TableNode extends CommonBlockNode {
     updateElementWithCommonBlockProps(tableElement, this);
 
     const colWidths = this.getColWidths();
     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);
     }
 
       tableElement.append(colgroup);
     }
 
@@ -117,11 +111,29 @@ export class TableNode extends CommonBlockNode {
     return tableElement;
   }
 
     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 {
   }
 
   exportDOM(editor: LexicalEditor): DOMExportOutput {
index 44801966996cb1e14c4dc81a8183e6213b3c3dd9..6e5e5416fa03f7fe68b19c5930f4d5bd00aaa7b8 100644 (file)
@@ -916,14 +916,14 @@ export function getTable(tableElement: HTMLElement): TableDOMTable {
   domRows.length = 0;
 
   while (currentNode != null) {
   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;
     }
 
       currentNode = currentNode.nextSibling;
       continue;
     }
 
-    if (nodeMame === 'TD' || nodeMame === 'TH') {
+    if (nodeName === 'TD' || nodeName === 'TH') {
       const elem = currentNode as HTMLElement;
       const cell = {
         elem,
       const elem = currentNode as HTMLElement;
       const cell = {
         elem,
index cdbc846584d2a0d20bc5edcae4d773c9214c77e5..bd807d7f993d9cb44f7d25697bc717fff3621eb9 100644 (file)
@@ -35,6 +35,7 @@ import {
   TableRowNode,
 } from './LexicalTableRowNode';
 import {$isTableSelection} from './LexicalTableSelection';
   TableRowNode,
 } from './LexicalTableRowNode';
 import {$isTableSelection} from './LexicalTableSelection';
+import {$isCaptionNode} from "@lexical/table/LexicalCaptionNode";
 
 export function $createTableNodeWithDimensions(
   rowCount: number,
 
 export function $createTableNodeWithDimensions(
   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++) {
     const row = gridChildren[i];
     invariant(
   for (let i = 0; i < gridChildren.length; i++) {
     const row = gridChildren[i];
     invariant(
index d3d8925505f8a71074b4138c35caec38c51b611e..c05e448f5a08f8ba890cce8344d17ead23be7e87 100644 (file)
@@ -56,7 +56,7 @@ class TableSelectionHandler {
                 tableNode,
                 tableElement,
                 this.editor,
                 tableNode,
                 tableElement,
                 this.editor,
-                false,
+                true,
             );
             this.tableSelections.set(nodeKey, tableSelection);
         }
             );
             this.tableSelections.set(nodeKey, tableSelection);
         }
index ed947ddcdcbce26fa1e4454a805e1a721f1d7816..8f4a6599f9a17d760fceb36d624b5a97d95603c6 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from "@lexical/table";
 import {$getParentOfType} from "./nodes";
 import {$getNodeFromSelection} from "./selection";
 } 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 {
 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] : '';
 }
 
     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();
 export function $getTableCellsFromSelection(selection: BaseSelection|null): TableCellNode[]  {
     if ($isTableSelection(selection)) {
         const nodes = selection.getNodes();