]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg/ui/framework/forms.ts
ZIP Exports: Built out initial import view
[bookstack] / resources / js / wysiwyg / ui / framework / forms.ts
1 import {
2     EditorUiContext,
3     EditorUiElement,
4     EditorContainerUiElement,
5     EditorUiBuilderDefinition,
6     isUiBuilderDefinition
7 } from "./core";
8 import {uniqueId} from "../../../services/util";
9 import {el} from "../../utils/dom";
10
11 export interface EditorFormFieldDefinition {
12     label: string;
13     name: string;
14     type: 'text' | 'select' | 'textarea';
15 }
16
17 export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition {
18     type: 'select',
19     valuesByLabel: Record<string, string>
20 }
21
22 interface EditorFormTabDefinition {
23     label: string;
24     contents: EditorFormFieldDefinition[];
25 }
26
27 export interface EditorFormDefinition {
28     submitText: string;
29     action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
30     fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[];
31 }
32
33 export class EditorFormField extends EditorUiElement {
34     protected definition: EditorFormFieldDefinition;
35
36     constructor(definition: EditorFormFieldDefinition) {
37         super();
38         this.definition = definition;
39     }
40
41     setValue(value: string) {
42         const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement;
43         input.value = value;
44     }
45
46     getName(): string {
47         return this.definition.name;
48     }
49
50     protected buildDOM(): HTMLElement {
51         const id = `editor-form-field-${this.definition.name}-${Date.now()}`;
52         let input: HTMLElement;
53
54         if (this.definition.type === 'select') {
55             const options = (this.definition as EditorSelectFormFieldDefinition).valuesByLabel
56             const labels = Object.keys(options);
57             const optionElems = labels.map(label => el('option', {value: options[label]}, [this.trans(label)]));
58             input = el('select', {id, name: this.definition.name, class: 'editor-form-field-input'}, optionElems);
59         } else if (this.definition.type === 'textarea') {
60             input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'});
61         } else {
62             input = el('input', {id, name: this.definition.name, class: 'editor-form-field-input'});
63         }
64
65         return el('div', {class: 'editor-form-field-wrapper'}, [
66             el('label', {class: 'editor-form-field-label', for: id}, [this.trans(this.definition.label)]),
67             input,
68         ]);
69     }
70 }
71
72 export class EditorForm extends EditorContainerUiElement {
73     protected definition: EditorFormDefinition;
74     protected onCancel: null|(() => void) = null;
75     protected onSuccessfulSubmit: null|(() => void) = null;
76
77     constructor(definition: EditorFormDefinition) {
78         let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => {
79             if (isUiBuilderDefinition(fieldDefinition)) {
80                 return fieldDefinition.build();
81             }
82             return new EditorFormField(fieldDefinition)
83         });
84
85         super(children);
86         this.definition = definition;
87     }
88
89     setValues(values: Record<string, string>) {
90         for (const name of Object.keys(values)) {
91             const field = this.getFieldByName(name);
92             if (field) {
93                 field.setValue(values[name]);
94             }
95         }
96     }
97
98     setOnCancel(callback: () => void) {
99         this.onCancel = callback;
100     }
101
102     setOnSuccessfulSubmit(callback: () => void) {
103         this.onSuccessfulSubmit = callback;
104     }
105
106     protected getFieldByName(name: string): EditorFormField|null {
107
108         const search = (children: EditorUiElement[]): EditorFormField|null => {
109             for (const child of children) {
110                 if (child instanceof EditorFormField && child.getName() === name) {
111                     return child;
112                 } else if (child instanceof EditorContainerUiElement) {
113                     const matchingChild = search(child.getChildren());
114                     if (matchingChild) {
115                         return matchingChild;
116                     }
117                 }
118             }
119
120             return null;
121         };
122
123         return search(this.getChildren());
124     }
125
126     protected buildDOM(): HTMLElement {
127         const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans('Cancel')]);
128         const form = el('form', {}, [
129             ...this.children.map(child => child.getDOMElement()),
130             el('div', {class: 'editor-form-actions'}, [
131                 cancelButton,
132                 el('button', {type: 'submit', class: 'editor-form-action-primary'}, [this.trans(this.definition.submitText)]),
133             ])
134         ]);
135
136         form.addEventListener('submit', async (event) => {
137             event.preventDefault();
138             const formData = new FormData(form as HTMLFormElement);
139             const result = await this.definition.action(formData, this.getContext());
140             if (result && this.onSuccessfulSubmit) {
141                 this.onSuccessfulSubmit();
142             }
143         });
144
145         cancelButton.addEventListener('click', (event) => {
146             if (this.onCancel) {
147                 this.onCancel();
148             }
149         });
150
151         return form;
152     }
153 }
154
155 export class EditorFormTab extends EditorContainerUiElement {
156
157     protected definition: EditorFormTabDefinition;
158     protected fields: EditorFormField[];
159     protected id: string;
160
161     constructor(definition: EditorFormTabDefinition) {
162         const fields = definition.contents.map(fieldDef => new EditorFormField(fieldDef));
163         super(fields);
164
165         this.definition = definition;
166         this.fields = fields;
167         this.id = uniqueId();
168     }
169
170     public getLabel(): string {
171         return this.getContext().translate(this.definition.label);
172     }
173
174     public getId(): string {
175         return this.id;
176     }
177
178     protected buildDOM(): HTMLElement {
179         return el(
180             'div',
181             {
182                 class: 'editor-form-tab-content',
183                 role: 'tabpanel',
184                 id: `editor-tabpanel-${this.id}`,
185                 'aria-labelledby': `editor-tab-${this.id}`,
186             },
187             this.fields.map(f => f.getDOMElement())
188         );
189     }
190 }
191 export class EditorFormTabs extends EditorContainerUiElement {
192
193     protected definitions: EditorFormTabDefinition[] = [];
194     protected tabs: EditorFormTab[] = [];
195
196     constructor(definitions: EditorFormTabDefinition[]) {
197         const tabs: EditorFormTab[] = definitions.map(d => new EditorFormTab(d));
198         super(tabs);
199
200         this.definitions = definitions;
201         this.tabs = tabs;
202     }
203
204     protected buildDOM(): HTMLElement {
205         const controls: HTMLElement[] = [];
206         const contents: HTMLElement[] = [];
207
208         const selectTab = (tabIndex: number) => {
209             for (let i = 0; i < controls.length; i++) {
210                 controls[i].setAttribute('aria-selected', (i === tabIndex) ? 'true' : 'false');
211             }
212             for (let i = 0; i < contents.length; i++) {
213                 contents[i].hidden = !(i === tabIndex);
214             }
215         };
216
217         for (const tab of this.tabs) {
218             const button = el('button', {
219                 class: 'editor-form-tab-control',
220                 type: 'button',
221                 role: 'tab',
222                 id: `editor-tab-${tab.getId()}`,
223                 'aria-controls': `editor-tabpanel-${tab.getId()}`
224             }, [tab.getLabel()]);
225             contents.push(tab.getDOMElement());
226             controls.push(button);
227
228             button.addEventListener('click', event => {
229                 selectTab(controls.indexOf(button));
230             });
231         }
232
233         selectTab(0);
234
235         return el('div', {class: 'editor-form-tab-container'}, [
236             el('div', {class: 'editor-form-tab-controls'}, controls),
237             el('div', {class: 'editor-form-tab-contents'}, contents),
238         ]);
239     }
240 }