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' | 'textarea'; } export interface EditorSelectFormFieldDefinition extends EditorFormFieldDefinition { type: 'select', valuesByLabel: Record } interface EditorFormTabDefinition { label: string; contents: EditorFormFieldDefinition[]; } export interface EditorFormDefinition { submitText: string; action: (formData: FormData, context: EditorUiContext) => Promise; fields: (EditorFormFieldDefinition|EditorUiBuilderDefinition)[]; } export class EditorFormField extends EditorUiElement { protected definition: EditorFormFieldDefinition; constructor(definition: EditorFormFieldDefinition) { super(); this.definition = definition; } setValue(value: string) { const input = this.getDOMElement().querySelector('input,select,textarea') as HTMLInputElement; input.value = value; } getName(): string { return this.definition.name; } 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 if (this.definition.type === 'textarea') { input = el('textarea', {id, name: this.definition.name, class: 'editor-form-field-input'}); } 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; protected onCancel: null|(() => void) = null; constructor(definition: EditorFormDefinition) { let children: (EditorFormField|EditorUiElement)[] = definition.fields.map(fieldDefinition => { if (isUiBuilderDefinition(fieldDefinition)) { return fieldDefinition.build(); } return new EditorFormField(fieldDefinition) }); super(children); this.definition = definition; } setValues(values: Record) { for (const name of Object.keys(values)) { const field = this.getFieldByName(name); if (field) { field.setValue(values[name]); } } } setOnCancel(callback: () => void) { this.onCancel = callback; } protected getFieldByName(name: string): EditorFormField|null { 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 search(this.getChildren()); } protected buildDOM(): HTMLElement { const cancelButton = el('button', {type: 'button', class: 'editor-form-action-secondary'}, [this.trans('Cancel')]); 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) => { if (this.onCancel) { this.onCancel(); } }); return form; } } export class EditorFormTab extends EditorContainerUiElement { protected definition: EditorFormTabDefinition; protected fields: EditorFormField[]; protected id: string; constructor(definition: EditorFormTabDefinition) { const fields = definition.contents.map(fieldDef => 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), ]); } }