]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Added details toolbar
authorDan Brown <redacted>
Sun, 15 Dec 2024 18:13:49 +0000 (18:13 +0000)
committerDan Brown <redacted>
Sun, 15 Dec 2024 18:13:49 +0000 (18:13 +0000)
Includes unwrap and toggle open actions.

resources/icons/editor/details-toggle.svg [new file with mode: 0644]
resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts
resources/js/wysiwyg/ui/defaults/buttons/objects.ts
resources/js/wysiwyg/ui/defaults/forms/objects.ts
resources/js/wysiwyg/ui/defaults/modals.ts
resources/js/wysiwyg/ui/index.ts
resources/js/wysiwyg/ui/toolbars.ts

diff --git a/resources/icons/editor/details-toggle.svg b/resources/icons/editor/details-toggle.svg
new file mode 100644 (file)
index 0000000..37194e0
--- /dev/null
@@ -0,0 +1 @@
+<svg viewbox="0 0 24 24"><path d="M8.12 19.3c.39.39 1.02.39 1.41 0L12 16.83l2.47 2.47c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41l-3.17-3.17c-.39-.39-1.02-.39-1.41 0l-3.17 3.17c-.4.38-.4 1.02-.01 1.41zm7.76-14.6c-.39-.39-1.02-.39-1.41 0L12 7.17 9.53 4.7c-.39-.39-1.02-.39-1.41 0-.39.39-.39 1.03 0 1.42l3.17 3.17c.39.39 1.02.39 1.41 0l3.17-3.17c.4-.39.4-1.03.01-1.42z"/></svg>
index 18d47110316ff8d600843747784c0190e5dc12e5..3c845359aecb38c19fe54cfbac1bb30cee0fc2ad 100644 (file)
@@ -18,6 +18,7 @@ export type SerializedDetailsNode = Spread<{
 export class DetailsNode extends ElementNode {
     __id: string = '';
     __summary: string = '';
+    __open: boolean = false;
 
     static getType() {
         return 'details';
@@ -43,11 +44,22 @@ export class DetailsNode extends ElementNode {
         return self.__summary;
     }
 
+    setOpen(open: boolean) {
+        const self = this.getWritable();
+        self.__open = open;
+    }
+
+    getOpen(): boolean {
+        const self = this.getLatest();
+        return self.__open;
+    }
+
     static clone(node: DetailsNode): DetailsNode {
         const newNode =  new DetailsNode(node.__key);
         newNode.__id = node.__id;
         newNode.__dir = node.__dir;
         newNode.__summary = node.__summary;
+        newNode.__open = node.__open;
         return newNode;
     }
 
@@ -61,17 +73,34 @@ export class DetailsNode extends ElementNode {
             el.setAttribute('dir', this.__dir);
         }
 
+        if (this.__open) {
+            el.setAttribute('open', 'true');
+        }
+
         const summary = document.createElement('summary');
         summary.textContent = this.__summary;
         summary.setAttribute('contenteditable', 'false');
+        summary.addEventListener('click', event => {
+            event.preventDefault();
+            _editor.update(() => {
+                this.select();
+            })
+        });
+
         el.append(summary);
 
         return el;
     }
 
     updateDOM(prevNode: DetailsNode, dom: HTMLElement) {
+
+        if (prevNode.__open !== this.__open) {
+            dom.toggleAttribute('open', this.__open);
+        }
+
         return prevNode.__id !== this.__id
-        || prevNode.__dir !== this.__dir;
+        || prevNode.__dir !== this.__dir
+        || prevNode.__summary !== this.__summary;
     }
 
     static importDOM(): DOMConversionMap|null {
@@ -114,6 +143,8 @@ export class DetailsNode extends ElementNode {
             elem.removeAttribute('contenteditable');
         }
 
+        element.removeAttribute('open');
+
         return {element};
     }
 
index f9c029ff14c63b1338d871eb5cbcf7531d3ac800..6612c0dc4514ce207d591fb360adfc8533edbb6b 100644 (file)
@@ -19,6 +19,9 @@ import editIcon from "@icons/edit.svg";
 import diagramIcon from "@icons/editor/diagram.svg";
 import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode";
 import detailsIcon from "@icons/editor/details.svg";
+import detailsToggleIcon from "@icons/editor/details-toggle.svg";
+import tableDeleteIcon from "@icons/editor/table-delete.svg";
+import tagIcon from "@icons/tag.svg";
 import mediaIcon from "@icons/editor/media.svg";
 import {$createDetailsNode, $isDetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 import {$isMediaNode, MediaNode} from "@lexical/rich-text/LexicalMediaNode";
@@ -29,7 +32,7 @@ import {
 } from "../../../utils/selection";
 import {$isDiagramNode, $openDrawingEditorForNode, showDiagramManagerForInsert} from "../../../utils/diagrams";
 import {$createLinkedImageNodeFromImageData, showImageManager} from "../../../utils/images";
-import {$showImageForm, $showLinkForm} from "../forms/objects";
+import {$showDetailsForm, $showImageForm, $showLinkForm} from "../forms/objects";
 import {formatCodeBlock} from "../../../utils/formats";
 
 export const link: EditorButtonDefinition = {
@@ -216,4 +219,58 @@ export const details: EditorButtonDefinition = {
     isActive(selection: BaseSelection | null): boolean {
         return $selectionContainsNodeType(selection, $isDetailsNode);
     }
+}
+
+export const detailsEditLabel: EditorButtonDefinition = {
+    label: 'Edit label',
+    icon: tagIcon,
+    action(context: EditorUiContext) {
+        context.editor.getEditorState().read(() => {
+            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
+            if ($isDetailsNode(details)) {
+                $showDetailsForm(details, context);
+            }
+        })
+    },
+    isActive(selection: BaseSelection | null): boolean {
+        return false;
+    }
+}
+
+export const detailsToggle: EditorButtonDefinition = {
+    label: 'Toggle open/closed',
+    icon: detailsToggleIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
+            if ($isDetailsNode(details)) {
+                details.setOpen(!details.getOpen());
+                context.manager.triggerLayoutUpdate();
+            }
+        })
+    },
+    isActive(selection: BaseSelection | null): boolean {
+        return false;
+    }
+}
+
+export const detailsUnwrap: EditorButtonDefinition = {
+    label: 'Unwrap',
+    icon: tableDeleteIcon,
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const details = $getNodeFromSelection($getSelection(), $isDetailsNode);
+            if ($isDetailsNode(details)) {
+                const children = details.getChildren();
+                for (const child of children) {
+                    details.insertBefore(child);
+                }
+                details.remove();
+                context.manager.triggerLayoutUpdate();
+            }
+        })
+    },
+    isActive(selection: BaseSelection | null): boolean {
+        return false;
+    }
 }
