]> BookStack Code Mirror - bookstack/blobdiff - resources/js/wysiwyg/ui/framework/forms.ts
Lexical: Media form improvements
[bookstack] / resources / js / wysiwyg / ui / framework / forms.ts
index c6338f7984925e33c55112aa9d62daebd3ae85c2..b12d9f692fca9e1d4ca15070b160c5a1b21b50b9 100644 (file)
@@ -1,11 +1,17 @@
-import {EditorUiContext, EditorUiElement} from "./core";
-import {EditorContainerUiElement} from "./containers";
-import {el} from "../../helpers";
+import {
+    EditorUiContext,
+    EditorUiElement,
+    EditorContainerUiElement,
+    EditorUiBuilderDefinition,
+    isUiBuilderDefinition
+} from "./core";
+import {uniqueId} from "../../../services/util";
+import {el} from "../../utils/dom";
 
 export interface EditorFormFieldDefinition {
     label: string;
     name: string;
-    type: 'text' | 'select';
+    type: 'text' | 'select' | 'textarea' | 'checkbox' | 'hidden';
 }
 
 export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
@@ -13,10 +19,17 @@ export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefiniti
     valuesByLabel: Record<string, string>
 }
 
+export type EditorFormFields = (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
+
+interface EditorFormTabDefinition {
+    label: string;
+    contents: EditorFormFields;
+}
+
 export interface EditorFormDefinition {
     submitText: string;
-    action: (formData: FormData, context: EditorUiContext) => boolean;
-    fields: EditorFormFieldDefinition[];
+    action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
+    fields: EditorFormFields;
 }
 
 export class EditorFormField extends EditorUiElement {
@@ -28,8 +41,13 @@ export class EditorFormField extends EditorUiElement {
     }
 
     setValue(value: string) {
-        const input = this.getDOMElement().querySelector('input,select') as HTMLInputElement;
-        input.value = value;
+        const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;
+        if (this.definition.type === 'checkbox') {
+            input.checked = Boolean(value);
+        } else {
+            input.value = value;
+        }
+        input.dispatchEvent(new Event('change'));
     }
 
     getName(): string {
@@ -43,8 +61,15 @@ export class EditorFormField extends EditorUiElement {
         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]));
+            const optionElems = labels.map(label => el('option', {value: options[label]}, [this.trans(label)]));
             input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
+        } else if (this.definition.type === 'textarea') {
+            input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
+        } else if (this.definition.type === 'checkbox') {
+            input = el('input', {id, name: this.definition.name, type: 'checkbox', class: 'editor-form-field-input-checkbox', value: 'true'});
+        } else if (this.definition.type === 'hidden') {
+            input = el('input', {id, name: this.definition.name, type: 'hidden'});
+            return el('div', {hidden: 'true'}, [input]);
         } else {
             input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
         }
@@ -59,9 +84,17 @@ export class EditorFormField extends EditorUiElement {
 export class EditorForm extends EditorContainerUiElement {
     protected definition: EditorFormDefinition;
     protected onCancel: null|(() => void) = null;
+    protected onSuccessfulSubmit: null|(() => void) = null;
 
     constructor(definition: EditorFormDefinition) {
-        super(definition.fields.map(fieldDefinition => new EditorFormField(fieldDefinition)));
+        let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => {
+            if (isUiBuilderDefinition(fieldDefinition)) {
+                return fieldDefinition.build();
+            }
+            return new EditorFormField(fieldDefinition)
+        });
+
+        super(children);
         this.definition = definition;
     }
 
@@ -78,14 +111,28 @@ export class EditorForm extends EditorContainerUiElement {
         this.onCancel = callback;
     }
 
+    setOnSuccessfulSubmit(callback: () => void) {
+        this.onSuccessfulSubmit = callback;
+    }
+
     protected getFieldByName(name: string): EditorFormField|null {
-        for (const child of this.children as EditorFormField[]) {
-            if (child.getName() === name) {
-                return child;
+
+        const search = (children: EditorUiElement[]): EditorFormField|null => {
+            for (const child of children) {
+                if (child instanceof EditorFormField && child.getName() === name) {
+                    return child;
+                } else if (child instanceof EditorContainerUiElement) {
+                    const matchingChild = search(child.getChildren());
+                    if (matchingChild) {
+                        return matchingChild;
+                    }
+                }
             }
-        }
 
-        return null;
+            return null;
+        };
+
+        return search(this.getChildren());
     }
 
     protected buildDOM(): HTMLElement {
@@ -98,10 +145,13 @@ export class EditorForm extends EditorContainerUiElement {
             ])
         ]);
 
