]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Made summary part of details node
authorDan Brown <redacted>
Sun, 15 Dec 2024 17:11:02 +0000 (17:11 +0000)
committerDan Brown <redacted>
Sun, 15 Dec 2024 17:12:54 +0000 (17:12 +0000)
To provide more control of the summary as part of details.
To support, added a way to ignore elements during import DOM, allowing
up to read summaries when parsing details without duplicate nodes
involved.

resources/js/wysiwyg/lexical/core/LexicalNode.ts
resources/js/wysiwyg/lexical/html/index.ts
resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts
resources/js/wysiwyg/nodes.ts

index c6bc2e642eed20dab0effb19392d264c5d21ac09..a6c9b6023e534eab3f343be79124b92492031c9a 100644 (file)
@@ -142,10 +142,15 @@ export type DOMConversionMap<T extends HTMLElement = HTMLElement> = Record<
 >;
 type NodeName = string;
 
+/**
+ * Output for a DOM conversion.
+ * Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
+ * including all its children.
+ */
 export type DOMConversionOutput = {
   after?: (childLexicalNodes: Array<LexicalNode>) => Array<LexicalNode>;
   forChild?: DOMChildConversion;
-  node: null | LexicalNode | Array<LexicalNode>;
+  node: null | LexicalNode | Array<LexicalNode> | 'ignore';
 };
 
 export type DOMExportOutputMap = Map<
index 3e962ec72f704ce4a157ae2049e410f19aae4072..5c3cb6cce29feb4d7fd9c5a09eaadf576bde6f5e 100644 (file)
@@ -217,6 +217,11 @@ function $createNodesFromDOM(
   if (transformOutput !== null) {
     postTransform = transformOutput.after;
     const transformNodes = transformOutput.node;
+
+    if (transformNodes === 'ignore') {
+      return lexicalNodes;
+    }
+
     currentLexicalNode = Array.isArray(transformNodes)
       ? transformNodes[transformNodes.length - 1]
       : transformNodes;
index 178b0d9531d424e2a6f90729da7fe7fb3dd4cc56..18d47110316ff8d600843747784c0190e5dc12e5 100644 (file)
@@ -5,18 +5,19 @@ import {
     LexicalEditor,
     LexicalNode,
     SerializedElementNode, Spread,
-    EditorConfig,
+    EditorConfig, DOMExportOutput,
 } from 'lexical';
 
-import {el} from "../../utils/dom";
 import {extractDirectionFromElement} from "lexical/nodes/common";
 
 export type SerializedDetailsNode = Spread<{
     id: string;
+    summary: string;
 }, SerializedElementNode>
 
 export class DetailsNode extends ElementNode {
     __id: string = '';
+    __summary: string = '';
 
     static getType() {
         return 'details';
@@ -32,10 +33,21 @@ export class DetailsNode extends ElementNode {
         return self.__id;
     }
 
+    setSummary(summary: string) {
+        const self = this.getWritable();
+        self.__summary = summary;
+    }
+
+    getSummary(): string {
+        const self = this.getLatest();
+        return self.__summary;
+    }
+
     static clone(node: DetailsNode): DetailsNode {
         const newNode =  new DetailsNode(node.__key);
         newNode.__id = node.__id;
         newNode.__dir = node.__dir;
+        newNode.__summary = node.__summary;
         return newNode;
     }
 
@@ -49,6 +61,11 @@ export class DetailsNode extends ElementNode {
             el.setAttribute('dir', this.__dir);
         }
 
+        const summary = document.createElement('summary');
+        summary.textContent = this.__summary;
+        summary.setAttribute('contenteditable', 'false');
+        el.append(summary);
+
         return el;
     }
 
@@ -71,20 +88,42 @@ export class DetailsNode extends ElementNode {
                             node.setDirection(extractDirectionFromElement(element));
                         }
 
+                        const summaryElem = Array.from(element.children).find(e => e.nodeName === 'SUMMARY');
+                        node.setSummary(summaryElem?.textContent || '');
+
                         return {node};
                     },
                     priority: 3,
                 };
             },
+            summary(node: HTMLElement): DOMConversion|null {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+                        return {node: 'ignore'};
+                    },
+                    priority: 3,
+                };
+            },
         };
     }
 
+    exportDOM(editor: LexicalEditor): DOMExportOutput {
+        const element = this.createDOM(editor._config, editor);
+        const editable = element.querySelectorAll('[contenteditable]');
+        for (const elem of editable) {
+            elem.removeAttribute('contenteditable');
+        }
+
+        return {element};
+    }
+
     exportJSON(): SerializedDetailsNode {
         return {
             ...super.exportJSON(),
             type: 'details',
             version: 1,
             id: this.__id,
+            summary: this.__summary,
         };
     }
 
@@ -104,58 +143,3 @@ export function $createDetailsNode() {
 export function $isDetailsNode(node: LexicalNode | null | undefined): node is DetailsNode {
     return node instanceof DetailsNode;
 }
-
-export class SummaryNode extends ElementNode {
-
-    static getType() {
-        return 'summary';
-    }
-
-    static clone(node: SummaryNode) {
-        return new SummaryNode(node.__key);
-    }
-
-    createDOM(_config: EditorConfig, _editor: LexicalEditor) {
-        return el('summary');
-    }
-
-    updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
-        return false;
-    }
-
-    static importDOM(): DOMConversionMap|null {
-        return {
-            summary(node: HTMLElement): DOMConversion|null {
-                return {
-                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
-                        return {
-                            node: new SummaryNode(),
-                        };
-                    },
-                    priority: 3,
-                };
-            },
-        };
-    }
-
-    exportJSON(): SerializedElementNode {
-        return {
-            ...super.exportJSON(),
-            type: 'summary',
-            version: 1,
-        };
-    }
-
-    static importJSON(serializedNode: SerializedElementNode): SummaryNode {
-        return $createSummaryNode();
-    }
-
-}
-
-export function $createSummaryNode(): SummaryNode {
-    return new SummaryNode();
-}
-
-export function $isSummaryNode(node: LexicalNode | null | undefined): node is SummaryNode {
-    return node instanceof SummaryNode;
-}
index eb836bdce023a030a6568cd6d8a965ae8f5d3d19..8a47f322d6d943e4ad7196e10e8ae6e9d82a9b27 100644 (file)
@@ -8,7 +8,7 @@ import {
 } from "lexical";
 import {LinkNode} from "@lexical/link";
 import {ImageNode} from "@lexical/rich-text/LexicalImageNode";
-import {DetailsNode, SummaryNode} from "@lexical/rich-text/LexicalDetailsNode";
+import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 import {ListItemNode, ListNode} from "@lexical/list";
 import {TableCellNode, TableNode, TableRowNode} from "@lexical/table";
 import {HorizontalRuleNode} from "@lexical/rich-text/LexicalHorizontalRuleNode";
@@ -34,7 +34,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         TableCellNode,
         ImageNode, // TODO - Alignment
         HorizontalRuleNode,
-        DetailsNode, SummaryNode,
+        DetailsNode,
         CodeBlockNode,
         DiagramNode,
         MediaNode, // TODO - Alignment