mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-12-15 07:33:36 +08:00
c8ccb2bac7
- Prevented ui shortcuts running in editor - Added form modal closing on submit - Fixed ability to escape lists via enter on empty last item
240 lines
7.7 KiB
TypeScript
240 lines
7.7 KiB
TypeScript
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<string, string>
|
|
}
|
|
|
|
interface EditorFormTabDefinition {
|
|
label: string;
|
|
contents: EditorFormFieldDefinition[];
|
|
}
|
|
|
|
export interface EditorFormDefinition {
|
|
submitText: string;
|
|
action: (formData: FormData, context: EditorUiContext) => Promise<boolean>;
|
|
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]}, [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 {
|
|
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;
|
|
protected onSuccessfulSubmit: 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<string, string>) {
|
|
for (const name of Object.keys(values)) {
|
|
const field = this.getFieldByName(name);
|
|
if (field) {
|
|
field.setValue(values[name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
setOnCancel(callback: () => void) {
|
|
this.onCancel = callback;
|
|
}
|
|
|
|
setOnSuccessfulSubmit(callback: () => void) {
|
|
this.onSuccessfulSubmit = 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', async (event) => {
|
|
event.preventDefault();
|
|
const formData = new FormData(form as HTMLFormElement);
|
|
const result = await this.definition.action(formData, this.getContext());
|
|
if (result && this.onSuccessfulSubmit) {
|
|
this.onSuccessfulSubmit();
|
|
}
|
|
});
|
|
|
|
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),
|
|
]);
|
|
}
|
|
} |