-        form.addEventListener('submit', (event) => {
+        form.addEventListener('submit', async (event) => {
             event.preventDefault();
             const formData = new FormData(form as HTMLFormElement);
-            this.definition.action(formData, this.getContext());
+            const result = await this.definition.action(formData, this.getContext());
+            if (result && this.onSuccessfulSubmit) {
+                this.onSuccessfulSubmit();
+            }
         });
 
         cancelButton.addEventListener('click', (event) => {
@@ -112,4 +162,97 @@ export class EditorForm extends EditorContainerUiElement {
 
         return form;
     }
+}
+
+export class EditorFormTab extends EditorContainerUiElement {
+
+    protected definition: EditorFormTabDefinition;
+    protected fields: EditorUiElement[];
+    protected id: string;
+
+    constructor(definition: EditorFormTabDefinition) {
+        const fields = definition.contents.map(fieldDef => {
+            if (isUiBuilderDefinition(fieldDef)) {
+                return fieldDef.build();
+            }
+            return new EditorFormField(fieldDef)
+        });
+
+        super(fields);
+
+        this.definition = definition;
+        this.fields = fields;
+        this.id = uniqueId();
+    }
+
+    public getLabel(): string {
+        return this.getContext().translate(this.definition.label);
+    }
+
+    public getId(): string {
+        return this.id;
+    }
+
+    protected buildDOM(): HTMLElement {
+        return el(
+            'div',
+            {
+                class: 'editor-form-tab-content',
+                role: 'tabpanel',
+                id: `editor-tabpanel-${this.id}`,
+                'aria-labelledby': `editor-tab-${this.id}`,
+            },
+            this.fields.map(f => f.getDOMElement())
+        );
+    }
+}
+export class EditorFormTabs extends EditorContainerUiElement {
+
+    protected definitions: EditorFormTabDefinition[] = [];
+    protected tabs: EditorFormTab[] = [];
+
+    constructor(definitions: EditorFormTabDefinition[]) {
+        const tabs: EditorFormTab[] = definitions.map(d => new EditorFormTab(d));
+        super(tabs);
+
+        this.definitions = definitions;
+        this.tabs = tabs;
+    }
+
+    protected buildDOM(): HTMLElement {
+        const controls: HTMLElement[] = [];
+        const contents: HTMLElement[] = [];
+
+        const selectTab = (tabIndex: number) => {
+            for (let i = 0; i < controls.length; i++) {
+                controls[i].setAttribute('aria-selected', (i === tabIndex) ? 'true' : 'false');
+            }
+            for (let i = 0; i < contents.length; i++) {
+                contents[i].hidden = !(i === tabIndex);
+            }
+        };
+
+        for (const tab of this.tabs) {
+            const button = el('button', {
+                class: 'editor-form-tab-control',
+                type: 'button',
+                role: 'tab',
+                id: `editor-tab-${tab.getId()}`,
+                'aria-controls': `editor-tabpanel-${tab.getId()}`
+            }, [tab.getLabel()]);
+            contents.push(tab.getDOMElement());
+            controls.push(button);
+
+            button.addEventListener('click', event => {
+                selectTab(controls.indexOf(button));
+            });
+        }
+
+        selectTab(0);
+
+        return el('div', {class: 'editor-form-tab-container'}, [
+            el('div', {class: 'editor-form-tab-controls'}, controls),
+            el('div', {class: 'editor-form-tab-contents'}, contents),
+        ]);
+    }
 }
\ No newline at end of file