Skip to content

Commit

Permalink
Merge pull request #35 from danielo515/feat-multi-select
Browse files Browse the repository at this point in the history
multi select input
  • Loading branch information
danielo515 authored Sep 15, 2023
2 parents 72cfc3e + f6f5227 commit 33ec614
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 300 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sync-version.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ name: sync versions

on:
pull_request:
types: [labeled]

permissions:
contents: write
pull-requests: write

jobs:
sync-versions:
if: "${{ github.event.label.name == 'autorelease: pending' }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ https://github.com/danielo515/obsidian-modal-form/assets/2270425/542974aa-c58b-4
- free text
- text with autocompletion for note names (from a folder or root)
- text with autocompletion from a dataview query (requires dataview plugin)
- multiple choice input
- select from a list
- list of fixed values
- list of notes from a folder
Expand Down
350 changes: 185 additions & 165 deletions src/FormModal.ts
Original file line number Diff line number Diff line change
@@ -1,180 +1,200 @@
import { App, Modal, Setting } from "obsidian";
import MultiSelect from "./views/components/MultiSelect.svelte";
import FormResult, { type ModalFormData } from "./FormResult";
import { exhaustiveGuard } from "./safety";
import { get_tfiles_from_folder } from "./utils/files";
import type { FormDefinition } from "./core/formDefinition";
import { FileSuggest } from "./suggesters/suggestFile";
import { DataviewSuggest } from "./suggesters/suggestFromDataview";
import { SvelteComponent } from "svelte";

export type SubmitFn = (formResult: FormResult) => void;

export class FormModal extends Modal {
modalDefinition: FormDefinition;
formResult: ModalFormData;
onSubmit: SubmitFn;
constructor(app: App, modalDefinition: FormDefinition, onSubmit: SubmitFn) {
super(app);
this.modalDefinition = modalDefinition;
this.onSubmit = onSubmit;
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 "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<string, string>,
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<string, string>,
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<string, string>,
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);
}
});
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Submit")
.setCta()
.onClick(() => {
this.onSubmit(new FormResult(this.formResult, "ok"));
this.close();
})
);
}
case "notes":
return fieldBase.addDropdown((element) => {
const files = get_tfiles_from_folder(fieldInput.folder, this.app);
const options = files.reduce(
(
acc: Record<string, string>,
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);
}
});
new Setting(contentEl).addButton((btn) =>
btn
.setButtonText("Submit")
.setCta()
.onClick(() => {
this.onSubmit(new FormResult(this.formResult, "ok"));
this.close();
})
);
}

onClose() {
const { contentEl } = this;
contentEl.empty();
this.formResult = {};
}
onClose() {
const { contentEl } = this;
this.svelteComponents.forEach(component => component.$destroy())
contentEl.empty();
this.formResult = {};
}
}
Loading

0 comments on commit 33ec614

Please sign in to comment.