]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/ui/defaults/button-definitions.ts
Lexical: Added basic list button/support
[bookstack] / resources / js / wysiwyg / ui / defaults / button-definitions.ts
index 874f632fe14059738fcc522d3883832552f83d59..57460ef60eda9a37cb4a96b02898fc86f25d9bb2 100644 (file)
@@ -1,14 +1,19 @@
-import {EditorButtonDefinition} from "../framework/buttons";
+import {EditorBasicButtonDefinition, EditorButtonDefinition} from "../framework/buttons";
 import {
-    $createParagraphNode,
-    $isParagraphNode,
-    BaseSelection, FORMAT_TEXT_COMMAND,
-    LexicalEditor,
+    $createNodeSelection,
+    $createParagraphNode, $getRoot, $getSelection,
+    $isParagraphNode, $isTextNode, $setSelection,
+    BaseSelection, ElementNode, FORMAT_TEXT_COMMAND,
     LexicalNode,
     REDO_COMMAND, TextFormatType,
     UNDO_COMMAND
 } from "lexical";
-import {selectionContainsNodeType, selectionContainsTextFormat, toggleSelectionBlockNodeType} from "../../helpers";
+import {
+    getNodeFromSelection,
+    selectionContainsNodeType,
+    selectionContainsTextFormat,
+    toggleSelectionBlockNodeType
+} from "../../helpers";
 import {$createCalloutNode, $isCalloutNodeOfCategory, CalloutCategory} from "../../nodes/callout";
 import {
     $createHeadingNode,
@@ -18,12 +23,17 @@ import {
     HeadingNode,
     HeadingTagType
 } from "@lexical/rich-text";
-import {$isLinkNode, $toggleLink} from "@lexical/link";
+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";
 
 export const undo: EditorButtonDefinition = {
     label: 'Undo',
-    action(editor: LexicalEditor) {
-        editor.dispatchCommand(UNDO_COMMAND, undefined);
+    action(context: EditorUiContext) {
+        context.editor.dispatchCommand(UNDO_COMMAND, undefined);
     },
     isActive(selection: BaseSelection|null): boolean {
         return false;
@@ -32,8 +42,8 @@ export const undo: EditorButtonDefinition = {
 
 export const redo: EditorButtonDefinition = {
     label: 'Redo',
-    action(editor: LexicalEditor) {
-        editor.dispatchCommand(REDO_COMMAND, undefined);
+    action(context: EditorUiContext) {
+        context.editor.dispatchCommand(REDO_COMMAND, undefined);
     },
     isActive(selection: BaseSelection|null): boolean {
         return false;
@@ -43,9 +53,9 @@ export const redo: EditorButtonDefinition = {
 function buildCalloutButton(category: CalloutCategory, name: string): EditorButtonDefinition {
     return {
         label: `${name} Callout`,
-        action(editor: LexicalEditor) {
+        action(context: EditorUiContext) {
             toggleSelectionBlockNodeType(
-                editor,
+                context.editor,
                 (node) => $isCalloutNodeOfCategory(node, category),
                 () => $createCalloutNode(category),
             )
@@ -68,9 +78,9 @@ const isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTag
 function buildHeaderButton(tag: HeadingTagType, name: string): EditorButtonDefinition {
     return {
         label: name,
-        action(editor: LexicalEditor) {
+        action(context: EditorUiContext) {
             toggleSelectionBlockNodeType(
-                editor,
+                context.editor,
                 (node) => isHeaderNodeOfTag(node, tag),
                 () => $createHeadingNode(tag),
             )
@@ -88,8 +98,8 @@ export const h5: EditorButtonDefinition = buildHeaderButton('h5', 'Tiny Header')
 
 export const blockquote: EditorButtonDefinition = {
     label: 'Blockquote',
-    action(editor: LexicalEditor) {
-        toggleSelectionBlockNodeType(editor, $isQuoteNode, $createQuoteNode);
+    action(context: EditorUiContext) {
+        toggleSelectionBlockNodeType(context.editor, $isQuoteNode, $createQuoteNode);
     },
     isActive(selection: BaseSelection|null): boolean {
         return selectionContainsNodeType(selection, $isQuoteNode);
@@ -98,8 +108,8 @@ export const blockquote: EditorButtonDefinition = {
 
 export const paragraph: EditorButtonDefinition = {
     label: 'Paragraph',
-    action(editor: LexicalEditor) {
-        toggleSelectionBlockNodeType(editor, $isParagraphNode, $createParagraphNode);
+    action(context: EditorUiContext) {
+        toggleSelectionBlockNodeType(context.editor, $isParagraphNode, $createParagraphNode);
     },
     isActive(selection: BaseSelection|null): boolean {
         return selectionContainsNodeType(selection, $isParagraphNode);
@@ -109,8 +119,8 @@ export const paragraph: EditorButtonDefinition = {
 function buildFormatButton(label: string, format: TextFormatType): EditorButtonDefinition {
     return {
         label: label,
-        action(editor: LexicalEditor) {
-            editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
+        action(context: EditorUiContext) {
+            context.editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
         },
         isActive(selection: BaseSelection|null): boolean {
             return selectionContainsTextFormat(selection, format);
@@ -121,24 +131,155 @@ function buildFormatButton(label: string, format: TextFormatType): EditorButtonD
 export const bold: EditorButtonDefinition = buildFormatButton('Bold', 'bold');
 export const italic: EditorButtonDefinition = buildFormatButton('Italic', 'italic');
 export const underline: EditorButtonDefinition = buildFormatButton('Underline', 'underline');
-// Todo - Text color
-// Todo - Highlight color
+export const textColor: EditorBasicButtonDefinition = {label: 'Text color'};
+export const highlightColor: EditorBasicButtonDefinition = {label: 'Highlight color'};
+
 export const strikethrough: EditorButtonDefinition = buildFormatButton('Strikethrough', 'strikethrough');
 export const superscript: EditorButtonDefinition = buildFormatButton('Superscript', 'superscript');
 export const subscript: EditorButtonDefinition = buildFormatButton('Subscript', 'subscript');
 export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'code');
-// Todo - Clear formatting
+export const clearFormating: EditorButtonDefinition = {
+    label: 'Clear formatting',
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = $getSelection();
+            for (const node of selection?.getNodes() || []) {
+                if ($isTextNode(node)) {
+                    node.setFormat(0);
+                }
+            }
+        });
+    },
+    isActive() {
+        return false;
+    }
+};
+
+function buildListButton(label: string, type: ListType): EditorButtonDefinition {
+    return {
+        label,
+        action(context: EditorUiContext) {
+            context.editor.getEditorState().read(() => {
+                const selection = $getSelection();
+                if (this.isActive(selection)) {
+                    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');
+export const numberList: EditorButtonDefinition = buildListButton('Numbered list', 'number');
+export const taskList: EditorButtonDefinition = buildListButton('Task list', 'check');
 
 
 export const link: EditorButtonDefinition = {
     label: 'Insert/edit link',
-    action(editor: LexicalEditor) {
-        editor.update(() => {
-            $toggleLink('http://example.com');
-        })
+    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 image: EditorButtonDefinition = {
+    label: 'Insert/Edit Image',
+    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 details: EditorButtonDefinition = {
+    label: 'Insert collapsible block',
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
+            const selection = $getSelection();
+            const detailsNode = $createDetailsNode();
+            const selectionNodes = selection?.getNodes() || [];
+            const topLevels = selectionNodes.map(n => n.getTopLevelElement())
+                .filter(n => n !== null) as ElementNode[];
+            const uniqueTopLevels = [...new Set(topLevels)];
+
+            if (uniqueTopLevels.length > 0) {
+                uniqueTopLevels[0].insertAfter(detailsNode);
+            } else {
+                $getRoot().append(detailsNode);
+            }
+
+            for (const node of uniqueTopLevels) {
+                detailsNode.append(node);
+            }
+        });
+    },
+    isActive(selection: BaseSelection|null): boolean {
+        return selectionContainsNodeType(selection, $isDetailsNode);
+    }
+}
+
+export const source: EditorButtonDefinition = {
+    label: 'Source code',
+    async action(context: EditorUiContext) {
+        const modal = context.manager.createModal('source');
+        const source = await getEditorContentAsHtml(context.editor);
+        modal.show({source});
+    },
+    isActive() {
+        return false;
+    }
+};
\ No newline at end of file