\ No newline at end of file
index f00a08bb5f5d218f40cd0235ce105a439054b6f7..21d333c3aa212789170b38ae535b9df84d35de2c 100644 (file)
@@ -19,6 +19,7 @@ import searchIcon from "@icons/search.svg";
 import {showLinkSelector} from "../../../utils/links";
 import {LinkField} from "../../framework/blocks/link-field";
 import {insertOrUpdateLink} from "../../../utils/formats";
+import {$isDetailsNode, DetailsNode} from "@lexical/rich-text/LexicalDetailsNode";
 
 export function $showImageForm(image: ImageNode, context: EditorUiContext) {
     const imageModal: EditorFormModal = context.manager.createModal('image');
@@ -262,4 +263,37 @@ export const media: EditorFormDefinition = {
             }
         },
     ],
+};
+
+export function $showDetailsForm(details: DetailsNode|null, context: EditorUiContext) {
+    const linkModal = context.manager.createModal('details');
+    if (!details) {
+        return;
+    }
+
+    linkModal.show({
+        summary: details.getSummary()
+    });
+}
+
+export const details: EditorFormDefinition = {
+    submitText: 'Save',
+    async action(formData, context: EditorUiContext) {
+        context.editor.update(() => {
+            const node = $getNodeFromSelection($getSelection(), $isDetailsNode);
+            const summary = (formData.get('summary') || '').toString().trim();
+            if ($isDetailsNode(node)) {
+                node.setSummary(summary);
+            }
+        });
+
+        return true;
+    },
+    fields: [
+        {
+            label: 'Toggle label',
+            name: 'summary',
+            type: 'text',
+        },
+    ],
 };
\ No newline at end of file
index c4392377828e3d2f791427829df58a92b06b6873..da3859266269c24901c824c0a8bdd8974db7b888 100644 (file)
@@ -1,5 +1,5 @@
 import {EditorFormModalDefinition} from "../framework/modals";
-import {image, link, media} from "./forms/objects";
+import {details, image, link, media} from "./forms/objects";
 import {source} from "./forms/controls";
 import {cellProperties, rowProperties, tableProperties} from "./forms/tables";
 
@@ -32,4 +32,8 @@ export const modals: Record<string, EditorFormModalDefinition> = {
         title: 'Table Properties',
         form: tableProperties,
     },
+    details: {
+        title: 'Edit collapsible block',
+        form: details,
+    }
 };
\ No newline at end of file
index 3811f44b9bfae2a17772bcae87a4d3f89e895139..40df433479f6b915d6cb39d507aca937d498491d 100644 (file)
@@ -1,6 +1,6 @@
 import {LexicalEditor} from "lexical";
 import {
-    getCodeToolbarContent,
+    getCodeToolbarContent, getDetailsToolbarContent,
     getImageToolbarContent,
     getLinkToolbarContent,
     getMainEditorFullToolbar, getTableToolbarContent
@@ -56,7 +56,6 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
         selector: '.editor-code-block-wrap',
         content: getCodeToolbarContent(),
     });
-
     manager.registerContextToolbar('table', {
         selector: 'td,th',
         content: getTableToolbarContent(),
@@ -64,6 +63,10 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
             return originalTarget.closest('table') as HTMLTableElement;
         }
     });
+    manager.registerContextToolbar('details', {
+        selector: 'details',
+        content: getDetailsToolbarContent(),
+    });
 
     // Register image decorator listener
     manager.registerDecoratorType('code', CodeBlockDecorator);
index 886e1394b20c447b9bfa0b4432004d45e6442bc4..1230cbdd298a4044fdfe00ddc6ffca432c6c7c99 100644 (file)
@@ -68,7 +68,7 @@ import {
 } from "./defaults/buttons/lists";
 import {
     codeBlock,
-    details,
+    details, detailsEditLabel, detailsToggle, detailsUnwrap,
     diagram, diagramManager,
     editCodeBlock,
     horizontalRule,
@@ -253,4 +253,12 @@ export function getTableToolbarContent(): EditorUiElement[] {
             new EditorButton(deleteColumn),
         ]),
     ];
+}
+
+export function getDetailsToolbarContent(): EditorUiElement[] {
+    return [
+        new EditorButton(detailsEditLabel),
+        new EditorButton(detailsToggle),
+        new EditorButton(detailsUnwrap),
+    ];
 }
\ No newline at end of file