diff --git a/src/FormModal.ts b/src/FormModal.ts index 346c2cb6..90960a99 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -11,187 +11,190 @@ import { SvelteComponent } from "svelte"; export type SubmitFn = (formResult: FormResult) => void; export class FormModal extends Modal { - modalDefinition: FormDefinition; - formResult: ModalFormData; - svelteComponents: SvelteComponent[] = []; - constructor(app: App, modalDefinition: FormDefinition, private onSubmit: SubmitFn) { - super(app); - this.modalDefinition = modalDefinition; - this.formResult = {}; - } + modalDefinition: FormDefinition; + formResult: ModalFormData; + svelteComponents: SvelteComponent[] = []; + constructor(app: App, modalDefinition: FormDefinition, private onSubmit: SubmitFn) { + super(app); + this.modalDefinition = modalDefinition; + this.formResult = {}; + } - onOpen() { - const { contentEl } = this; - contentEl.createEl("h1", { text: this.modalDefinition.title }); - this.modalDefinition.fields.forEach((definition) => { - const fieldBase = new Setting(contentEl) - .setName(definition.label || definition.name) - .setDesc(definition.description); - // This intermediary constants are necessary so typescript can narrow down the proper types. - // without them, you will have to use the whole access path (definition.input.folder), - // and it is no specific enough when you use it in a switch statement. - const fieldInput = definition.input; - const type = fieldInput.type; - switch (type) { - case "text": - return fieldBase.addText((text) => - text.onChange(async (value) => { - this.formResult[definition.name] = value; - }) - ); - case "number": - return fieldBase.addText((text) => { - text.inputEl.type = "number"; - text.onChange(async (value) => { - if (value !== "") { - this.formResult[definition.name] = - Number(value) + ""; - } - }); - }); - case "date": - return fieldBase.addText((text) => { - text.inputEl.type = "date"; - text.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - case "time": - return fieldBase.addText((text) => { - text.inputEl.type = "time"; - text.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - case "datetime": - return fieldBase.addText((text) => { - text.inputEl.type = "datetime-local"; - text.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - case "toggle": - return fieldBase.addToggle((toggle) => { - toggle.setValue(false); - this.formResult[definition.name] = false; - return toggle.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - } - ); - case "note": - return fieldBase.addText((element) => { - new FileSuggest(this.app, element.inputEl, { - renderSuggestion(file) { - return file.basename; - }, - selectSuggestion(file) { - return file.basename; - }, - }, fieldInput.folder); - element.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - case "slider": - return fieldBase.addSlider((slider) => { - slider.setLimits(fieldInput.min, fieldInput.max, 1); - slider.setDynamicTooltip(); - slider.setValue(fieldInput.min); - slider.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - case 'multiselect': - { - this.formResult[definition.name] = this.formResult[definition.name] || [] - this.svelteComponents.push(new MultiSelect({ - target: fieldBase.controlEl, - props: { - selectedVales: this.formResult[definition.name] as string[], - availableOptions: ['a', 'b', 'c'], - setting: fieldBase, - } - })) - return; - } - case "dataview": - { - const query = fieldInput.query; - return fieldBase.addText((element) => { - new DataviewSuggest(element.inputEl, query, this.app); - element.onChange(async (value) => { - this.formResult[definition.name] = value; - }); - }); - } - case "select": - { - const source = fieldInput.source; - switch (source) { - case "fixed": - return fieldBase.addDropdown((element) => { - const options = fieldInput.options.reduce( - ( - acc: Record, - option - ) => { - acc[option.value] = option.label; - return acc; - }, - {} - ); - element.addOptions(options); - element.onChange(async (value) => { - this.formResult[definition.name] = - value; - }); - }); + onOpen() { + const { contentEl } = this; + contentEl.createEl("h1", { text: this.modalDefinition.title }); + this.modalDefinition.fields.forEach((definition) => { + const fieldBase = new Setting(contentEl) + .setName(definition.label || definition.name) + .setDesc(definition.description); + // This intermediary constants are necessary so typescript can narrow down the proper types. + // without them, you will have to use the whole access path (definition.input.folder), + // and it is no specific enough when you use it in a switch statement. + const fieldInput = definition.input; + const type = fieldInput.type; + switch (type) { + case "text": + return fieldBase.addText((text) => + text.onChange(async (value) => { + this.formResult[definition.name] = value; + }) + ); + case "number": + return fieldBase.addText((text) => { + text.inputEl.type = "number"; + text.onChange(async (value) => { + if (value !== "") { + this.formResult[definition.name] = + Number(value) + ""; + } + }); + }); + case "date": + return fieldBase.addText((text) => { + text.inputEl.type = "date"; + text.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + case "time": + return fieldBase.addText((text) => { + text.inputEl.type = "time"; + text.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + case "datetime": + return fieldBase.addText((text) => { + text.inputEl.type = "datetime-local"; + text.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + case "toggle": + return fieldBase.addToggle((toggle) => { + toggle.setValue(false); + this.formResult[definition.name] = false; + return toggle.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + } + ); + case "note": + return fieldBase.addText((element) => { + new FileSuggest(this.app, element.inputEl, { + renderSuggestion(file) { + return file.basename; + }, + selectSuggestion(file) { + return file.basename; + }, + }, fieldInput.folder); + element.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + case "slider": + return fieldBase.addSlider((slider) => { + slider.setLimits(fieldInput.min, fieldInput.max, 1); + slider.setDynamicTooltip(); + slider.setValue(fieldInput.min); + slider.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + case 'multiselect': + { + this.formResult[definition.name] = this.formResult[definition.name] || [] + const options = fieldInput.source == 'fixed' + ? fieldInput.options + : get_tfiles_from_folder(fieldInput.folder, this.app).map(file => file.basename); + this.svelteComponents.push(new MultiSelect({ + target: fieldBase.controlEl, + props: { + selectedVales: this.formResult[definition.name] as string[], + availableOptions: options, + setting: fieldBase, + } + })) + return; + } + case "dataview": + { + const query = fieldInput.query; + return fieldBase.addText((element) => { + new DataviewSuggest(element.inputEl, query, this.app); + element.onChange(async (value) => { + this.formResult[definition.name] = value; + }); + }); + } + case "select": + { + const source = fieldInput.source; + switch (source) { + case "fixed": + return fieldBase.addDropdown((element) => { + const options = fieldInput.options.reduce( + ( + acc: Record, + option + ) => { + acc[option.value] = option.label; + return acc; + }, + {} + ); + element.addOptions(options); + element.onChange(async (value) => { + this.formResult[definition.name] = + value; + }); + }); - case "notes": - return fieldBase.addDropdown((element) => { - const files = get_tfiles_from_folder(fieldInput.folder, this.app); - const options = files.reduce( - ( - acc: Record, - option - ) => { - acc[option.basename] = - option.basename; - return acc; - }, - {} - ); - element.addOptions(options); - element.onChange(async (value) => { - this.formResult[definition.name] = - value; - }); - }); - default: - exhaustiveGuard(source); - } - } - break; - default: - return exhaustiveGuard(type); + case "notes": + return fieldBase.addDropdown((element) => { + const files = get_tfiles_from_folder(fieldInput.folder, this.app); + const options = files.reduce( + ( + acc: Record, + option + ) => { + acc[option.basename] = + option.basename; + return acc; + }, + {} + ); + element.addOptions(options); + element.onChange(async (value) => { + this.formResult[definition.name] = + value; + }); + }); + default: + exhaustiveGuard(source); } - }); - new Setting(contentEl).addButton((btn) => - btn - .setButtonText("Submit") - .setCta() - .onClick(() => { - this.onSubmit(new FormResult(this.formResult, "ok")); - this.close(); - }) - ); - } + } + break; + default: + return exhaustiveGuard(type); + } + }); + new Setting(contentEl).addButton((btn) => + btn + .setButtonText("Submit") + .setCta() + .onClick(() => { + this.onSubmit(new FormResult(this.formResult, "ok")); + this.close(); + }) + ); + } - onClose() { - const { contentEl } = this; - this.svelteComponents.forEach(component => component.$destroy()) - contentEl.empty(); - this.formResult = {}; - } + onClose() { + const { contentEl } = this; + this.svelteComponents.forEach(component => component.$destroy()) + contentEl.empty(); + this.formResult = {}; + } } diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts index 649595c0..98faae22 100644 --- a/src/core/formDefinition.ts +++ b/src/core/formDefinition.ts @@ -5,91 +5,91 @@ */ export type FieldType = - | "text" - | "number" - | "date" - | "time" - | "datetime" - | "toggle"; + | "text" + | "number" + | "date" + | "time" + | "datetime" + | "toggle"; type selectFromNotes = { type: "select"; source: "notes", folder: string }; type inputSlider = { type: "slider"; min: number, max: number }; type inputNoteFromFolder = { type: "note"; folder: string }; type inputDataviewSource = { type: 'dataview', query: string }; type inputSelectFixed = { - type: "select"; - source: "fixed"; - options: { value: string; label: string }[]; + type: "select"; + source: "fixed"; + options: { value: string; label: string }[]; } type basicInput = { type: FieldType }; -type multiselect = { type: 'multiselect' }; +type multiselect = { type: 'multiselect', source: 'notes', folder: string } | { type: 'multiselect', source: 'fixed', options: string[] } type inputType = - | basicInput - | inputNoteFromFolder - | inputSlider - | selectFromNotes - | inputDataviewSource - | multiselect - | inputSelectFixed; + | basicInput + | inputNoteFromFolder + | inputSlider + | selectFromNotes + | inputDataviewSource + | multiselect + | inputSelectFixed; export const FieldTypeReadable: Record = { - "text": "Text", - "number": "Number", - "date": "Date", - "time": "Time", - "datetime": "DateTime", - "toggle": "Toggle", - "note": "Note", - "slider": "Slider", - "select": "Select", - "dataview": "Dataview", - "multiselect": "Multiselect", + "text": "Text", + "number": "Number", + "date": "Date", + "time": "Time", + "datetime": "DateTime", + "toggle": "Toggle", + "note": "Note", + "slider": "Slider", + "select": "Select", + "dataview": "Dataview", + "multiselect": "Multiselect", } as const; function isObject(input: unknown): input is Record { - return typeof input === "object" && input !== null; + return typeof input === "object" && input !== null; } export function isDataViewSource(input: unknown): input is inputDataviewSource { - return isObject(input) && input.type === 'dataview' && typeof input.query === 'string'; + return isObject(input) && input.type === 'dataview' && typeof input.query === 'string'; } export function isInputSlider(input: unknown): input is inputSlider { - if (!isObject(input)) { - return false; - } - if ('min' in input && 'max' in input && typeof input.min === 'number' && typeof input.max === 'number' && input.type === 'slider') { - return true; - } - return false + if (!isObject(input)) { + return false; + } + if ('min' in input && 'max' in input && typeof input.min === 'number' && typeof input.max === 'number' && input.type === 'slider') { + return true; + } + return false } export function isSelectFromNotes(input: unknown): input is selectFromNotes { - if (!isObject(input)) { - return false; - } - return input.type === "select" && input.source === "notes" && typeof input.folder === "string"; + if (!isObject(input)) { + return false; + } + return input.type === "select" && input.source === "notes" && typeof input.folder === "string"; } export function isInputNoteFromFolder(input: unknown): input is inputNoteFromFolder { - if (!isObject(input)) { - return false; - } - return input.type === "note" && typeof input.folder === "string"; + if (!isObject(input)) { + return false; + } + return input.type === "note" && typeof input.folder === "string"; } export function isInputSelectFixed(input: unknown): input is inputSelectFixed { - if (!isObject(input)) { - return false; - } - return input.type === "select" && input.source === "fixed" && Array.isArray(input.options) && input.options.every((option: unknown) => { - return isObject(option) && typeof option.value === "string" && typeof option.label === "string"; - }) + if (!isObject(input)) { + return false; + } + return input.type === "select" && input.source === "fixed" && Array.isArray(input.options) && input.options.every((option: unknown) => { + return isObject(option) && typeof option.value === "string" && typeof option.label === "string"; + }) } export type AllFieldTypes = inputType['type'] export type FieldDefinition = { - name: string; - label?: string; - description: string; - input: inputType; + name: string; + label?: string; + description: string; + input: inputType; } /** * FormDefinition is an already valid form, ready to be used in the form modal. @@ -102,104 +102,104 @@ export type FieldDefinition = { * @param type - The type of the field. Can be one of "text", "number", "date", "time", "datetime", "toggle". */ export type FormDefinition = { - title: string; - name: string; - fields: FieldDefinition[]; + title: string; + name: string; + fields: FieldDefinition[]; }; // When an input is in edit state, it is represented by this type. // It has all the possible values, and then you need to narrow it down // to the actual type. export type EditableInput = { - type: AllFieldTypes; - source?: "notes" | "fixed"; - folder?: string; - min?: number; - max?: number; - options?: { value: string; label: string }[]; - query?: string; + type: AllFieldTypes; + source?: "notes" | "fixed"; + folder?: string; + min?: number; + max?: number; + options?: { value: string; label: string }[]; + query?: string; }; export type EditableFormDefinition = { - title: string; - name: string; - fields: { - name: string; - label?: string; - description: string; - input: EditableInput; - }[]; + title: string; + name: string; + fields: { + name: string; + label?: string; + description: string; + input: EditableInput; + }[]; }; export function isValidBasicInput(input: unknown): input is basicInput { - if (!isObject(input)) { - return false; - } - return ["text", "number", "date", "time", "datetime", "toggle"].includes(input.type as string); + if (!isObject(input)) { + return false; + } + return ["text", "number", "date", "time", "datetime", "toggle"].includes(input.type as string); } export function isInputTypeValid(input: unknown): input is inputType { - if (isValidBasicInput(input)) { - return true; - } else if (isInputNoteFromFolder(input)) { - return true; - } else if (isInputSlider(input)) { - return true; - } else if (isSelectFromNotes(input)) { - return true; - } else if (isInputSelectFixed(input)) { - return true; - } else if (isDataViewSource(input)) { - return true; - } else { - return false; - } + if (isValidBasicInput(input)) { + return true; + } else if (isInputNoteFromFolder(input)) { + return true; + } else if (isInputSlider(input)) { + return true; + } else if (isSelectFromNotes(input)) { + return true; + } else if (isInputSelectFixed(input)) { + return true; + } else if (isDataViewSource(input)) { + return true; + } else { + return false; + } } export function decodeInputType(input: EditableInput): inputType | null { - if (isInputSlider(input)) { - return { type: "slider", min: input.min, max: input.max }; - } else if (isSelectFromNotes(input)) { - return { type: "select", source: "notes", folder: input.folder }; - } else if (isInputNoteFromFolder(input)) { - return { type: "note", folder: input.folder! }; - } else if (isInputSelectFixed(input)) { - return { type: "select", source: "fixed", options: input.options }; - } else if (isValidBasicInput(input)) { - return { type: input.type }; - } else { - return null; - } + if (isInputSlider(input)) { + return { type: "slider", min: input.min, max: input.max }; + } else if (isSelectFromNotes(input)) { + return { type: "select", source: "notes", folder: input.folder }; + } else if (isInputNoteFromFolder(input)) { + return { type: "note", folder: input.folder! }; + } else if (isInputSelectFixed(input)) { + return { type: "select", source: "fixed", options: input.options }; + } else if (isValidBasicInput(input)) { + return { type: input.type }; + } else { + return null; + } } export function isFieldValid(input: unknown): input is FieldDefinition { - if (!isObject(input)) { - return false; - } - if (typeof input.name !== "string" || input.name.length === 0) { - return false; - } - if (typeof input.description !== "string") { - return false; - } - if (input.label !== undefined && typeof input.label !== "string") { - return false; - } - console.log('basic input fields are valid') - return isInputTypeValid(input.input); + if (!isObject(input)) { + return false; + } + if (typeof input.name !== "string" || input.name.length === 0) { + return false; + } + if (typeof input.description !== "string") { + return false; + } + if (input.label !== undefined && typeof input.label !== "string") { + return false; + } + console.log('basic input fields are valid') + return isInputTypeValid(input.input); } export function isValidFormDefinition(input: unknown): input is FormDefinition { - if (!isObject(input)) { - return false; - } - if (typeof input.title !== "string") { - return false; - } - if (typeof input.name !== "string" || input.name === '') { - return false; - } - console.log('basic is valid'); - return Array.isArray(input.fields) && input.fields.every(isFieldValid); + if (!isObject(input)) { + return false; + } + if (typeof input.title !== "string") { + return false; + } + if (typeof input.name !== "string" || input.name === '') { + return false; + } + console.log('basic is valid'); + return Array.isArray(input.fields) && input.fields.every(isFieldValid); } diff --git a/src/exampleModalDefinition.ts b/src/exampleModalDefinition.ts index e36dd9f7..07b2f9c1 100644 --- a/src/exampleModalDefinition.ts +++ b/src/exampleModalDefinition.ts @@ -41,9 +41,15 @@ export const exampleModalDefinition: FormDefinition = { }, { name: "multi_example", - label: "Multi select example", - description: "Pick many", - input: { type: "multiselect" }, + label: "Multi select folder", + description: "Allows to pick many notes from a folder", + input: { type: "multiselect", source: "notes", folder: "Books" }, + }, + { + name: "multi_example_2", + label: "Multi select fixed", + description: "Allows to pick many notes from a fixed list", + input: { type: "multiselect", source: "fixed", options: ['Android', 'iOS', 'Windows', 'MacOS', 'Linux', 'Solaris', 'MS2'] }, }, { name: "best_fried",