]> BookStack Code Mirror - bookstack/commitdiff
Lexical: Started on form UI
authorDan Brown <redacted>
Thu, 30 May 2024 15:50:55 +0000 (16:50 +0100)
committerDan Brown <redacted>
Thu, 30 May 2024 15:50:55 +0000 (16:50 +0100)
resources/js/wysiwyg/ui/defaults/button-definitions.ts
resources/js/wysiwyg/ui/defaults/form-definitions.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/framework/buttons.ts
resources/js/wysiwyg/ui/framework/containers.ts
resources/js/wysiwyg/ui/framework/core.ts [moved from resources/js/wysiwyg/ui/framework/base-elements.ts with 75% similarity]
resources/js/wysiwyg/ui/framework/forms.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/framework/manager.ts [new file with mode: 0644]
resources/js/wysiwyg/ui/index.ts

index 874f632fe14059738fcc522d3883832552f83d59..da0a1e2c53aa7c5e281ce7c172700dbed091b430 100644 (file)
@@ -3,7 +3,6 @@ import {
     $createParagraphNode,
     $isParagraphNode,
     BaseSelection, FORMAT_TEXT_COMMAND,
-    LexicalEditor,
     LexicalNode,
     REDO_COMMAND, TextFormatType,
     UNDO_COMMAND
@@ -19,11 +18,12 @@ import {
     HeadingTagType
 } from "@lexical/rich-text";
 import {$isLinkNode, $toggleLink} from "@lexical/link";
+import {EditorUiContext} from "../framework/core";
 
 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 +32,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 +43,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 +68,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 +88,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 +98,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 +109,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);
