]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/ui/defaults/button-definitions.ts
Lexical: Standardised helper function format
[bookstack] / resources / js / wysiwyg / ui / defaults / button-definitions.ts
index 589567c031a01fb283bfa1e35aabfd7c93fe2ff3..bf725f8c8c86f58afb8d482d75f5ae43584306a6 100644 (file)
@@ -1,18 +1,31 @@
 import {EditorBasicButtonDefinition, EditorButton, EditorButtonDefinition} from "../framework/buttons";
 import {
     $createNodeSelection,
-    $createParagraphNode, $getRoot, $getSelection,
-    $isParagraphNode, $isTextNode, $setSelection,
-    BaseSelection, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_LOW, ElementNode, FORMAT_TEXT_COMMAND,
+    $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,
+    REDO_COMMAND,
+    TextFormatType,
     UNDO_COMMAND
 } from "lexical";
 import {
-    getNodeFromSelection,
-    selectionContainsNodeType,
-    selectionContainsTextFormat,
-    toggleSelectionBlockNodeType
+    $getBlockElementNodesInSelection,
+    $getNodeFromSelection, $insertNewBlockNodeAtSelection, $selectionContainsElementFormat,
+    $selectionContainsNodeType,
+    $selectionContainsTextFormat,
+    $toggleSelectionBlockNodeType
 } from "../../helpers";
 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
 import {
@@ -29,23 +42,37 @@ 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 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 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 imageIcon from "@icons/editor/image.svg"
-import detailsIcon from "@icons/editor/details.svg"
-import sourceIcon from "@icons/editor/source-view.svg"
+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 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";
 
 export const undo: EditorButtonDefinition = {
     label: 'Undo',
@@ -89,14 +116,15 @@ function buildCalloutButton(category: CalloutCategory, name: string): EditorButt
     return {
         label: `${name} Callout`,
         action(context: EditorUiContext) {
-            toggleSelectionBlockNodeType(
-                context.editor,
-                (node) => $isCalloutNodeOfCategory(node, category),
-                () => $createCalloutNode(category),
-            )
+            context.editor.update(() => {
+                $toggleSelectionBlockNodeType(
+                    (node) => $isCalloutNodeOfCategory(node, category),
+                    () => $createCalloutNode(category),
+                )
+            });
         },
         isActive(selection: BaseSelection|null): boolean {
-            return selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
+            return $selectionContainsNodeType(selection, (node) => $isCalloutNodeOfCategory(node, category));
         }
     };
 }
@@ -114,14 +142,15 @@ function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefin
     return {
         label: name,
         action(context: EditorUiContext) {
-            toggleSelectionBlockNodeType(
-                context.editor,
+            context.editor.update(() => {
+                $toggleSelectionBlockNodeType(
                 (node) => isHeaderNodeOfTag(node, tag),
                 () => $createHeadingNode(tag),
-            )
+                )
+            });
         },
         isActive(selection: BaseSelection|null): boolean {
-            return selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
+            return $selectionContainsNodeType(selection, (node) => isHeaderNodeOfTag(node, tag));
         }
     };
 }
@@ -134,20 +163,24 @@ export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header')
 export const blockquote: EditorButtonDefinition = {
     label: 'Blockquote',
     action(context: EditorUiContext) {
-        toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
+        context.editor.update(() => {
+            $toggleSelectionBlockNodeType($isQuoteNode, $createQuoteNode);
+        });
     },
     isActive(selection: BaseSelection|null): boolean {
-        return selectionContainsNodeType(selection, $isQuoteNode);
+        return $selectionContainsNodeType(selection, $isQuoteNode);
     }
 };
 
 export const paragraph: EditorButtonDefinition = {
     label: 'Paragraph',
     action(context: EditorUiContext) {
-        toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
+        context.editor.update(() => {
+            $toggleSelectionBlockNodeType($isParagraphNode, $createParagraphNode);
+        });
     },
     isActive(selection: BaseSelection|null): boolean {
-        return selectionContainsNodeType(selection, $isParagraphNode);
+        return $selectionContainsNodeType(selection, $isParagraphNode);
     }
 }
 
@@ -159,7 +192,7 @@ function buildFormatButton(label: string, format: TextFormatType, icon: string):
             context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
         },
         isActive(selection: BaseSelection|null): boolean {
-            return selectionContainsTextFormat(selection, format);
+            return $selectionContainsTextFormat(selection, format);
         }
     };
 }
@@ -167,8 +200,8 @@ function buildFormatButton(label: string, format: TextFormatType, icon: string):
 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'};
-export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color'};
+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);
@@ -183,6 +216,7 @@ export const clearFormating: EditorButtonDefinition = {
             for (const node of selection?.getNodes() || []) {
                 if ($isTextNode(node)) {
                     node.setFormat(0);
+                    node.setStyle('');
                 }
             }
         });
@@ -192,6 +226,59 @@ export const clearFormating: EditorButtonDefinition = {
     }
 };
 
+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,
@@ -199,7 +286,7 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
         action(context: EditorUiContext) {
             context.editor.getEditorState().read(() => {
                 const selection = $getSelection();
-                if (this.isActive(selection)) {
+                if (this.isActive(selection, context)) {
                     removeList(context.editor);
                 } else {
                     insertList(context.editor, type);
@@ -207,7 +294,7 @@ function buildListButton(label: string, type: ListType, icon: string): EditorBut
             });
         },
         isActive(selection: BaseSelection|null): boolean {
-            return selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
+            return $selectionContainsNodeType(selection, (node: LexicalNode | null | undefined): boolean => {
                 return $isListNode(node) && (node as ListNode).getListType() === type;
             });
         }
@@ -226,7 +313,7 @@ export const link: EditorButtonDefinition = {
         const linkModal = context.manager.createModal('link');
         context.editor.getEditorState().read(() => {
             const selection = $getSelection();
-            const selectedLink = getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
+            const selectedLink = $getNodeFromSelection(selection, $isLinkNode) as LinkNode|null;
 
             let formDefaults = {};
             if (selectedLink) {
@@ -248,12 +335,38 @@ export const link: EditorButtonDefinition = {
         });
     },
     isActive(selection: BaseSelection|null): boolean {
-        return selectionContainsNodeType(selection, $isLinkNode);
+        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 = {
@@ -262,7 +375,7 @@ export const image: EditorButtonDefinition = {
     action(context: EditorUiContext) {
         const imageModal = context.manager.createModal('image');
         const selection = context.lastSelection;
-        const selectedImage = getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
+        const selectedImage = $getNodeFromSelection(selection, $isImageNode) as ImageNode|null;
 
         context.editor.getEditorState().read(() => {
             let formDefaults = {};
@@ -285,10 +398,53 @@ export const image: EditorButtonDefinition = {
         });
     },
     isActive(selection: BaseSelection|null): boolean {
-        return selectionContainsNodeType(selection, $isImageNode);
+        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 details: EditorButtonDefinition = {
     label: 'Insert collapsible block',
     icon: detailsIcon,
@@ -313,7 +469,7 @@ export const details: EditorButtonDefinition = {
         });
     },
     isActive(selection: BaseSelection|null): boolean {
-        return selectionContainsNodeType(selection, $isDetailsNode);
+        return $selectionContainsNodeType(selection, $isDetailsNode);
     }
 }
 
@@ -328,4 +484,18 @@ export const source: EditorButtonDefinition = {
     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