From 5887322178c0fd95319ed104abee3bd4733c5d0d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 15 Dec 2024 18:13:49 +0000 Subject: [PATCH] Lexical: Added details toolbar Includes unwrap and toggle open actions. --- resources/icons/editor/details-toggle.svg | 1 + .../lexical/rich-text/LexicalDetailsNode.ts | 33 ++++++++++- .../js/wysiwyg/ui/defaults/buttons/objects.ts | 59 ++++++++++++++++++- .../js/wysiwyg/ui/defaults/forms/objects.ts | 34 +++++++++++ resources/js/wysiwyg/ui/defaults/modals.ts | 6 +- resources/js/wysiwyg/ui/index.ts | 7 ++- resources/js/wysiwyg/ui/toolbars.ts | 10 +++- 7 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 resources/icons/editor/details-toggle.svg diff --git a/resources/icons/editor/details-toggle.svg b/resources/icons/editor/details-toggle.svg new file mode 100644 index 000000000..37194e059 --- /dev/null +++ b/resources/icons/editor/details-toggle.svg @@ -0,0 +1 @@ + diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts index 18d471103..3c845359a 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDetailsNode.ts @@ -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}; } diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts index f9c029ff1..6612c0dc4 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/defaults/forms/objects.ts b/resources/js/wysiwyg/ui/defaults/forms/objects.ts index f00a08bb5..21d333c3a 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/objects.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/objects.ts @@ -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 diff --git a/resources/js/wysiwyg/ui/defaults/modals.ts b/resources/js/wysiwyg/ui/defaults/modals.ts index c43923778..da3859266 100644 --- a/resources/js/wysiwyg/ui/defaults/modals.ts +++ b/resources/js/wysiwyg/ui/defaults/modals.ts @@ -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 = { title: 'Table Properties', form: tableProperties, }, + details: { + title: 'Edit collapsible block', + form: details, + } }; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index 3811f44b9..40df43347 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -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); diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index 886e1394b..1230cbdd2 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -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