From 179b6e136cf05590d6c42d7e2b39e52a0fdad4ab Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Thu, 14 Sep 2023 20:07:07 +0200
Subject: [PATCH 1/7] WIP: multi select

---
 src/FormModal.ts                        | 17 +++++++++------
 src/core/formDefinition.ts              |  3 +++
 src/suggesters/MultiSelect.ts           | 28 +++++++++++++++++++++++++
 src/views/components/MultiSelect.svelte | 21 +++++++++++++++++++
 4 files changed, 63 insertions(+), 6 deletions(-)
 create mode 100644 src/suggesters/MultiSelect.ts
 create mode 100644 src/views/components/MultiSelect.svelte

diff --git a/src/FormModal.ts b/src/FormModal.ts
index ca2f1b0f..c2ceac92 100644
--- a/src/FormModal.ts
+++ b/src/FormModal.ts
@@ -1,4 +1,5 @@
 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";
@@ -101,14 +102,18 @@ export class FormModal extends Modal {
 							this.formResult[definition.name] = value;
 						});
 					});
+				case 'multiselect':
+					return fieldBase.controlEl.appendChild(new MultiSelect({}))
 				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;
+					{
+						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;
diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts
index 2c5354b6..649595c0 100644
--- a/src/core/formDefinition.ts
+++ b/src/core/formDefinition.ts
@@ -22,12 +22,14 @@ type inputSelectFixed = {
 	options: { value: string; label: string }[];
 }
 type basicInput = { type: FieldType };
+type multiselect = { type: 'multiselect' };
 type inputType =
 	| basicInput
 	| inputNoteFromFolder
 	| inputSlider
 	| selectFromNotes
 	| inputDataviewSource
+	| multiselect
 	| inputSelectFixed;
 
 export const FieldTypeReadable: Record<AllFieldTypes, string> = {
@@ -41,6 +43,7 @@ export const FieldTypeReadable: Record<AllFieldTypes, string> = {
 	"slider": "Slider",
 	"select": "Select",
 	"dataview": "Dataview",
+	"multiselect": "Multiselect",
 } as const;
 
 function isObject(input: unknown): input is Record<string, unknown> {
diff --git a/src/suggesters/MultiSelect.ts b/src/suggesters/MultiSelect.ts
new file mode 100644
index 00000000..6ff3b4a1
--- /dev/null
+++ b/src/suggesters/MultiSelect.ts
@@ -0,0 +1,28 @@
+import { TextInputSuggest } from "./suggest";
+
+export class MultiSelect extends TextInputSuggest<string> {
+	content: Set<string>;
+
+	constructor(input: HTMLInputElement, content: Set<string>, private onSelect: (value: string) => void) {
+		super(app, input);
+		this.content = content;
+	}
+
+	getSuggestions(inputStr: string): string[] {
+		const lowerCaseInputStr = inputStr.toLowerCase();
+		return [...this.content].filter((content) =>
+			content.contains(lowerCaseInputStr)
+		);
+	}
+
+	renderSuggestion(content: string, el: HTMLElement): void {
+		el.setText(content);
+	}
+
+	selectSuggestion(content: string): void {
+		this.inputEl.value = "";
+		this.inputEl.trigger("input");
+		this.onSelect(content);
+		this.close();
+	}
+}
diff --git a/src/views/components/MultiSelect.svelte b/src/views/components/MultiSelect.svelte
new file mode 100644
index 00000000..aa2148f8
--- /dev/null
+++ b/src/views/components/MultiSelect.svelte
@@ -0,0 +1,21 @@
+<script lang="ts">
+	import { MultiSelect } from "src/suggesters/MultiSelect";
+
+	export let selectedVales: string[] = [];
+	export let availableOptions: string[] = [];
+
+	function createInput(element){
+		new MultiSelect(element, availableOptions, (selected) => {
+			selectedVales.push(selected)
+		});
+	}
+</script >
+
+<div >
+
+	<input use:createInput type="text" class="form-control" placeholder="Select" />
+
+	{#each selectedVales as value}
+		<div class="badge bg-primary">{value}</div>
+	{/each}
+</div>

From 9688ffc65b3cc0801e69acf78751737c6647c53a Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 12:49:54 +0200
Subject: [PATCH 2/7] WIP: multi-select UI

---
 src/FormModal.ts                              | 350 +++++++++---------
 src/FormResult.ts                             |  62 ++--
 src/exampleModalDefinition.ts                 | 181 ++++-----
 .../{MultiSelect.ts => MultiSuggest.ts}       |   7 +-
 src/views/components/MultiSelect.svelte       | 134 ++++++-
 5 files changed, 432 insertions(+), 302 deletions(-)
 rename src/suggesters/{MultiSelect.ts => MultiSuggest.ts} (84%)

diff --git a/src/FormModal.ts b/src/FormModal.ts
index c2ceac92..346c2cb6 100644
--- a/src/FormModal.ts
+++ b/src/FormModal.ts
@@ -6,180 +6,192 @@ 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 'multiselect':
-					return fieldBase.controlEl.appendChild(new MultiSelect({}))
-				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] || []
+                        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<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 = {};
+    }
 }
diff --git a/src/FormResult.ts b/src/FormResult.ts
index 2ec34733..4df9df70 100644
--- a/src/FormResult.ts
+++ b/src/FormResult.ts
@@ -3,37 +3,37 @@ import { stringifyYaml } from "obsidian";
 type ResultStatus = "ok" | "cancelled";
 
 // We don't use FormData because that is builtin browser API
-export type ModalFormData = { [key: string]: string | boolean | number };
+export type ModalFormData = { [key: string]: string | boolean | number | string[] };
 
 export default class FormResult {
-	constructor(private data: ModalFormData, public status: ResultStatus) { }
-	asFrontmatterString() {
-		return stringifyYaml(this.data);
-	}
-	/**
-	 * Return the current data as a block of dataview properties
-	 * @returns string
-	 */
-	asDataviewProperties(): string {
-		return Object.entries(this.data)
-			.map(([key, value]) => `${key}:: ${value}`)
-			.join("\n");
-	}
-	/**
-	Returns a copy of the data contained on this result.
-	*/
-	getData() {
-		return { ...this.data };
-	}
-	/**
-	 * Returns the data formatted as a string matching the provided
-	 * template.
-	 */
-	asString(template: string): string {
-		let result = template;
-		for (const [key, value] of Object.entries(this.data)) {
-			result = result.replace(new RegExp(`{{${key}}}`, 'g'), value + "");
-		}
-		return result;
-	}
+  constructor(private data: ModalFormData, public status: ResultStatus) { }
+  asFrontmatterString() {
+    return stringifyYaml(this.data);
+  }
+  /**
+   * Return the current data as a block of dataview properties
+   * @returns string
+   */
+  asDataviewProperties(): string {
+    return Object.entries(this.data)
+      .map(([key, value]) => `${key}:: ${value}`)
+      .join("\n");
+  }
+  /**
+  Returns a copy of the data contained on this result.
+  */
+  getData() {
+    return { ...this.data };
+  }
+  /**
+   * Returns the data formatted as a string matching the provided
+   * template.
+   */
+  asString(template: string): string {
+    let result = template;
+    for (const [key, value] of Object.entries(this.data)) {
+      result = result.replace(new RegExp(`{{${key}}}`, 'g'), value + "");
+    }
+    return result;
+  }
 }
diff --git a/src/exampleModalDefinition.ts b/src/exampleModalDefinition.ts
index 13375bc7..e36dd9f7 100644
--- a/src/exampleModalDefinition.ts
+++ b/src/exampleModalDefinition.ts
@@ -1,94 +1,99 @@
 import type { FormDefinition } from "./core/formDefinition";
 
 export const exampleModalDefinition: FormDefinition = {
-	title: "Example form",
-	name: "example-form",
-	fields: [
-		{
-			name: "Name",
-			description: "It is named how?",
-			input: { type: "text" },
-		},
-		{
-			name: "age",
-			label: "Age",
-			description: "How old",
-			input: { type: "number" },
-		},
-		{
-			name: "dateOfBirth",
-			label: "Date of Birth",
-			description: "When were you born?",
-			input: { type: "date" },
-		},
-		{
-			name: "timeOfDay",
-			label: "Time of day",
-			description: "The time you can do this",
-			input: { type: "time" },
-		},
-		{
-			name: "is_family",
-			label: "Is family",
-			description: "If it is part of the family",
-			input: { type: "toggle" },
-		},
-		{
-			name: "favorite_book",
-			label: "Favorite book",
-			description: "Pick one",
-			input: { type: "note", folder: "Books" },
-		},
+  title: "Example form",
+  name: "example-form",
+  fields: [
+    {
+      name: "Name",
+      description: "It is named how?",
+      input: { type: "text" },
+    },
+    {
+      name: "age",
+      label: "Age",
+      description: "How old",
+      input: { type: "number" },
+    },
+    {
+      name: "dateOfBirth",
+      label: "Date of Birth",
+      description: "When were you born?",
+      input: { type: "date" },
+    },
+    {
+      name: "timeOfDay",
+      label: "Time of day",
+      description: "The time you can do this",
+      input: { type: "time" },
+    },
+    {
+      name: "is_family",
+      label: "Is family",
+      description: "If it is part of the family",
+      input: { type: "toggle" },
+    },
+    {
+      name: "favorite_book",
+      label: "Favorite book",
+      description: "Pick one",
+      input: { type: "note", folder: "Books" },
+    },
+    {
+      name: "multi_example",
+      label: "Multi select example",
+      description: "Pick many",
+      input: { type: "multiselect" },
+    },
+    {
+      name: "best_fried",
+      label: "Best friend",
+      description: "Pick one",
+      input: {
+        type: 'select',
+        source: 'notes',
+        folder: 'People'
+      }
+    },
+    {
+      name: 'dataview_example',
+      label: 'Dataview example',
+      description: 'Only people matching the dataview query will be shown',
+      input: {
+        type: 'dataview',
+        query: 'dv.pages("#person").filter(p => p.age < 30).map(p => p.file.name)'
+      }
+    },
+    {
+      name: "friendship_level",
+      label: "Friendship level",
+      description: "How good friends are you?",
+      input: {
+        type: 'slider',
+        min: 0,
+        max: 10
+      }
+    },
+    {
+      name: "favorite_meal",
+      label: "Favorite meal",
+      description: "Pick one option",
+      input: {
+        type: "select", source: "fixed", options: [
+          { value: "pizza", label: "🍕 Pizza" },
+          { value: "pasta", label: "🍝 Pasta" },
+          { value: "burger", label: "🍔 Burger" },
+          { value: "salad", label: "🥗 Salad" },
+          { value: "steak", label: "🥩 Steak" },
+          { value: "sushi", label: "🍣 Sushi" },
+          { value: "ramen", label: "🍜 Ramen" },
+          { value: "tacos", label: "🌮 Tacos" },
+          { value: "fish", label: "🐟 Fish" },
+          { value: "chicken", label: "🍗 Chicken" }
+        ]
+      },
+    },
 
-		{
-			name: "best_fried",
-			label: "Best friend",
-			description: "Pick one",
-			input: {
-				type: 'select',
-				source: 'notes',
-				folder: 'People'
-			}
-		},
-		{
-			name: 'dataview_example',
-			label: 'Dataview example',
-			description: 'Only people matching the dataview query will be shown',
-			input: {
-				type: 'dataview',
-				query: 'dv.pages("#person").filter(p => p.age < 30).map(p => p.file.name)'
-			}
-		},
-		{
-			name: "friendship_level",
-			label: "Friendship level",
-			description: "How good friends are you?",
-			input: {
-				type: 'slider',
-				min: 0,
-				max: 10
-			}
-		},
-		{
-			name: "favorite_meal",
-			label: "Favorite meal",
-			description: "Pick one option",
-			input: {
-				type: "select", source: "fixed", options: [
-					{ value: "pizza", label: "🍕 Pizza" },
-					{ value: "pasta", label: "🍝 Pasta" },
-					{ value: "burger", label: "🍔 Burger" },
-					{ value: "salad", label: "🥗 Salad" },
-					{ value: "steak", label: "🥩 Steak" },
-					{ value: "sushi", label: "🍣 Sushi" },
-					{ value: "ramen", label: "🍜 Ramen" },
-					{ value: "tacos", label: "🌮 Tacos" },
-					{ value: "fish", label: "🐟 Fish" },
-					{ value: "chicken", label: "🍗 Chicken" }
-				]
-			},
-		},
 
-
-	],
+  ],
 };
diff --git a/src/suggesters/MultiSelect.ts b/src/suggesters/MultiSuggest.ts
similarity index 84%
rename from src/suggesters/MultiSelect.ts
rename to src/suggesters/MultiSuggest.ts
index 6ff3b4a1..c154aefa 100644
--- a/src/suggesters/MultiSelect.ts
+++ b/src/suggesters/MultiSuggest.ts
@@ -1,6 +1,6 @@
 import { TextInputSuggest } from "./suggest";
 
-export class MultiSelect extends TextInputSuggest<string> {
+export class MultiSuggest extends TextInputSuggest<string> {
 	content: Set<string>;
 
 	constructor(input: HTMLInputElement, content: Set<string>, private onSelect: (value: string) => void) {
@@ -20,9 +20,10 @@ export class MultiSelect extends TextInputSuggest<string> {
 	}
 
 	selectSuggestion(content: string): void {
-		this.inputEl.value = "";
-		this.inputEl.trigger("input");
 		this.onSelect(content);
+		this.inputEl.value = "";
+		// this.inputEl.trigger("blur");
+		this.inputEl.blur()
 		this.close();
 	}
 }
diff --git a/src/views/components/MultiSelect.svelte b/src/views/components/MultiSelect.svelte
index aa2148f8..0231257d 100644
--- a/src/views/components/MultiSelect.svelte
+++ b/src/views/components/MultiSelect.svelte
@@ -1,21 +1,133 @@
 <script lang="ts">
-	import { MultiSelect } from "src/suggesters/MultiSelect";
+	import type { Setting } from "obsidian";
+	import { MultiSuggest } from "../../suggesters/MultiSuggest";
 
 	export let selectedVales: string[] = [];
 	export let availableOptions: string[] = [];
+	// We take the setting to make it consistent with the other input components
+	export let setting: Setting;
 
-	function createInput(element){
-		new MultiSelect(element, availableOptions, (selected) => {
-			selectedVales.push(selected)
+	setting.settingEl.setCssStyles({
+		alignItems: "baseline",
+	});
+
+	let remainingOptions = new Set(availableOptions);
+
+	function createInput(element: HTMLInputElement) {
+		new MultiSuggest(element, remainingOptions, (selected) => {
+			selectedVales.push(selected);
+			selectedVales = [...new Set(selectedVales)];
+			remainingOptions.delete(selected);
 		});
 	}
-</script >
+	function reomoveValue(value: string) {
+		selectedVales = selectedVales.filter((v) => v !== value);
+		remainingOptions.add(value);
+	}
+</script>
 
-<div >
+<div class="multi-select-root">
+	<input
+		use:createInput
+		type="text"
+		class="form-control"
+		placeholder="Select"
+	/>
+	<div class="badges">
+		{#each selectedVales as value}
+			<div class="badge">
+				<span>{value}</span>
+				<button on:click={() => reomoveValue(value)}>
+					<svg
+						xmlns="http://www.w3.org/2000/svg"
+						width="24"
+						height="24"
+						viewBox="0 0 24 24"
+						fill="none"
+						stroke="currentColor"
+						stroke-width="2"
+						stroke-linecap="round"
+						stroke-linejoin="round"
+						class="svg-icon lucide-x"
+						><line x1="18" y1="6" x2="6" y2="18" /><line
+							x1="6"
+							y1="6"
+							x2="18"
+							y2="18"
+						/></svg
+					></button
+				>
+			</div>
+		{:else}
+			<div class="badge hidden">
+				<span>Nothing selected</span>
+			</div>
+		{/each}
+	</div>
+</div>
 
-	<input use:createInput type="text" class="form-control" placeholder="Select" />
+<style>
+	.multi-select-root {
+		display: flex;
+		flex-direction: column;
+		gap: 0.5rem;
+		flex: 1;
+		--button-size: 1.5rem;
+	}
+	.badge {
+		--icon-size: var(--icon-xs);
+		--icon-stroke: var(--icon-xs-stroke-width);
+		display: flex;
+		align-items: center;
+		background-color: var(--pill-background);
+		border: var(--pill-border-width) solid var(--pill-border-color);
+		border-radius: var(--pill-radius);
+		color: var(--pill-color);
+		cursor: var(--cursor);
+		font-weight: var(--pill-weight);
+		padding: var(--pill-padding-y);
+		line-height: 1;
+		max-width: 100%;
+		gap: var(--size-4-2);
+		justify-content: center;
+		align-items: center;
+	}
+	.hidden {
+		visibility: hidden;
+	}
+	.hidden span {
+		height: var(--button-size);
+	}
+	.badge span {
+		white-space: nowrap;
+		overflow: hidden;
+		text-overflow: ellipsis;
+		min-width: 1rem;
+	}
+	.badges {
+		display: flex;
+		flex-wrap: wrap;
+		gap: 8px;
+		min-height: 2rem;
+		padding: 0.5rem 0 0 0;
+	}
+	button {
+		/* reset button styles */
 
-	{#each selectedVales as value}
-		<div class="badge bg-primary">{value}</div>
-	{/each}
-</div>
+		background: none;
+		border: none;
+		color: inherit;
+		font: inherit;
+		line-height: inherit;
+		padding: 0;
+		-webkit-appearance: none;
+		-moz-appearance: none;
+		-o-appearance: none;
+		appearance: none;
+		box-shadow: none;
+		border: none;
+		cursor: pointer;
+		height: var(--button-size);
+		width: var(--button-size);
+	}
+</style>

From e3ee49281f39ed6708cdd48f8bba527e74d5584b Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 13:27:07 +0200
Subject: [PATCH 3/7] feat: multi-select v1

---
 src/FormModal.ts              | 361 +++++++++++++++++-----------------
 src/core/formDefinition.ts    | 260 ++++++++++++------------
 src/exampleModalDefinition.ts |  12 +-
 3 files changed, 321 insertions(+), 312 deletions(-)

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<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);
+              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);
             }
-        });
-        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<AllFieldTypes, string> = {
-	"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<string, unknown> {
-	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",

From fe5610818ad725303afea61b6f15a5972beed7fd Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 13:28:34 +0200
Subject: [PATCH 4/7] chore: docs

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 1dd16e23..27900126 100644
--- a/README.md
+++ b/README.md
@@ -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

From 3eff7a89c7177faa47940c16d04f97f5d72a3a21 Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 14:34:12 +0200
Subject: [PATCH 5/7] feat: multi select UI builder

---
 src/core/formDefinition.ts                    | 264 +++++++++---------
 src/views/FormBuilder.svelte                  |   2 +-
 src/views/components/inputBuilderMulti.svelte |  32 +++
 3 files changed, 168 insertions(+), 130 deletions(-)
 create mode 100644 src/views/components/inputBuilderMulti.svelte

diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts
index 98faae22..e95710d6 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', 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<AllFieldTypes, string> = {
-  "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<string, unknown> {
-  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,110 @@ 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 isMultiSelect(input: unknown): input is multiselect {
+	return isObject(input)
+		&& input.type === 'multiselect'
+		&& (
+			(input.source === 'notes' && typeof input.folder === 'string') || (input.source === 'fixed' && Array.isArray(input.options))
+		)
 }
 
 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;
-  }
+	switch (true) {
+		case isValidBasicInput(input):
+		case isInputNoteFromFolder(input):
+		case isInputSlider(input):
+		case isSelectFromNotes(input):
+		case isInputSelectFixed(input):
+		case isDataViewSource(input):
+		case isMultiSelect(input):
+			return true;
+		default:
+			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/views/FormBuilder.svelte b/src/views/FormBuilder.svelte
index 765ec925..6ef989c2 100644
--- a/src/views/FormBuilder.svelte
+++ b/src/views/FormBuilder.svelte
@@ -205,7 +205,7 @@
 						</div>
 					</div>
 					<div class="flex gap1">
-						{#if field.input.type === "select"}
+						{#if field.input.type === "select" || field.input.type === "multiselect"}
 							{@const source_id = `source_${index}`}
 							<div class="flex column gap1">
 								<label for={source_id}>Source</label>
diff --git a/src/views/components/inputBuilderMulti.svelte b/src/views/components/inputBuilderMulti.svelte
new file mode 100644
index 00000000..522e54d3
--- /dev/null
+++ b/src/views/components/inputBuilderMulti.svelte
@@ -0,0 +1,32 @@
+<script lang="ts">
+	import FormRow from "./FormRow.svelte";
+
+	export let index: number;
+	export let value: string = "";
+	$: id = `dataview_${index}`;
+</script>
+
+<FormRow label="Dataview Query" {id}>
+	<span class="modal-form-hint">
+		This is a <a
+			href="https://blacksmithgu.github.io/obsidian-dataview/api/intro/"
+			>Dataview</a
+		>
+		query that will be used to populate the input suggestions. You should provide
+		a query that returns a list of strings, for example:
+		<pre class="language-js"><code
+				>dv.pages('#tag').map(p => p.file.name)</code
+			></pre>
+	</span>
+	<textarea
+		{id}
+		bind:value
+		name="dataview_query"
+		class="form-control"
+		rows="3"
+		placeholder="dv.pages('#tag').map(p => p.file.name)"
+	/>
+</FormRow>
+
+<style>
+</style>

From c799a28a74f99f2c2b9cbe6f845a2085b5809c5e Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 14:41:18 +0200
Subject: [PATCH 6/7] chore: remove unused

---
 src/views/components/inputBuilderMulti.svelte | 32 -------------------
 1 file changed, 32 deletions(-)
 delete mode 100644 src/views/components/inputBuilderMulti.svelte

diff --git a/src/views/components/inputBuilderMulti.svelte b/src/views/components/inputBuilderMulti.svelte
deleted file mode 100644
index 522e54d3..00000000
--- a/src/views/components/inputBuilderMulti.svelte
+++ /dev/null
@@ -1,32 +0,0 @@
-<script lang="ts">
-	import FormRow from "./FormRow.svelte";
-
-	export let index: number;
-	export let value: string = "";
-	$: id = `dataview_${index}`;
-</script>
-
-<FormRow label="Dataview Query" {id}>
-	<span class="modal-form-hint">
-		This is a <a
-			href="https://blacksmithgu.github.io/obsidian-dataview/api/intro/"
-			>Dataview</a
-		>
-		query that will be used to populate the input suggestions. You should provide
-		a query that returns a list of strings, for example:
-		<pre class="language-js"><code
-				>dv.pages('#tag').map(p => p.file.name)</code
-			></pre>
-	</span>
-	<textarea
-		{id}
-		bind:value
-		name="dataview_query"
-		class="form-control"
-		rows="3"
-		placeholder="dv.pages('#tag').map(p => p.file.name)"
-	/>
-</FormRow>
-
-<style>
-</style>

From f6f52270dfa03924f0f9beb09215982eb0b3ba48 Mon Sep 17 00:00:00 2001
From: Danielo Rodriguez <rdanielo@gmail.com>
Date: Fri, 15 Sep 2023 14:43:53 +0200
Subject: [PATCH 7/7] chore: experiment with sync-version

---
 .github/workflows/sync-version.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/sync-version.yml b/.github/workflows/sync-version.yml
index c7d31e83..e567fbdc 100644
--- a/.github/workflows/sync-version.yml
+++ b/.github/workflows/sync-version.yml
@@ -5,7 +5,6 @@ name: sync versions
 
 on:
     pull_request:
-        types: [labeled]
 
 permissions:
     contents: write
@@ -13,6 +12,7 @@ permissions:
 
 jobs:
     sync-versions:
+        if: "${{ github.event.label.name == 'autorelease: pending' }}"
         runs-on: ubuntu-latest
         steps:
             - uses: actions/checkout@v3