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