@@ -132,8 +132,8 @@ export const code: EditorButtonDefinition = buildFormatButton('Inline Code', 'co
 
 export const link: EditorButtonDefinition = {
     label: 'Insert/edit link',
-    action(editor: LexicalEditor) {
-        editor.update(() => {
+    action(context: EditorUiContext) {
+        context.editor.update(() => {
             $toggleLink('http://example.com');
         })
     },
diff --git a/resources/js/wysiwyg/ui/defaults/form-definitions.ts b/resources/js/wysiwyg/ui/defaults/form-definitions.ts
new file mode 100644 (file)
index 0000000..c8477d9
--- /dev/null
@@ -0,0 +1,43 @@
+import {EditorFormDefinition, EditorFormFieldDefinition, EditorSelectFormFieldDefinition} from "../framework/forms";
+import {EditorUiContext} from "../framework/core";
+
+
+export const link: EditorFormDefinition = {
+    submitText: 'Apply',
+    cancelText: 'Cancel',
+    action(formData, context: EditorUiContext) {
+        // Todo
+        console.log('link-form-action', formData);
+        return true;
+    },
+    cancel() {
+        // Todo
+        console.log('link-form-cancel');
+    },
+    fields: [
+        {
+            label: 'URL',
+            name: 'url',
+            type: 'text',
+        },
+        {
+            label: 'Text to display',
+            name: 'text',
+            type: 'text',
+        },
+        {
+            label: 'Title',
+            name: 'title',
+            type: 'text',
+        },
+        {
+            label: 'Open link in...',
+            name: 'target',
+            type: 'select',
+            valuesByLabel: {
+                'Current window': '',
+                'New window': '_blank',
+            }
+        } as EditorSelectFormFieldDefinition,
+    ],
+};
\ No newline at end of file
index 2a6f5a976fd21532bab01b5341fecd9f8615ea78..48046e9de9a953fcf435dead1db512b8075898ae 100644 (file)
@@ -1,15 +1,16 @@
-import {BaseSelection, LexicalEditor} from "lexical";
-import {EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {BaseSelection} from "lexical";
+import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
 import {el} from "../../helpers";
 
 export interface EditorButtonDefinition {
     label: string;
-    action: (editor: LexicalEditor) => void;
+    action: (context: EditorUiContext) => void;
     isActive: (selection: BaseSelection|null) => boolean;
 }
 
 export class EditorButton extends EditorUiElement {
     protected definition: EditorButtonDefinition;
+    protected active: boolean = false;
 
     constructor(definition: EditorButtonDefinition) {
         super();
@@ -20,7 +21,7 @@ export class EditorButton extends EditorUiElement {
         const button = el('button', {
             type: 'button',
             class: 'editor-button',
-        }, [this.definition.label]) as HTMLButtonElement;
+        }, [this.getLabel()]) as HTMLButtonElement;
 
         button.addEventListener('click', this.onClick.bind(this));
 
@@ -28,17 +29,25 @@ export class EditorButton extends EditorUiElement {
     }
 
     protected onClick() {
-        this.definition.action(this.getContext().editor);
+        this.definition.action(this.getContext());
     }
 
     updateActiveState(selection: BaseSelection|null) {
-        const isActive = this.definition.isActive(selection);
-        this.dom?.classList.toggle('editor-button-active', isActive);
+        this.active = this.definition.isActive(selection);
+        this.dom?.classList.toggle('editor-button-active', this.active);
     }
 
     updateState(state: EditorUiStateUpdate): void {
         this.updateActiveState(state.selection);
     }
+
+    isActive(): boolean {
+        return this.active;
+    }
+
+    getLabel(): string {
+        return this.trans(this.definition.label);
+    }
 }
 
 export class FormatPreviewButton extends EditorButton {
@@ -55,7 +64,7 @@ export class FormatPreviewButton extends EditorButton {
 
         const preview = el('span', {
             class: 'editor-button-format-preview'
-        }, [this.definition.label]);
+        }, [this.getLabel()]);
 
         const stylesToApply = this.getStylesFromPreview();
         console.log(stylesToApply);
@@ -70,7 +79,7 @@ export class FormatPreviewButton extends EditorButton {
     protected getStylesFromPreview(): Record<string, string> {
         const wrap = el('div', {style: 'display: none', hidden: 'true', class: 'page-content'});
         const sampleClone = this.previewSampleElement.cloneNode() as HTMLElement;
-        sampleClone.textContent = this.definition.label;
+        sampleClone.textContent = this.getLabel();
         wrap.append(sampleClone);
         document.body.append(wrap);
 
index e58988e7b843dc11b6708a55ab1904a343002df4..ed191a882059fb8698d6cd2ab36edbbfaedda053 100644 (file)
@@ -1,5 +1,6 @@
-import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./base-elements";
+import {EditorUiContext, EditorUiElement, EditorUiStateUpdate} from "./core";
 import {el} from "../../helpers";
+import {EditorButton} from "./buttons";
 
 export class EditorContainerUiElement extends EditorUiElement {
     protected children : EditorUiElement[];
@@ -24,6 +25,7 @@ export class EditorContainerUiElement extends EditorUiElement {
     }
 
     setContext(context: EditorUiContext) {
+        super.setContext(context);
         for (const child of this.getChildren()) {
             child.setContext(context);
         }
@@ -54,9 +56,9 @@ export class EditorFormatMenu extends EditorContainerUiElement {
         }, childElements);
 
         const toggle = el('button', {
-            class: 'editor-format-menu-toggle',
+            class: 'editor-format-menu-toggle editor-button',
             type: 'button',
-        }, ['Formats']);
+        }, [this.trans('Formats')]);
 
         const wrapper = el('div', {
             class: 'editor-format-menu editor-dropdown-menu-container',
@@ -88,4 +90,24 @@ export class EditorFormatMenu extends EditorContainerUiElement {
 
         return wrapper;
     }
+
+    updateState(state: EditorUiStateUpdate) {
+        super.updateState(state);
+
+        for (const child of this.children) {
+            if (child instanceof EditorButton && child.isActive()) {
+                this.updateToggleLabel(child.getLabel());
+                return;
+            }
+        }
+
+        this.updateToggleLabel(this.trans('Formats'));
+    }
+
+    protected updateToggleLabel(text: string): void {
+        const button = this.getDOMElement().querySelector('button');
+        if (button) {
+            button.innerText = text;
+        }
+    }
 }
\ No newline at end of file
similarity index 75%
rename from resources/js/wysiwyg/ui/framework/base-elements.ts
rename to resources/js/wysiwyg/ui/framework/core.ts
index 66501178238016ac34fe0f836531e97d95220e04..68d845b4249b655054cd2f3f9b7560dbeb5658e0 100644 (file)
@@ -1,4 +1,5 @@
 import {BaseSelection, LexicalEditor} from "lexical";
+import {EditorUIManager} from "./manager";
 
 export type EditorUiStateUpdate = {
     editor: LexicalEditor,
@@ -7,6 +8,8 @@ export type EditorUiStateUpdate = {
 
 export type EditorUiContext = {
     editor: LexicalEditor,
+    translate: (text: string) => string,
+    manager: EditorUIManager,
 };
 
 export abstract class EditorUiElement {
@@ -35,5 +38,11 @@ export abstract class EditorUiElement {
         return this.dom;
     }
 
-    abstract updateState(state: EditorUiStateUpdate): void;
+    trans(text: string) {
+        return this.getContext().translate(text);
+    }
+
+    updateState(state: EditorUiStateUpdate): void {
+        return;
+    }
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/framework/forms.ts b/resources/js/wysiwyg/ui/framework/forms.ts
new file mode 100644 (file)
index 0000000..0fce73c
--- /dev/null
@@ -0,0 +1,82 @@
+import {EditorUiContext, EditorUiElement} from "./core";
+import {EditorContainerUiElement} from "./containers";
+import {el} from "../../helpers";
+
+export interface EditorFormFieldDefinition {
+    label: string;
+    name: string;
+    type: 'text' | 'select';
+}
+
+export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
+    type: 'select',
+    valuesByLabel: Record<string, string>
+}
+
+export interface EditorFormDefinition {
+    submitText: string;
+    cancelText: string;
+    action: (formData: FormData, context: EditorUiContext) => boolean;
+    cancel: () => void;
+    fields: EditorFormFieldDefinition[];
+}
+
+export class EditorFormField extends EditorUiElement {
+    protected definition: EditorFormFieldDefinition;
+
+    constructor(definition: EditorFormFieldDefinition) {
+        super();
+        this.definition = definition;
+    }
+
+    protected buildDOM(): HTMLElement {
+        const id = `editor-form-field-${this.definition.name}-${Date.now()}`;
+        let input: HTMLElement;
+
+        if (this.definition.type === 'select') {
+            const options = (this.definition as EditorSelectFormFieldDefinition).valuesByLabel
+            const labels = Object.keys(options);
+            const optionElems = labels.map(label => el('option', {value: options[label]}, [label]));
+            input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
+        } else {
+            input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
+        }
+
+        return el('div', {class: 'editor-form-field-wrapper'}, [
+            el('label', {class: 'editor-form-field-label', for: id}, [this.trans(this.definition.label)]),
+            input,
+        ]);
+    }
+}
+
+export class EditorForm extends EditorContainerUiElement {
+    protected definition: EditorFormDefinition;
+
+    constructor(definition: EditorFormDefinition) {
+        super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition)));
+        this.definition = definition;
+    }
+
+    protected buildDOM(): HTMLElement {
+        const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans(this.definition.cancelText)]);
+        const form = el('form', {}, [
+            ...this.children.map(child => child.getDOMElement()),
+            el('div', {class: 'editor-form-actions'}, [
+                cancelButton,
+                el('button', {type: 'submit', class: 'editor-form-action-primary'}, [this.trans(this.definition.submitText)]),
+            ])
+        ]);
+
+        form.addEventListener('submit', (event) => {
+            event.preventDefault();
+            const formData = new FormData(form as HTMLFormElement);
+            this.definition.action(formData, this.getContext());
+        });
+
+        cancelButton.addEventListener('click', (event) => {
+            this.definition.cancel();
+        });
+
+        return form;
+    }
+}
\ No newline at end of file
diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts
new file mode 100644 (file)
index 0000000..f1a34c9
--- /dev/null
@@ -0,0 +1,11 @@
+
+
+
+
+
+export class EditorUIManager {
+
+    // Todo - Register and show modal via this
+    //  (Part of UI context)
+
+}
\ No newline at end of file
index 56ae9354ae2bb1651bba932e4dc33f0c2e0ce00a..5a0d7fd2d5e3778da0dfbc97f8aba32655c62a9a 100644 (file)
@@ -5,12 +5,28 @@ import {
     SELECTION_CHANGE_COMMAND
 } from "lexical";
 import {getMainEditorFullToolbar} from "./toolbars";
+import {EditorUIManager} from "./framework/manager";
+import {EditorForm} from "./framework/forms";
+import {link} from "./defaults/form-definitions";
 
 export function buildEditorUI(element: HTMLElement, editor: LexicalEditor) {
+    const manager = new EditorUIManager();
+    const context = {
+        editor,
+        manager,
+        translate: (text: string): string => text,
+    };
+
+    // Create primary toolbar
     const toolbar = getMainEditorFullToolbar();
-    toolbar.setContext({editor});
+    toolbar.setContext(context);
     element.before(toolbar.getDOMElement());
 
+    // Form test
+    const linkForm = new EditorForm(link);
+    linkForm.setContext(context);
+    element.before(linkForm.getDOMElement());
+
     // Update button states on editor selection change
     editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
         const selection = $getSelection();
Morty Proxy This is a proxified and sanitized view of the page, visit original site.