]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added support for table caption nodes
authorDan Brown <redacted>
Wed, 22 Jan 2025 12:54:13 +0000 (12:54 +0000)
committerDan Brown <redacted>
Wed, 22 Jan 2025 12:54:13 +0000 (12:54 +0000)
Needs linking up to the table form still.

resources/js/wysiwyg/lexical/core/LexicalNode.ts
resources/js/wysiwyg/lexical/core/LexicalReconciler.ts
resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts [new file with mode: 0644]
resources/js/wysiwyg/lexical/table/LexicalTableNode.ts
resources/js/wysiwyg/nodes.ts

index a6c9b6023e534eab3f343be79124b92492031c9a..163bb8c31c85119a59802db6fb5933ce4b1d8f55 100644 (file)
@@ -1165,6 +1165,16 @@ export class LexicalNode {
   markDirty(): void {
     this.getWritable();
   }
+
+  /**
+   * Insert the DOM of this node into that of the parent.
+   * Allows this node to implement custom DOM attachment logic.
+   * Boolean result indicates if the insertion was handled by the function.
+   * A true return value prevents default insertion logic from taking place.
+   */
+  insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
+    return false;
+  }
 }
 
 function errorOnTypeKlassMismatch(
index fccf1ae23a8424c97393161e6ee86fe1c862a3b5..297e96ce018ff4e1bc0373c45380150469aa98d1 100644 (file)
@@ -171,16 +171,21 @@ function $createNode(
   }
 
   if (parentDOM !== null) {
-    if (insertDOM != null) {
-      parentDOM.insertBefore(dom, insertDOM);
-    } else {
-      // @ts-expect-error: internal field
-      const possibleLineBreak = parentDOM.__lexicalLineBreak;
 
-      if (possibleLineBreak != null) {
-        parentDOM.insertBefore(dom, possibleLineBreak);
+    const inserted = node?.insertDOMIntoParent(dom, parentDOM);
+
+    if (!inserted) {
+      if (insertDOM != null) {
+        parentDOM.insertBefore(dom, insertDOM);
       } else {
-        parentDOM.appendChild(dom);
+        // @ts-expect-error: internal field
+        const possibleLineBreak = parentDOM.__lexicalLineBreak;
+
+        if (possibleLineBreak != null) {
+          parentDOM.insertBefore(dom, possibleLineBreak);
+        } else {
+          parentDOM.appendChild(dom);
+        }
       }
     }
   }
diff --git a/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts b/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts
new file mode 100644 (file)
index 0000000..08c6870
--- /dev/null
@@ -0,0 +1,74 @@
+import {
+    DOMConversionMap,
+    DOMExportOutput,
+    EditorConfig,
+    ElementNode,
+    LexicalEditor,
+    LexicalNode,
+    SerializedElementNode
+} from "lexical";
+
+
+export class CaptionNode extends ElementNode {
+    static getType(): string {
+        return 'caption';
+    }
+
+    static clone(node: CaptionNode): CaptionNode {
+        return new CaptionNode(node.__key);
+    }
+
+    createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
+        return document.createElement('caption');
+    }
+
+    updateDOM(_prevNode: unknown, _dom: HTMLElement, _config: EditorConfig): boolean {
+        return false;
+    }
+
+    isParentRequired(): true {
+        return true;
+    }
+
+    canBeEmpty(): boolean {
+        return false;
+    }
+
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: 'caption',
+            version: 1,
+        };
+    }
+
+    insertDOMIntoParent(nodeDOM: HTMLElement, parentDOM: HTMLElement): boolean {
+        parentDOM.insertBefore(nodeDOM, parentDOM.firstChild);
+        return true;
+    }
+
+    static importJSON(serializedNode: SerializedElementNode): CaptionNode {
+        return $createCaptionNode();
+    }
+
+    static importDOM(): DOMConversionMap | null {
+        return {
+            caption: (node: Node) => ({
+                conversion(domNode: Node) {
+                    return {
+                        node: $createCaptionNode(),
+                    }
+                },
+                priority: 0,
+            }),
+        };
+    }
+}
+
+export function $createCaptionNode(): CaptionNode {
+    return new CaptionNode();
+}
+
+export function $isCaptionNode(node: LexicalNode | null | undefined): node is CaptionNode {
+    return node instanceof CaptionNode;
+}
\ No newline at end of file
index 9443747a6f716d6f9b832f5c95fb0914c41a3766..a103614753ef88b2eaaeb8d3efe1316ee4144637 100644 (file)
@@ -139,6 +139,8 @@ export class TableNode extends CommonBlockNode {
           for (const child of Array.from(tableElement.children)) {
             if (child.nodeName === 'TR') {
               tBody.append(child);
+            } else if (child.nodeName === 'CAPTION') {
+              newElement.insertBefore(child, newElement.firstChild);
             } else {
               newElement.append(child);
             }
index 8a47f322d6d943e4ad7196e10e8ae6e9d82a9b27..c1db0f0869fc3b59b110652598b4e8158c692d1c 100644 (file)
@@ -18,6 +18,7 @@ import {EditorUiContext} from "./ui/framework/core";
 import {MediaNode} from "@lexical/rich-text/LexicalMediaNode";
 import {HeadingNode} from "@lexical/rich-text/LexicalHeadingNode";
 import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode";
+import {CaptionNode} from "@lexical/table/LexicalCaptionNode";
 
 /**
  * Load the nodes for lexical.
@@ -32,6 +33,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         TableNode,
         TableRowNode,
         TableCellNode,
+        CaptionNode,
         ImageNode, // TODO - Alignment
         HorizontalRuleNode,
         DetailsNode,