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