From b6182875858d637f19f49a2a98c61887a0c5c5d3 Mon Sep 17 00:00:00 2001 From: Dan Brown <ssddanbrown@googlemail.com> Date: Sun, 21 Jul 2024 15:11:24 +0100 Subject: [PATCH] Lexical: Added table toolbar, organised button code --- .../icons/editor/table-delete-column.svg | 1 + resources/icons/editor/table-delete-row.svg | 1 + resources/icons/editor/table-delete.svg | 1 + .../editor/table-insert-column-after.svg | 1 + .../editor/table-insert-column-before.svg | 1 + .../icons/editor/table-insert-row-above.svg | 1 + .../icons/editor/table-insert-row-below.svg | 1 + resources/js/wysiwyg/helpers.ts | 17 +- resources/js/wysiwyg/todo.md | 12 +- .../wysiwyg/ui/defaults/button-definitions.ts | 528 ------------------ .../wysiwyg/ui/defaults/buttons/alignments.ts | 61 ++ .../ui/defaults/buttons/block-formats.ts | 85 +++ .../wysiwyg/ui/defaults/buttons/controls.ts | 81 +++ .../ui/defaults/buttons/inline-formats.ts | 56 ++ .../js/wysiwyg/ui/defaults/buttons/lists.ts | 35 ++ .../js/wysiwyg/ui/defaults/buttons/objects.ts | 215 +++++++ .../js/wysiwyg/ui/defaults/buttons/tables.ts | 122 ++++ resources/js/wysiwyg/ui/index.ts | 10 +- resources/js/wysiwyg/ui/toolbars.ts | 74 ++- 19 files changed, 756 insertions(+), 547 deletions(-) create mode 100644 resources/icons/editor/table-delete-column.svg create mode 100644 resources/icons/editor/table-delete-row.svg create mode 100644 resources/icons/editor/table-delete.svg create mode 100644 resources/icons/editor/table-insert-column-after.svg create mode 100644 resources/icons/editor/table-insert-column-before.svg create mode 100644 resources/icons/editor/table-insert-row-above.svg create mode 100644 resources/icons/editor/table-insert-row-below.svg delete mode 100644 resources/js/wysiwyg/ui/defaults/button-definitions.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/alignments.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/controls.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/lists.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/objects.ts create mode 100644 resources/js/wysiwyg/ui/defaults/buttons/tables.ts diff --git a/resources/icons/editor/table-delete-column.svg b/resources/icons/editor/table-delete-column.svg new file mode 100644 index 000000000..428fc29a6 --- /dev/null +++ b/resources/icons/editor/table-delete-column.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14c1.1 0 2 .9 2 2zm-2 0V5h-4v2.2h-2V5h-2v2.2H9V5H5v14h4v-2.1h2V19h2v-2.1h2V19Z"/><path d="M14.829 10.585 13.415 12l1.414 1.414c.943.943-.472 2.357-1.414 1.414L12 13.414l-1.414 1.414c-.944.944-2.358-.47-1.414-1.414L10.586 12l-1.414-1.415c-.943-.942.471-2.357 1.414-1.414L12 10.585l1.344-1.343c1.111-1.112 2.2.627 1.485 1.343z" style="fill-rule:nonzero"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-delete-row.svg b/resources/icons/editor/table-delete-row.svg new file mode 100644 index 000000000..ee2f8a00d --- /dev/null +++ b/resources/icons/editor/table-delete-row.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14v-4h-2.2v-2H19v-2h-2.2V9H19V5H5v4h2.1v2H5v2h2.1v2H5Z"/><path d="M13.415 14.829 12 13.415l-1.414 1.414c-.943.943-2.357-.472-1.414-1.414L10.586 12l-1.414-1.414c-.944-.944.47-2.358 1.414-1.414L12 10.586l1.415-1.414c.942-.943 2.357.471 1.414 1.414L13.415 12l1.343 1.344c1.112 1.111-.627 2.2-1.343 1.485z" style="fill-rule:nonzero"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-delete.svg b/resources/icons/editor/table-delete.svg new file mode 100644 index 000000000..412cf0732 --- /dev/null +++ b/resources/icons/editor/table-delete.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 21a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14c0 1.1-.9 2-2 2zm0-2h14V5H5v14z"/><path d="m13.711 15.423-1.71-1.712-1.712 1.712c-1.14 1.14-2.852-.57-1.71-1.712l1.71-1.71-1.71-1.712c-1.143-1.142.568-2.853 1.71-1.71L12 10.288l1.711-1.71c1.141-1.142 2.852.57 1.712 1.71L13.71 12l1.626 1.626c1.345 1.345-.76 2.663-1.626 1.797z" style="fill-rule:nonzero;stroke-width:1.20992"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-insert-column-after.svg b/resources/icons/editor/table-insert-column-after.svg new file mode 100644 index 000000000..75abd9a85 --- /dev/null +++ b/resources/icons/editor/table-insert-column-after.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 5h-5v14h5c1.235 0 1.234 2 0 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11c1.229 0 1.236 2 0 2zm-7 6V5H5v6zm0 8v-6H5v6zm11.076-6h-2v2c0 1.333-2 1.333-2 0v-2h-2c-1.335 0-1.335-2 0-2h2V9c0-1.333 2-1.333 2 0v2h1.9c1.572 0 1.113 2 .1 2z"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-insert-column-before.svg b/resources/icons/editor/table-insert-column-before.svg new file mode 100644 index 000000000..5bb38cd29 --- /dev/null +++ b/resources/icons/editor/table-insert-column-before.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 19h5V5H8C6.764 5 6.766 3 8 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H8c-1.229 0-1.236-2 0-2zm7-6v6h4v-6zm0-8v6h4V5ZM3.924 11h2V9c0-1.333 2-1.333 2 0v2h2c1.335 0 1.335 2 0 2h-2v2c0 1.333-2 1.333-2 0v-2h-1.9c-1.572 0-1.113-2-.1-2z"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-insert-row-above.svg b/resources/icons/editor/table-insert-row-above.svg new file mode 100644 index 000000000..df951485a --- /dev/null +++ b/resources/icons/editor/table-insert-row-above.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 8v5h14V8c0-1.235 2-1.234 2 0v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8C3 6.77 5 6.764 5 8zm6 7H5v4h6zm8 0h-6v4h6zM13 3.924v2h2c1.333 0 1.333 2 0 2h-2v2c0 1.335-2 1.335-2 0v-2H9c-1.333 0-1.333-2 0-2h2v-1.9c0-1.572 2-1.113 2-.1z"/></svg> \ No newline at end of file diff --git a/resources/icons/editor/table-insert-row-below.svg b/resources/icons/editor/table-insert-row-below.svg new file mode 100644 index 000000000..b2af77592 --- /dev/null +++ b/resources/icons/editor/table-insert-row-below.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 16v-5H5v5c0 1.235-2 1.234-2 0V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v11c0 1.229-2 1.236-2 0zm-6-7h6V5h-6zM5 9h6V5H5Zm6 11.076v-2H9c-1.333 0-1.333-2 0-2h2v-2c0-1.335 2-1.335 2 0v2h2c1.333 0 1.333 2 0 2h-2v1.9c0 1.572-2 1.113-2 .1z"/></svg> \ No newline at end of file diff --git a/resources/js/wysiwyg/helpers.ts b/resources/js/wysiwyg/helpers.ts index a7c3e4453..6a55c429c 100644 --- a/resources/js/wysiwyg/helpers.ts +++ b/resources/js/wysiwyg/helpers.ts @@ -44,10 +44,19 @@ export function $getNodeFromSelection(selection: BaseSelection|null, matcher: Le return node; } - for (const parent of node.getParents()) { - if (matcher(parent)) { - return parent; - } + const matchedParent = $getParentOfType(node, matcher); + if (matchedParent) { + return matchedParent; + } + } + + return null; +} + +export function $getParentOfType(node: LexicalNode, matcher: LexicalNodeMatcher): LexicalNode|null { + for (const parent of node.getParents()) { + if (matcher(parent)) { + return parent; } } diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md index 9950254df..7b9588194 100644 --- a/resources/js/wysiwyg/todo.md +++ b/resources/js/wysiwyg/todo.md @@ -2,12 +2,22 @@ ## In progress +- Add Type: Video/media/embed + - TinyMce media embed supported: + - iframe + - embed + - object + - video - Can take sources + - audio - Can take sources + - Pretty much all attributes look like they were supported. + - Core old logic seen here: https://github.com/tinymce/tinymce/blob/main/modules/tinymce/src/plugins/media/main/ts/core/DataToHtml.ts + - Copy/store attributes on node based on allow list? + - width, height, src, controls, etc... Take valid values from MDN ## Main Todo - Alignments: Use existing classes for blocks - Alignments: Handle inline block content (image, video) -- Add Type: Video/media/embed - Table features - Image paste upload - Keyboard shortcuts support diff --git a/resources/js/wysiwyg/ui/defaults/button-definitions.ts b/resources/js/wysiwyg/ui/defaults/button-definitions.ts deleted file mode 100644 index 5316dacf7..000000000 --- a/resources/js/wysiwyg/ui/defaults/button-definitions.ts +++ /dev/null @@ -1,528 +0,0 @@ -import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons"; -import { - $createNodeSelection, - $createParagraphNode, - $createTextNode, - $getRoot, - $getSelection, - $isParagraphNode, - $isTextNode, - $setSelection, - BaseSelection, - CAN_REDO_COMMAND, - CAN_UNDO_COMMAND, - COMMAND_PRIORITY_LOW, - ElementFormatType, - ElementNode, - FORMAT_TEXT_COMMAND, - LexicalNode, - REDO_COMMAND, - TextFormatType, - UNDO_COMMAND -} from "lexical"; -import { - $getBlockElementNodesInSelection, - $getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsElementFormat, - $selectionContainsNodeType, - $selectionContainsTextFormat, - $toggleSelectionBlockNodeType -} from "../../helpers"; -import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout"; -import { - $createHeadingNode, - $createQuoteNode, - $isHeadingNode, - $isQuoteNode, - HeadingNode, - HeadingTagType -} from "@lexical/rich-text"; -import {$isLinkNode, LinkNode} from "@lexical/link"; -import {EditorUiContext} from "../framework/core"; -import {$isImageNode, ImageNode} from "../../nodes/image"; -import {$createDetailsNode, $isDetailsNode} from "../../nodes/details"; -import {getEditorContentAsHtml} from "../../actions"; -import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list"; -import undoIcon from "@icons/editor/undo.svg"; -import redoIcon from "@icons/editor/redo.svg"; -import boldIcon from "@icons/editor/bold.svg"; -import italicIcon from "@icons/editor/italic.svg"; -import underlinedIcon from "@icons/editor/underlined.svg"; -import textColorIcon from "@icons/editor/text-color.svg"; -import highlightIcon from "@icons/editor/highlighter.svg"; -import strikethroughIcon from "@icons/editor/strikethrough.svg"; -import superscriptIcon from "@icons/editor/superscript.svg"; -import subscriptIcon from "@icons/editor/subscript.svg"; -import codeIcon from "@icons/editor/code.svg"; -import formatClearIcon from "@icons/editor/format-clear.svg"; -import alignLeftIcon from "@icons/editor/align-left.svg"; -import alignCenterIcon from "@icons/editor/align-center.svg"; -import alignRightIcon from "@icons/editor/align-right.svg"; -import alignJustifyIcon from "@icons/editor/align-justify.svg"; -import listBulletIcon from "@icons/editor/list-bullet.svg"; -import listNumberedIcon from "@icons/editor/list-numbered.svg"; -import listCheckIcon from "@icons/editor/list-check.svg"; -import linkIcon from "@icons/editor/link.svg"; -import unlinkIcon from "@icons/editor/unlink.svg"; -import tableIcon from "@icons/editor/table.svg"; -import imageIcon from "@icons/editor/image.svg"; -import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"; -import codeBlockIcon from "@icons/editor/code-block.svg"; -import diagramIcon from "@icons/editor/diagram.svg"; -import detailsIcon from "@icons/editor/details.svg"; -import sourceIcon from "@icons/editor/source-view.svg"; -import fullscreenIcon from "@icons/editor/fullscreen.svg"; -import editIcon from "@icons/edit.svg"; -import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../nodes/horizontal-rule"; -import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../nodes/code-block"; -import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../nodes/diagram"; - -export const undo: EditorButtonDefinition = { - label: 'Undo', - icon: undoIcon, - action(context: EditorUiContext) { - context.editor.dispatchCommand(UNDO_COMMAND, undefined); - }, - isActive(selection: BaseSelection|null): boolean { - return false; - }, - setup(context: EditorUiContext, button: EditorButton) { - button.toggleDisabled(true); - - context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => { - button.toggleDisabled(!payload) - return false; - }, COMMAND_PRIORITY_LOW); - } -} - -export const redo: EditorButtonDefinition = { - label: 'Redo', - icon: redoIcon, - action(context: EditorUiContext) { - context.editor.dispatchCommand(REDO_COMMAND, undefined); - }, - isActive(selection: BaseSelection|null): boolean { - return false; - }, - setup(context: EditorUiContext, button: EditorButton) { - button.toggleDisabled(true); - - context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => { - button.toggleDisabled(!payload) - return false; - }, COMMAND_PRIORITY_LOW); - } -} - -function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition { - return { - label: `${name} Callout`, - action(context: EditorUiContext) { - context.editor.update(() => { - $toggleSelectionBlockNodeType( - (node) => $isCalloutNodeOfCategory(node, category), - () => $createCalloutNode(category), - ) - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category)); - } - }; -} - -export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info'); -export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger'); -export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning'); -export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success'); - -const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => { - return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag; -}; - -function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition { - return { - label: name, - action(context: EditorUiContext) { - context.editor.update(() => { - $toggleSelectionBlockNodeType( - (node) => isHeaderNodeOfTag(node, tag), - () => $createHeadingNode(tag), - ) - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag)); - } - }; -} - -export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header'); -export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header'); -export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header'); -export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header'); - -export const blockquote: EditorButtonDefinition = { - label: 'Blockquote', - action(context: EditorUiContext) { - context.editor.update(() => { - $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode); - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isQuoteNode); - } -}; - -export const paragraph: EditorButtonDefinition = { - label: 'Paragraph', - action(context: EditorUiContext) { - context.editor.update(() => { - $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode); - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isParagraphNode); - } -} - -function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition { - return { - label: label, - icon, - action(context: EditorUiContext) { - context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsTextFormat(selection, format); - } - }; -} - -export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon); -export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon); -export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon); -export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon}; -export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon}; - -export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon); -export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon); -export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon); -export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon); -export const clearFormating: EditorButtonDefinition = { - label: 'Clear formatting', - icon: formatClearIcon, - action(context: EditorUiContext) { - context.editor.update(() => { - const selection = $getSelection(); - for (const node of selection?.getNodes() || []) { - if ($isTextNode(node)) { - node.setFormat(0); - node.setStyle(''); - } - } - }); - }, - isActive() { - return false; - } -}; - -function setAlignmentForSection(alignment: ElementFormatType): void { - const selection = $getSelection(); - const elements = $getBlockElementNodesInSelection(selection); - for (const node of elements) { - node.setFormat(alignment); - } -} - -export const alignLeft: EditorButtonDefinition = { - label: 'Align left', - icon: alignLeftIcon, - action(context: EditorUiContext) { - context.editor.update(() => setAlignmentForSection('left')); - }, - isActive(selection: BaseSelection|null) { - return $selectionContainsElementFormat(selection, 'left'); - } -}; - -export const alignCenter: EditorButtonDefinition = { - label: 'Align center', - icon: alignCenterIcon, - action(context: EditorUiContext) { - context.editor.update(() => setAlignmentForSection('center')); - }, - isActive(selection: BaseSelection|null) { - return $selectionContainsElementFormat(selection, 'center'); - } -}; - -export const alignRight: EditorButtonDefinition = { - label: 'Align right', - icon: alignRightIcon, - action(context: EditorUiContext) { - context.editor.update(() => setAlignmentForSection('right')); - }, - isActive(selection: BaseSelection|null) { - return $selectionContainsElementFormat(selection, 'right'); - } -}; - -export const alignJustify: EditorButtonDefinition = { - label: 'Align justify', - icon: alignJustifyIcon, - action(context: EditorUiContext) { - context.editor.update(() => setAlignmentForSection('justify')); - }, - isActive(selection: BaseSelection|null) { - return $selectionContainsElementFormat(selection, 'justify'); - } -}; - - -function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition { - return { - label, - icon, - action(context: EditorUiContext) { - context.editor.getEditorState().read(() => { - const selection = $getSelection(); - if (this.isActive(selection, context)) { - removeList(context.editor); - } else { - insertList(context.editor, type); - } - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => { - return $isListNode(node) && (node as ListNode).getListType() === type; - }); - } - }; -} - -export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon); -export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon); -export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon); - - -export const link: EditorButtonDefinition = { - label: 'Insert/edit link', - icon: linkIcon, - action(context: EditorUiContext) { - const linkModal = context.manager.createModal('link'); - context.editor.getEditorState().read(() => { - const selection = $getSelection(); - const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; - - let formDefaults = {}; - if (selectedLink) { - formDefaults = { - url: selectedLink.getURL(), - text: selectedLink.getTextContent(), - title: selectedLink.getTitle(), - target: selectedLink.getTarget(), - } - - context.editor.update(() => { - const selection = $createNodeSelection(); - selection.add(selectedLink.getKey()); - $setSelection(selection); - }); - } - - linkModal.show(formDefaults); - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isLinkNode); - } -}; - -export const unlink: EditorButtonDefinition = { - label: 'Remove link', - icon: unlinkIcon, - action(context: EditorUiContext) { - context.editor.update(() => { - const selection = context.lastSelection; - const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; - const selectionPoints = selection?.getStartEndPoints(); - - if (selectedLink) { - const newNode = $createTextNode(selectedLink.getTextContent()); - selectedLink.replace(newNode); - if (selectionPoints?.length === 2) { - newNode.select(selectionPoints[0].offset, selectionPoints[1].offset); - } else { - newNode.select(); - } - } - }); - }, - isActive(selection: BaseSelection|null): boolean { - return false; - } -}; - -export const table: EditorBasicButtonDefinition = { - label: 'Table', - icon: tableIcon, -}; - -export const image: EditorButtonDefinition = { - label: 'Insert/Edit Image', - icon: imageIcon, - action(context: EditorUiContext) { - const imageModal = context.manager.createModal('image'); - const selection = context.lastSelection; - const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null; - - context.editor.getEditorState().read(() => { - let formDefaults = {}; - if (selectedImage) { - formDefaults = { - src: selectedImage.getSrc(), - alt: selectedImage.getAltText(), - height: selectedImage.getHeight(), - width: selectedImage.getWidth(), - } - - context.editor.update(() => { - const selection = $createNodeSelection(); - selection.add(selectedImage.getKey()); - $setSelection(selection); - }); - } - - imageModal.show(formDefaults); - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isImageNode); - } -}; - -export const horizontalRule: EditorButtonDefinition = { - label: 'Insert horizontal line', - icon: horizontalRuleIcon, - action(context: EditorUiContext) { - context.editor.update(() => { - $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false); - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isHorizontalRuleNode); - } -}; - -export const codeBlock: EditorButtonDefinition = { - label: 'Insert code block', - icon: codeBlockIcon, - action(context: EditorUiContext) { - context.editor.getEditorState().read(() => { - const selection = $getSelection(); - const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null); - if (codeBlock === null) { - context.editor.update(() => { - const codeBlock = $createCodeBlockNode(); - codeBlock.setCode(selection?.getTextContent() || ''); - $insertNewBlockNodeAtSelection(codeBlock, true); - $openCodeEditorForNode(context.editor, codeBlock); - codeBlock.selectStart(); - }); - } else { - $openCodeEditorForNode(context.editor, codeBlock); - } - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isCodeBlockNode); - } -}; - -export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, { - label: 'Edit code block', - icon: editIcon, -}); - -export const diagram: EditorButtonDefinition = { - label: 'Insert/edit drawing', - icon: diagramIcon, - action(context: EditorUiContext) { - context.editor.getEditorState().read(() => { - const selection = $getSelection(); - const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null); - if (diagramNode === null) { - context.editor.update(() => { - const diagram = $createDiagramNode(); - $insertNewBlockNodeAtSelection(diagram, true); - $openDrawingEditorForNode(context, diagram); - diagram.selectStart(); - }); - } else { - $openDrawingEditorForNode(context, diagramNode); - } - }); - }, - isActive(selection: BaseSelection|null): boolean { - return $selectionContainsNodeType(selection, $isDiagramNode); - } -}; - - -export const details: EditorButtonDefinition = { - label: 'Insert collapsible block', - icon: detailsIcon, - 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); - } -} - -export const source: EditorButtonDefinition = { - label: 'Source code', - icon: sourceIcon, - async action(context: EditorUiContext) { - const modal = context.manager.createModal('source'); - const source = await getEditorContentAsHtml(context.editor); - modal.show({source}); - }, - isActive() { - return false; - } -}; - -export const fullscreen: EditorButtonDefinition = { - label: 'Fullscreen', - icon: fullscreenIcon, - async action(context: EditorUiContext, button: EditorButton) { - const isFullScreen = context.containerDOM.classList.contains('fullscreen'); - context.containerDOM.classList.toggle('fullscreen', !isFullScreen); - (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen); - button.setActiveState(!isFullScreen); - }, - isActive(selection, context: EditorUiContext) { - return context.containerDOM.classList.contains('fullscreen'); - } -}; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts b/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts new file mode 100644 index 000000000..2b441e5da --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/alignments.ts @@ -0,0 +1,61 @@ +import {$getSelection, BaseSelection, ElementFormatType} from "lexical"; +import {$getBlockElementNodesInSelection, $selectionContainsElementFormat} from "../../../helpers"; +import {EditorButtonDefinition} from "../../framework/buttons"; +import alignLeftIcon from "@icons/editor/align-left.svg"; +import {EditorUiContext} from "../../framework/core"; +import alignCenterIcon from "@icons/editor/align-center.svg"; +import alignRightIcon from "@icons/editor/align-right.svg"; +import alignJustifyIcon from "@icons/editor/align-justify.svg"; + + +function setAlignmentForSection(alignment: ElementFormatType): void { + const selection = $getSelection(); + const elements = $getBlockElementNodesInSelection(selection); + for (const node of elements) { + node.setFormat(alignment); + } +} + +export const alignLeft: EditorButtonDefinition = { + label: 'Align left', + icon: alignLeftIcon, + action(context: EditorUiContext) { + context.editor.update(() => setAlignmentForSection('left')); + }, + isActive(selection: BaseSelection|null) { + return $selectionContainsElementFormat(selection, 'left'); + } +}; + +export const alignCenter: EditorButtonDefinition = { + label: 'Align center', + icon: alignCenterIcon, + action(context: EditorUiContext) { + context.editor.update(() => setAlignmentForSection('center')); + }, + isActive(selection: BaseSelection|null) { + return $selectionContainsElementFormat(selection, 'center'); + } +}; + +export const alignRight: EditorButtonDefinition = { + label: 'Align right', + icon: alignRightIcon, + action(context: EditorUiContext) { + context.editor.update(() => setAlignmentForSection('right')); + }, + isActive(selection: BaseSelection|null) { + return $selectionContainsElementFormat(selection, 'right'); + } +}; + +export const alignJustify: EditorButtonDefinition = { + label: 'Align justify', + icon: alignJustifyIcon, + action(context: EditorUiContext) { + context.editor.update(() => setAlignmentForSection('justify')); + }, + isActive(selection: BaseSelection|null) { + return $selectionContainsElementFormat(selection, 'justify'); + } +}; diff --git a/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts new file mode 100644 index 000000000..0eb07ecf1 --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/block-formats.ts @@ -0,0 +1,85 @@ +import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../../nodes/callout"; +import {EditorButtonDefinition} from "../../framework/buttons"; +import {EditorUiContext} from "../../framework/core"; +import {$selectionContainsNodeType, $toggleSelectionBlockNodeType} from "../../../helpers"; +import {$createParagraphNode, $isParagraphNode, BaseSelection, LexicalNode} from "lexical"; +import { + $createHeadingNode, + $createQuoteNode, + $isHeadingNode, + $isQuoteNode, + HeadingNode, + HeadingTagType +} from "@lexical/rich-text"; + +function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition { + return { + label: `${name} Callout`, + action(context: EditorUiContext) { + context.editor.update(() => { + $toggleSelectionBlockNodeType( + (node) => $isCalloutNodeOfCategory(node, category), + () => $createCalloutNode(category), + ) + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category)); + } + }; +} + +export const infoCallout: EditorButtonDefinition = buildCalloutButton('info', 'Info'); +export const dangerCallout: EditorButtonDefinition = buildCalloutButton('danger', 'Danger'); +export const warningCallout: EditorButtonDefinition = buildCalloutButton('warning', 'Warning'); +export const successCallout: EditorButtonDefinition = buildCalloutButton('success', 'Success'); + +const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => { + return $isHeadingNode(node) && (node as HeadingNode).getTag() === tag; +}; + +function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition { + return { + label: name, + action(context: EditorUiContext) { + context.editor.update(() => { + $toggleSelectionBlockNodeType( + (node) => isHeaderNodeOfTag(node, tag), + () => $createHeadingNode(tag), + ) + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag)); + } + }; +} + +export const h2: EditorButtonDefinition = buildHeaderButton('h2', 'Large Header'); +export const h3: EditorButtonDefinition = buildHeaderButton('h3', 'Medium Header'); +export const h4: EditorButtonDefinition = buildHeaderButton('h4', 'Small Header'); +export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header'); + +export const blockquote: EditorButtonDefinition = { + label: 'Blockquote', + action(context: EditorUiContext) { + context.editor.update(() => { + $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode); + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isQuoteNode); + } +}; + +export const paragraph: EditorButtonDefinition = { + label: 'Paragraph', + action(context: EditorUiContext) { + context.editor.update(() => { + $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode); + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isParagraphNode); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/buttons/controls.ts b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts new file mode 100644 index 000000000..ad69d69d1 --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/controls.ts @@ -0,0 +1,81 @@ +import {EditorButton, EditorButtonDefinition} from "../../framework/buttons"; +import undoIcon from "@icons/editor/undo.svg"; +import {EditorUiContext} from "../../framework/core"; +import { + BaseSelection, + CAN_REDO_COMMAND, + CAN_UNDO_COMMAND, + COMMAND_PRIORITY_LOW, + REDO_COMMAND, + UNDO_COMMAND +} from "lexical"; +import redoIcon from "@icons/editor/redo.svg"; +import sourceIcon from "@icons/editor/source-view.svg"; +import {getEditorContentAsHtml} from "../../../actions"; +import fullscreenIcon from "@icons/editor/fullscreen.svg"; + +export const undo: EditorButtonDefinition = { + label: 'Undo', + icon: undoIcon, + action(context: EditorUiContext) { + context.editor.dispatchCommand(UNDO_COMMAND, undefined); + }, + isActive(selection: BaseSelection|null): boolean { + return false; + }, + setup(context: EditorUiContext, button: EditorButton) { + button.toggleDisabled(true); + + context.editor.registerCommand(CAN_UNDO_COMMAND, (payload: boolean): boolean => { + button.toggleDisabled(!payload) + return false; + }, COMMAND_PRIORITY_LOW); + } +} + +export const redo: EditorButtonDefinition = { + label: 'Redo', + icon: redoIcon, + action(context: EditorUiContext) { + context.editor.dispatchCommand(REDO_COMMAND, undefined); + }, + isActive(selection: BaseSelection|null): boolean { + return false; + }, + setup(context: EditorUiContext, button: EditorButton) { + button.toggleDisabled(true); + + context.editor.registerCommand(CAN_REDO_COMMAND, (payload: boolean): boolean => { + button.toggleDisabled(!payload) + return false; + }, COMMAND_PRIORITY_LOW); + } +} + + +export const source: EditorButtonDefinition = { + label: 'Source code', + icon: sourceIcon, + async action(context: EditorUiContext) { + const modal = context.manager.createModal('source'); + const source = await getEditorContentAsHtml(context.editor); + modal.show({source}); + }, + isActive() { + return false; + } +}; + +export const fullscreen: EditorButtonDefinition = { + label: 'Fullscreen', + icon: fullscreenIcon, + async action(context: EditorUiContext, button: EditorButton) { + const isFullScreen = context.containerDOM.classList.contains('fullscreen'); + context.containerDOM.classList.toggle('fullscreen', !isFullScreen); + (context.containerDOM.closest('body') as HTMLElement).classList.toggle('editor-is-fullscreen', !isFullScreen); + button.setActiveState(!isFullScreen); + }, + isActive(selection, context: EditorUiContext) { + return context.containerDOM.classList.contains('fullscreen'); + } +}; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts new file mode 100644 index 000000000..d04f72a2e --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/inline-formats.ts @@ -0,0 +1,56 @@ +import {$getSelection, $isTextNode, BaseSelection, FORMAT_TEXT_COMMAND, TextFormatType} from "lexical"; +import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons"; +import {EditorUiContext} from "../../framework/core"; +import {$selectionContainsTextFormat} from "../../../helpers"; +import boldIcon from "@icons/editor/bold.svg"; +import italicIcon from "@icons/editor/italic.svg"; +import underlinedIcon from "@icons/editor/underlined.svg"; +import textColorIcon from "@icons/editor/text-color.svg"; +import highlightIcon from "@icons/editor/highlighter.svg"; +import strikethroughIcon from "@icons/editor/strikethrough.svg"; +import superscriptIcon from "@icons/editor/superscript.svg"; +import subscriptIcon from "@icons/editor/subscript.svg"; +import codeIcon from "@icons/editor/code.svg"; +import formatClearIcon from "@icons/editor/format-clear.svg"; + +function buildFormatButton(label: string, format: TextFormatType, icon: string): EditorButtonDefinition { + return { + label: label, + icon, + action(context: EditorUiContext) { + context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsTextFormat(selection, format); + } + }; +} + +export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold', boldIcon); +export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic', italicIcon); +export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline', underlinedIcon); +export const textColor: EditorBasicButtonDefinition = {label: 'Text color', icon: textColorIcon}; +export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color', icon: highlightIcon}; + +export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough', strikethroughIcon); +export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript', superscriptIcon); +export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript', subscriptIcon); +export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code', codeIcon); +export const clearFormating: EditorButtonDefinition = { + label: 'Clear formatting', + icon: formatClearIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + const selection = $getSelection(); + for (const node of selection?.getNodes() || []) { + if ($isTextNode(node)) { + node.setFormat(0); + node.setStyle(''); + } + } + }); + }, + isActive() { + return false; + } +}; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/buttons/lists.ts b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts new file mode 100644 index 000000000..ecda290a1 --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/lists.ts @@ -0,0 +1,35 @@ +import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list"; +import {EditorButtonDefinition} from "../../framework/buttons"; +import {EditorUiContext} from "../../framework/core"; +import {$getSelection, BaseSelection, LexicalNode} from "lexical"; +import {$selectionContainsNodeType} from "../../../helpers"; +import listBulletIcon from "@icons/editor/list-bullet.svg"; +import listNumberedIcon from "@icons/editor/list-numbered.svg"; +import listCheckIcon from "@icons/editor/list-check.svg"; + + +function buildListButton(label: string, type: ListType, icon: string): EditorButtonDefinition { + return { + label, + icon, + action(context: EditorUiContext) { + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + if (this.isActive(selection, context)) { + removeList(context.editor); + } else { + insertList(context.editor, type); + } + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => { + return $isListNode(node) && (node as ListNode).getListType() === type; + }); + } + }; +} + +export const bulletList: EditorButtonDefinition = buildListButton('Bullet list', 'bullet', listBulletIcon); +export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number', listNumberedIcon); +export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check', listCheckIcon); diff --git a/resources/js/wysiwyg/ui/defaults/buttons/objects.ts b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts new file mode 100644 index 000000000..88241e926 --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/objects.ts @@ -0,0 +1,215 @@ +import {EditorButtonDefinition} from "../../framework/buttons"; +import linkIcon from "@icons/editor/link.svg"; +import {EditorUiContext} from "../../framework/core"; +import { + $createNodeSelection, + $createTextNode, + $getRoot, + $getSelection, + $setSelection, + BaseSelection, + ElementNode +} from "lexical"; +import {$getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsNodeType} from "../../../helpers"; +import {$isLinkNode, LinkNode} from "@lexical/link"; +import unlinkIcon from "@icons/editor/unlink.svg"; +import imageIcon from "@icons/editor/image.svg"; +import {$isImageNode, ImageNode} from "../../../nodes/image"; +import horizontalRuleIcon from "@icons/editor/horizontal-rule.svg"; +import {$createHorizontalRuleNode, $isHorizontalRuleNode} from "../../../nodes/horizontal-rule"; +import codeBlockIcon from "@icons/editor/code-block.svg"; +import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "../../../nodes/code-block"; +import editIcon from "@icons/edit.svg"; +import diagramIcon from "@icons/editor/diagram.svg"; +import {$createDiagramNode, $isDiagramNode, $openDrawingEditorForNode, DiagramNode} from "../../../nodes/diagram"; +import detailsIcon from "@icons/editor/details.svg"; +import {$createDetailsNode, $isDetailsNode} from "../../../nodes/details"; + +export const link: EditorButtonDefinition = { + label: 'Insert/edit link', + icon: linkIcon, + action(context: EditorUiContext) { + const linkModal = context.manager.createModal('link'); + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; + + let formDefaults = {}; + if (selectedLink) { + formDefaults = { + url: selectedLink.getURL(), + text: selectedLink.getTextContent(), + title: selectedLink.getTitle(), + target: selectedLink.getTarget(), + } + + context.editor.update(() => { + const selection = $createNodeSelection(); + selection.add(selectedLink.getKey()); + $setSelection(selection); + }); + } + + linkModal.show(formDefaults); + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isLinkNode); + } +}; + +export const unlink: EditorButtonDefinition = { + label: 'Remove link', + icon: unlinkIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + const selection = context.lastSelection; + const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null; + const selectionPoints = selection?.getStartEndPoints(); + + if (selectedLink) { + const newNode = $createTextNode(selectedLink.getTextContent()); + selectedLink.replace(newNode); + if (selectionPoints?.length === 2) { + newNode.select(selectionPoints[0].offset, selectionPoints[1].offset); + } else { + newNode.select(); + } + } + }); + }, + isActive(selection: BaseSelection|null): boolean { + return false; + } +}; + + + +export const image: EditorButtonDefinition = { + label: 'Insert/Edit Image', + icon: imageIcon, + action(context: EditorUiContext) { + const imageModal = context.manager.createModal('image'); + const selection = context.lastSelection; + const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null; + + context.editor.getEditorState().read(() => { + let formDefaults = {}; + if (selectedImage) { + formDefaults = { + src: selectedImage.getSrc(), + alt: selectedImage.getAltText(), + height: selectedImage.getHeight(), + width: selectedImage.getWidth(), + } + + context.editor.update(() => { + const selection = $createNodeSelection(); + selection.add(selectedImage.getKey()); + $setSelection(selection); + }); + } + + imageModal.show(formDefaults); + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isImageNode); + } +}; + +export const horizontalRule: EditorButtonDefinition = { + label: 'Insert horizontal line', + icon: horizontalRuleIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $insertNewBlockNodeAtSelection($createHorizontalRuleNode(), false); + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isHorizontalRuleNode); + } +}; + +export const codeBlock: EditorButtonDefinition = { + label: 'Insert code block', + icon: codeBlockIcon, + action(context: EditorUiContext) { + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + const codeBlock = $getNodeFromSelection(context.lastSelection, $isCodeBlockNode) as (CodeBlockNode|null); + if (codeBlock === null) { + context.editor.update(() => { + const codeBlock = $createCodeBlockNode(); + codeBlock.setCode(selection?.getTextContent() || ''); + $insertNewBlockNodeAtSelection(codeBlock, true); + $openCodeEditorForNode(context.editor, codeBlock); + codeBlock.selectStart(); + }); + } else { + $openCodeEditorForNode(context.editor, codeBlock); + } + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isCodeBlockNode); + } +}; + +export const editCodeBlock: EditorButtonDefinition = Object.assign({}, codeBlock, { + label: 'Edit code block', + icon: editIcon, +}); + +export const diagram: EditorButtonDefinition = { + label: 'Insert/edit drawing', + icon: diagramIcon, + action(context: EditorUiContext) { + context.editor.getEditorState().read(() => { + const selection = $getSelection(); + const diagramNode = $getNodeFromSelection(context.lastSelection, $isDiagramNode) as (DiagramNode|null); + if (diagramNode === null) { + context.editor.update(() => { + const diagram = $createDiagramNode(); + $insertNewBlockNodeAtSelection(diagram, true); + $openDrawingEditorForNode(context, diagram); + diagram.selectStart(); + }); + } else { + $openDrawingEditorForNode(context, diagramNode); + } + }); + }, + isActive(selection: BaseSelection|null): boolean { + return $selectionContainsNodeType(selection, $isDiagramNode); + } +}; + + +export const details: EditorButtonDefinition = { + label: 'Insert collapsible block', + icon: detailsIcon, + 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); + } +} \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts new file mode 100644 index 000000000..32fa49f88 --- /dev/null +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -0,0 +1,122 @@ +import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../../framework/buttons"; +import tableIcon from "@icons/editor/table.svg"; +import deleteIcon from "@icons/editor/table-delete.svg"; +import deleteColumnIcon from "@icons/editor/table-delete-column.svg"; +import deleteRowIcon from "@icons/editor/table-delete-row.svg"; +import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg"; +import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg"; +import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg"; +import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg"; +import {EditorUiContext} from "../../framework/core"; +import {$getBlockElementNodesInSelection, $getNodeFromSelection, $getParentOfType} from "../../../helpers"; +import {$getSelection} from "lexical"; +import {$isCustomTableNode, CustomTableNode} from "../../../nodes/custom-table"; +import { + $deleteTableColumn, $deleteTableColumn__EXPERIMENTAL, + $deleteTableRow__EXPERIMENTAL, + $getTableRowIndexFromTableCellNode, $insertTableColumn, $insertTableColumn__EXPERIMENTAL, + $insertTableRow, $insertTableRow__EXPERIMENTAL, + $isTableCellNode, + $isTableRowNode, + TableCellNode +} from "@lexical/table"; + + +export const table: EditorBasicButtonDefinition = { + label: 'Table', + icon: tableIcon, +}; + +export const deleteTable: EditorButtonDefinition = { + label: 'Delete table', + icon: deleteIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + const table = $getNodeFromSelection($getSelection(), $isCustomTableNode); + if (table) { + table.remove(); + } + }); + }, + isActive() { + return false; + } +}; + +export const insertRowAbove: EditorButtonDefinition = { + label: 'Insert row above', + icon: insertRowAboveIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $insertTableRow__EXPERIMENTAL(false); + }); + }, + isActive() { + return false; + } +}; + +export const insertRowBelow: EditorButtonDefinition = { + label: 'Insert row below', + icon: insertRowBelowIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $insertTableRow__EXPERIMENTAL(true); + }); + }, + isActive() { + return false; + } +}; + +export const deleteRow: EditorButtonDefinition = { + label: 'Delete row', + icon: deleteRowIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $deleteTableRow__EXPERIMENTAL(); + }); + }, + isActive() { + return false; + } +}; + +export const insertColumnBefore: EditorButtonDefinition = { + label: 'Insert column before', + icon: insertColumnBeforeIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $insertTableColumn__EXPERIMENTAL(false); + }); + }, + isActive() { + return false; + } +}; + +export const insertColumnAfter: EditorButtonDefinition = { + label: 'Insert column after', + icon: insertColumnAfterIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $insertTableColumn__EXPERIMENTAL(true); + }); + }, + isActive() { + return false; + } +}; + +export const deleteColumn: EditorButtonDefinition = { + label: 'Delete column', + icon: deleteColumnIcon, + action(context: EditorUiContext) { + context.editor.update(() => { + $deleteTableColumn__EXPERIMENTAL(); + }); + }, + isActive() { + return false; + } +}; \ No newline at end of file diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts index f728ae48f..5dee6b62b 100644 --- a/resources/js/wysiwyg/ui/index.ts +++ b/resources/js/wysiwyg/ui/index.ts @@ -3,7 +3,7 @@ import { getCodeToolbarContent, getImageToolbarContent, getLinkToolbarContent, - getMainEditorFullToolbar + getMainEditorFullToolbar, getTableToolbarContent } from "./toolbars"; import {EditorUIManager} from "./framework/manager"; import {image as imageFormDefinition, link as linkFormDefinition, source as sourceFormDefinition} from "./defaults/form-definitions"; @@ -61,6 +61,14 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro content: getCodeToolbarContent(), }); + manager.registerContextToolbar('table', { + selector: 'td,th', + content: getTableToolbarContent(), + displayTargetLocator(originalTarget: HTMLElement): HTMLElement { + return originalTarget.closest('table') as HTMLTableElement; + } + }); + // Register image decorator listener manager.registerDecoratorType('image', ImageDecorator); manager.registerDecoratorType('code', CodeBlockDecorator); diff --git a/resources/js/wysiwyg/ui/toolbars.ts b/resources/js/wysiwyg/ui/toolbars.ts index f5eae6b21..5d40578a5 100644 --- a/resources/js/wysiwyg/ui/toolbars.ts +++ b/resources/js/wysiwyg/ui/toolbars.ts @@ -1,17 +1,4 @@ import {EditorButton} from "./framework/buttons"; -import { - alignCenter, alignJustify, - alignLeft, - alignRight, - blockquote, bold, bulletList, clearFormating, code, codeBlock, - dangerCallout, details, diagram, editCodeBlock, fullscreen, - h2, h3, h4, h5, highlightColor, horizontalRule, image, - infoCallout, italic, link, numberList, paragraph, - redo, source, strikethrough, subscript, - successCallout, superscript, table, taskList, textColor, underline, - undo, unlink, - warningCallout -} from "./defaults/button-definitions"; import {EditorContainerUiElement, EditorSimpleClassContainer, EditorUiElement} from "./framework/core"; import {el} from "../helpers"; import {EditorFormatMenu} from "./framework/blocks/format-menu"; @@ -21,6 +8,48 @@ import {EditorColorPicker} from "./framework/blocks/color-picker"; import {EditorTableCreator} from "./framework/blocks/table-creator"; import {EditorColorButton} from "./framework/blocks/color-button"; import {EditorOverflowContainer} from "./framework/blocks/overflow-container"; +import { + deleteColumn, + deleteRow, + deleteTable, insertColumnAfter, + insertColumnBefore, + insertRowAbove, + insertRowBelow, + table +} from "./defaults/buttons/tables"; +import {fullscreen, redo, source, undo} from "./defaults/buttons/controls"; +import { + blockquote, dangerCallout, + h2, + h3, + h4, + h5, + infoCallout, + paragraph, + successCallout, + warningCallout +} from "./defaults/buttons/block-formats"; +import { + bold, clearFormating, code, + highlightColor, + italic, + strikethrough, subscript, + superscript, + textColor, + underline +} from "./defaults/buttons/inline-formats"; +import {alignCenter, alignJustify, alignLeft, alignRight} from "./defaults/buttons/alignments"; +import {bulletList, numberList, taskList} from "./defaults/buttons/lists"; +import { + codeBlock, + details, + diagram, + editCodeBlock, + horizontalRule, + image, + link, + unlink +} from "./defaults/buttons/objects"; export function getMainEditorFullToolbar(): EditorContainerUiElement { return new EditorSimpleClassContainer('editor-toolbar-main', [ @@ -129,4 +158,23 @@ export function getCodeToolbarContent(): EditorUiElement[] { return [ new EditorButton(editCodeBlock), ]; +} + +export function getTableToolbarContent(): EditorUiElement[] { + return [ + new EditorOverflowContainer(2, [ + // Todo - Table properties + new EditorButton(deleteTable), + ]), + new EditorOverflowContainer(3, [ + new EditorButton(insertRowAbove), + new EditorButton(insertRowBelow), + new EditorButton(deleteRow), + ]), + new EditorOverflowContainer(3, [ + new EditorButton(insertColumnBefore), + new EditorButton(insertColumnAfter), + new EditorButton(deleteColumn), + ]), + ]; } \ No newline at end of file