]> BookStack Code Mirror - bookstack/commitdiff
Added base node/button for details/summary
authorDan Brown <redacted>
Thu, 6 Jun 2024 13:43:50 +0000 (14:43 +0100)
committerDan Brown <redacted>
Thu, 6 Jun 2024 13:43:50 +0000 (14:43 +0100)
resources/js/wysiwyg/nodes/details.ts [new file with mode: 0644]
resources/js/wysiwyg/nodes/index.ts
resources/js/wysiwyg/ui/defaults/button-definitions.ts
resources/js/wysiwyg/ui/toolbars.ts
resources/views/pages/parts/wysiwyg-editor.blade.php

diff --git a/resources/js/wysiwyg/nodes/details.ts b/resources/js/wysiwyg/nodes/details.ts
new file mode 100644 (file)
index 0000000..a18c4d8
--- /dev/null
@@ -0,0 +1,120 @@
+import {
+    DOMConversion,
+    DOMConversionMap, DOMConversionOutput,
+    ElementNode,
+    LexicalEditor,
+    LexicalNode,
+    SerializedElementNode,
+} from 'lexical';
+import type {EditorConfig} from "lexical/LexicalEditor";
+import {el} from "../helpers";
+
+export class DetailsNode extends ElementNode {
+
+    static getType() {
+        return 'details';
+    }
+
+    static clone(node: DetailsNode) {
+        return new DetailsNode(node.__key);
+    }
+
+    createDOM(_config: EditorConfig, _editor: LexicalEditor) {
+        return el('details');
+    }
+
+    updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
+        return false;
+    }
+
+    static importDOM(): DOMConversionMap|null {
+        return {
+            details(node: HTMLElement): DOMConversion|null {
+                return {
+                    conversion: (element: HTMLElement): DOMConversionOutput|null => {
+                        return {
+                            node: new DetailsNode(),
+                        };
+                    },
+                    priority: 3,
+                };
+            },
+        };
+    }
+
+    exportJSON(): SerializedElementNode {
+        return {
+            ...super.exportJSON(),
+            type: 'details',
+            version: 1,
+        };
+    }
+
+    static importJSON(serializedNode: SerializedElementNode): DetailsNode {
+        return $createDetailsNode();
+    }
+
+}
+
+export function $createDetailsNode() {
+    return new DetailsNode();
+}
+
+export function $isDetailsNode(node: LexicalNode | null | undefined) {
+    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): DetailsNode {
+        return $createSummaryNode();
+    }
+
+}
+
+export function $createSummaryNode() {
+    return new SummaryNode();
+}
+
+export function $isSummaryNode(node: LexicalNode | null | undefined) {
+    return node instanceof SummaryNode;
+}
index 1d492a87a25d2c0ddf218d7bd74441fae3b120ca..f47575bc5f615bb25ed84c57e8978910768ecc11 100644 (file)
@@ -4,6 +4,7 @@ import {ElementNode, KlassConstructor, LexicalNode, LexicalNodeReplacement, Para
 import {CustomParagraphNode} from "./custom-paragraph";
 import {LinkNode} from "@lexical/link";
 import {ImageNode} from "./image";
+import {DetailsNode, SummaryNode} from "./details";
 
 /**
  * Load the nodes for lexical.
@@ -14,6 +15,7 @@ export function getNodesForPageEditor(): (KlassConstructor<typeof LexicalNode> |
         HeadingNode, // Todo - Create custom
         QuoteNode, // Todo - Create custom
         ImageNode,
+        DetailsNode, SummaryNode,
         CustomParagraphNode,
         {
             replace: ParagraphNode,
index 92f0cfc8120104751c1ae50cd931369c85e7601a..e549e69a2d71fac92d91b9e0581ee7a16571370e 100644 (file)
@@ -1,9 +1,9 @@
 import {EditorButtonDefinition} from "../framework/buttons";
 import {
     $createNodeSelection,
-    $createParagraphNode, $getSelection,
+    $createParagraphNode, $getRoot, $getSelection, $insertNodes,
     $isParagraphNode, $setSelection,
-    BaseSelection, FORMAT_TEXT_COMMAND,
+    BaseSelection, ElementNode, FORMAT_TEXT_COMMAND,
     LexicalNode,
     REDO_COMMAND, TextFormatType,
     UNDO_COMMAND
@@ -26,6 +26,8 @@ import {
 import {$isLinkNode, $toggleLink, LinkNode} from "@lexical/link";
 import {EditorUiContext} from "../framework/core";
 import {$isImageNode, ImageNode} from "../../nodes/image";
+import {$createDetailsNode, $isDetailsNode} from "../../nodes/details";
+import {$insertNodeToNearestRoot} from "@lexical/utils";
 
 export const undo: EditorButtonDefinition = {
     label: 'Undo',
@@ -201,3 +203,30 @@ export const image: EditorButtonDefinition = {
     }
 };
 
+export const details: EditorButtonDefinition = {
+    label: 'Insert collapsible block',
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = $getSelection();
+            const detailsNode = $createDetailsNode();
+            const selectionNodes = selection?.getNodes() || [];
+            const topLevels = selectionNodes.map(n => n.getTopLevelElement())
+                .filter(n => n !== null) as ElementNode[];
+            const uniqueTopLevels = [...new Set(topLevels)];
+
+            if (uniqueTopLevels.length > 0) {
+                uniqueTopLevels[0].insertAfter(detailsNode);
+            } else {
+                $getRoot().append(detailsNode);
+            }
+
+            for (const node of uniqueTopLevels) {
+                detailsNode.append(node);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return selectionContainsNodeType(selection, $isDetailsNode);
+    }
+}
+
index 2802b1ca7ba98c4db7e25bdcb451a30af5567e78..b5d151fc11676f97905ddb709c44a2ffd4aec913 100644 (file)
@@ -1,7 +1,7 @@
 import {EditorButton, FormatPreviewButton} from "./framework/buttons";
 import {
     blockquote, bold, code,
-    dangerCallout,
+    dangerCallout, details,
     h2, h3, h4, h5, image,
     infoCallout, italic, link, paragraph,
     redo, strikethrough, subscript,
@@ -41,5 +41,6 @@ export function getMainEditorFullToolbar(): EditorContainerUiElement {
 
         new EditorButton(link),
         new EditorButton(image),
+        new EditorButton(details),
     ]);
 }
\ No newline at end of file
index c0ceddc45d024c14bf5baa1867639d46e5169304..6414027697c33ae3479a555662de9288f843916f 100644 (file)
                 <li>Hello</li>
             </ul>
 
+            <details>
+                <summary>Collapsible details/summary block</summary>
+                <p>Inner text here</p>
+                <h4>Inner Header</h4>
+                <p>More text <strong>with bold in</strong> it</p>
+            </details>
+
             <p class="callout info">
                 Hello there, this is an info callout
             </p>