From 6346b7a808c06ec46f2367999912a0f1a519e4a4 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Mon, 16 Oct 2023 12:53:34 +0200 Subject: [PATCH] refactor: migrate multi suggest to obsidian native --- package-lock.json | 12 +- package.json | 2 +- src/FormModal.ts | 1 + src/suggesters/MultiSuggest.ts | 46 +-- src/suggesters/suggest.ts | 395 ++++++++++++------------ src/views/ManageFormsView.ts | 3 +- src/views/components/MultiSelect.svelte | 250 +++++++-------- 7 files changed, 358 insertions(+), 351 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3aac6530..37125204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-modal-form", - "version": "1.10.0", + "version": "1.16.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-modal-form", - "version": "1.10.0", + "version": "1.16.4", "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.8", @@ -20,7 +20,7 @@ "builtin-modules": "3.3.0", "esbuild": "0.17.3", "esbuild-svelte": "^0.8.0", - "obsidian": "latest", + "obsidian": "^1.4.11", "svelte": "^4.2.0", "svelte-preprocess": "^5.0.4", "tslib": "2.4.0", @@ -1946,9 +1946,9 @@ "dev": true }, "node_modules/obsidian": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.4.tgz", - "integrity": "sha512-q2V5GNT/M40uYOENdVw5kovPSoaO6vppiiyBCkIqWgKp4oN654jA/GQ0OaNBA7p5NdfS245QCeRgCFQ42wOZiw==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.11.tgz", + "integrity": "sha512-BCVYTvaXxElJMl6MMbDdY/CGK+aq18SdtDY/7vH8v6BxCBQ6KF4kKxL0vG9UZ0o5qh139KpUoJHNm+6O5dllKA==", "dev": true, "dependencies": { "@types/codemirror": "5.60.8", diff --git a/package.json b/package.json index b06c3dbb..14314013 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "builtin-modules": "3.3.0", "esbuild": "0.17.3", "esbuild-svelte": "^0.8.0", - "obsidian": "latest", + "obsidian": "^1.4.11", "svelte": "^4.2.0", "svelte-preprocess": "^5.0.4", "tslib": "2.4.0", diff --git a/src/FormModal.ts b/src/FormModal.ts index 035c3788..f9f56701 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -140,6 +140,7 @@ export class FormModal extends Modal { selectedVales: this.formResult[definition.name] as string[], availableOptions: options, setting: fieldBase, + app: this.app, } })) return; diff --git a/src/suggesters/MultiSuggest.ts b/src/suggesters/MultiSuggest.ts index ce526967..b051328d 100644 --- a/src/suggesters/MultiSuggest.ts +++ b/src/suggesters/MultiSuggest.ts @@ -1,29 +1,29 @@ -import { TextInputSuggest } from "./suggest"; +import { AbstractInputSuggest, App } from 'obsidian' -export class MultiSuggest extends TextInputSuggest { - content: Set; +export class MultiSuggest extends AbstractInputSuggest { + content: Set; - constructor(input: HTMLInputElement, content: Set, private onSelect: (value: string) => void) { - super(app, input); - this.content = content; - } + constructor(private inputEl: HTMLInputElement, content: Set, private onSelectCb: (value: string) => void, app: App) { + super(app, inputEl); + this.content = content; + } - getSuggestions(inputStr: string): string[] { - const lowerCaseInputStr = inputStr.toLocaleLowerCase(); - return [...this.content].filter((content) => - content.toLocaleLowerCase().contains(lowerCaseInputStr) - ); - } + getSuggestions(inputStr: string): string[] { + const lowerCaseInputStr = inputStr.toLocaleLowerCase(); + return [...this.content].filter((content) => + content.toLocaleLowerCase().contains(lowerCaseInputStr) + ); + } - renderSuggestion(content: string, el: HTMLElement): void { - el.setText(content); - } + renderSuggestion(content: string, el: HTMLElement): void { + el.setText(content); + } - selectSuggestion(content: string): void { - this.onSelect(content); - this.inputEl.value = ""; - // this.inputEl.trigger("blur"); - this.inputEl.blur() - this.close(); - } + selectSuggestion(content: string, evt: MouseEvent | KeyboardEvent): void { + this.onSelectCb(content); + this.inputEl.value = ""; + // this.inputEl.trigger("blur"); + this.inputEl.blur() + this.close(); + } } diff --git a/src/suggesters/suggest.ts b/src/suggesters/suggest.ts index fd23e45d..13902997 100644 --- a/src/suggesters/suggest.ts +++ b/src/suggesters/suggest.ts @@ -10,207 +10,208 @@ import { createPopper } from "@popperjs/core"; import type { Instance as PopperInstance } from "@popperjs/core"; const wrapAround = (value: number, size: number): number => { - return ((value % size) + size) % size; + return ((value % size) + size) % size; }; class Suggest { - private owner: ISuggestOwner; - // @ts-ignore - private values: T[]; - // @ts-ignore - private suggestions: HTMLDivElement[]; - // @ts-ignore - private selectedItem: number; - private containerEl: HTMLElement; - - constructor( - owner: ISuggestOwner, - containerEl: HTMLElement, - scope: Scope - ) { - this.owner = owner; - this.containerEl = containerEl; - - containerEl.on( - "click", - ".suggestion-item", - // @ts-ignore - this.onSuggestionClick.bind(this) - ); - containerEl.on( - "mousemove", - ".suggestion-item", - // @ts-ignore - this.onSuggestionMouseover.bind(this) - ); - - scope.register([], "ArrowUp", (event) => { - if (!event.isComposing) { - this.setSelectedItem(this.selectedItem - 1, true); - return false; - } - }); - - scope.register([], "ArrowDown", (event) => { - if (!event.isComposing) { - this.setSelectedItem(this.selectedItem + 1, true); - return false; - } - }); - - scope.register([], "Enter", (event) => { - if (!event.isComposing) { - this.useSelectedItem(event); - return false; - } - }); - } - - onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { - event.preventDefault(); - - const item = this.suggestions.indexOf(el); - this.setSelectedItem(item, false); - this.useSelectedItem(event); - } - - onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { - const item = this.suggestions.indexOf(el); - this.setSelectedItem(item, false); - } - - setSuggestions(values: T[]) { - this.containerEl.empty(); - const suggestionEls: HTMLDivElement[] = []; - - values.forEach((value) => { - const suggestionEl = this.containerEl.createDiv("suggestion-item"); - this.owner.renderSuggestion(value, suggestionEl); - suggestionEls.push(suggestionEl); - }); - - this.values = values; - this.suggestions = suggestionEls; - this.setSelectedItem(0, false); - } - - useSelectedItem(event: MouseEvent | KeyboardEvent) { - const currentValue = this.values[this.selectedItem]; - if (currentValue) { - this.owner.selectSuggestion(currentValue, event); - } - } - - setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { - const normalizedIndex = wrapAround( - selectedIndex, - this.suggestions.length - ); - const prevSelectedSuggestion = this.suggestions[this.selectedItem]; - const selectedSuggestion = this.suggestions[normalizedIndex]; - - prevSelectedSuggestion?.removeClass("is-selected"); - selectedSuggestion?.addClass("is-selected"); - - this.selectedItem = normalizedIndex; - - if (scrollIntoView) { - selectedSuggestion.scrollIntoView(false); - } - } + private owner: ISuggestOwner; + // @ts-ignore + private values: T[]; + // @ts-ignore + private suggestions: HTMLDivElement[]; + // @ts-ignore + private selectedItem: number; + private containerEl: HTMLElement; + + constructor( + owner: ISuggestOwner, + containerEl: HTMLElement, + scope: Scope + ) { + this.owner = owner; + this.containerEl = containerEl; + + containerEl.on( + "click", + ".suggestion-item", + // @ts-ignore + this.onSuggestionClick.bind(this) + ); + containerEl.on( + "mousemove", + ".suggestion-item", + // @ts-ignore + this.onSuggestionMouseover.bind(this) + ); + + scope.register([], "ArrowUp", (event) => { + if (!event.isComposing) { + this.setSelectedItem(this.selectedItem - 1, true); + return false; + } + }); + + scope.register([], "ArrowDown", (event) => { + if (!event.isComposing) { + this.setSelectedItem(this.selectedItem + 1, true); + return false; + } + }); + + scope.register([], "Enter", (event) => { + if (!event.isComposing) { + this.useSelectedItem(event); + return false; + } + }); + } + + onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void { + event.preventDefault(); + + const item = this.suggestions.indexOf(el); + this.setSelectedItem(item, false); + this.useSelectedItem(event); + } + + onSuggestionMouseover(_event: MouseEvent, el: HTMLDivElement): void { + const item = this.suggestions.indexOf(el); + this.setSelectedItem(item, false); + } + + setSuggestions(values: T[]) { + this.containerEl.empty(); + const suggestionEls: HTMLDivElement[] = []; + + values.forEach((value) => { + const suggestionEl = this.containerEl.createDiv("suggestion-item"); + this.owner.renderSuggestion(value, suggestionEl); + suggestionEls.push(suggestionEl); + }); + + this.values = values; + this.suggestions = suggestionEls; + this.setSelectedItem(0, false); + } + + useSelectedItem(event: MouseEvent | KeyboardEvent) { + const currentValue = this.values[this.selectedItem]; + if (currentValue) { + this.owner.selectSuggestion(currentValue, event); + } + } + + setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { + const normalizedIndex = wrapAround( + selectedIndex, + this.suggestions.length + ); + const prevSelectedSuggestion = this.suggestions[this.selectedItem]; + const selectedSuggestion = this.suggestions[normalizedIndex]; + + prevSelectedSuggestion?.removeClass("is-selected"); + selectedSuggestion?.addClass("is-selected"); + + this.selectedItem = normalizedIndex; + + if (scrollIntoView) { + selectedSuggestion?.scrollIntoView(false); + } + } } export abstract class TextInputSuggest implements ISuggestOwner { - protected app: App; - protected inputEl: HTMLInputElement | HTMLTextAreaElement; - - // @ts-ignore - private popper: PopperInstance; - private scope: Scope; - private suggestEl: HTMLElement; - private suggest: Suggest; - - constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) { - this.app = app; - this.inputEl = inputEl; - this.scope = new Scope(); - - this.suggestEl = createDiv("suggestion-container"); - const suggestion = this.suggestEl.createDiv("suggestion"); - this.suggest = new Suggest(this, suggestion, this.scope); - - this.scope.register([], "Escape", this.close.bind(this)); - - this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); - this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); - this.inputEl.addEventListener("blur", this.close.bind(this)); - this.suggestEl.on( - "mousedown", - ".suggestion-container", - (event: MouseEvent) => { - event.preventDefault(); - } - ); - } - - onInputChanged(): void { - const inputStr = this.inputEl.value; - const suggestions = this.getSuggestions(inputStr); - - if (!suggestions) { - this.close(); - return; - } - - if (suggestions.length > 0) { - this.suggest.setSuggestions(suggestions); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - this.open((this.app).dom.appContainerEl, this.inputEl); - } else { - this.close(); - } - } - - open(container: HTMLElement, inputEl: HTMLElement): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.pushScope(this.scope); - - container.appendChild(this.suggestEl); - this.popper = createPopper(inputEl, this.suggestEl, { - placement: "bottom-start", - modifiers: [ - { - name: "sameWidth", - enabled: true, - fn: ({ state, instance }) => { - // Note: positioning needs to be calculated twice - - // first pass - positioning it according to the width of the popper - // second pass - position it with the width bound to the reference element - // we need to early exit to avoid an infinite loop - const targetWidth = `${state.rects.reference.width}px`; - if (state.styles.popper.width === targetWidth) { - return; - } - state.styles.popper.width = targetWidth; - void instance.update(); - }, - phase: "beforeWrite", - requires: ["computeStyles"], - }, - ], - }); - } - - close(): void { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (this.app).keymap.popScope(this.scope); - - this.suggest.setSuggestions([]); - if (this.popper) this.popper.destroy(); - this.suggestEl.detach(); - } - - abstract getSuggestions(inputStr: string): T[]; - abstract renderSuggestion(item: T, el: HTMLElement): void; - abstract selectSuggestion(item: T): void; + protected app: App; + protected inputEl: HTMLInputElement | HTMLTextAreaElement; + + // @ts-ignore + private popper: PopperInstance; + private scope: Scope; + private suggestEl: HTMLElement; + private suggest: Suggest; + + constructor(app: App, inputEl: HTMLInputElement | HTMLTextAreaElement) { + this.app = app; + this.inputEl = inputEl; + this.scope = new Scope(); + + this.suggestEl = createDiv("suggestion-container"); + const suggestion = this.suggestEl.createDiv("suggestion"); + this.suggest = new Suggest(this, suggestion, this.scope); + + this.scope.register([], "Escape", this.close.bind(this)); + + this.inputEl.addEventListener("input", this.onInputChanged.bind(this)); + this.inputEl.addEventListener("focus", this.onInputChanged.bind(this)); + this.inputEl.addEventListener("blur", this.close.bind(this)); + this.suggestEl.on( + "mousedown", + ".suggestion-container", + (event: MouseEvent) => { + event.preventDefault(); + } + ); + } + + onInputChanged(): void { + const inputStr = this.inputEl.value; + const suggestions = this.getSuggestions(inputStr); + + if (!suggestions) { + this.close(); + return; + } + + if (suggestions.length > 0) { + this.suggest.setSuggestions(suggestions); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.open((this.app).dom.appContainerEl, this.inputEl); + } else { + this.close(); + } + } + + open(container: HTMLElement, inputEl: HTMLElement): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app).keymap.pushScope(this.scope); + + container.appendChild(this.suggestEl); + this.popper = createPopper(inputEl, this.suggestEl, { + placement: "bottom-start", + modifiers: [ + { + name: "sameWidth", + enabled: true, + fn: ({ state, instance }) => { + // Note: positioning needs to be calculated twice - + // first pass - positioning it according to the width of the popper + // second pass - position it with the width bound to the reference element + // we need to early exit to avoid an infinite loop + const targetWidth = `${state.rects.reference.width}px`; + if (state.styles.popper?.width === targetWidth) { + return; + } + //@ts-ignore + state.styles.popper.width = targetWidth; + void instance.update(); + }, + phase: "beforeWrite", + requires: ["computeStyles"], + }, + ], + }); + } + + close(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.app).keymap.popScope(this.scope); + + this.suggest.setSuggestions([]); + if (this.popper) this.popper.destroy(); + this.suggestEl.detach(); + } + + abstract getSuggestions(inputStr: string): T[]; + abstract renderSuggestion(item: T, el: HTMLElement): void; + abstract selectSuggestion(item: T): void; } diff --git a/src/views/ManageFormsView.ts b/src/views/ManageFormsView.ts index b32715fe..bcb0af70 100644 --- a/src/views/ManageFormsView.ts +++ b/src/views/ManageFormsView.ts @@ -1,6 +1,5 @@ import ModalFormPlugin from "../main"; import { ItemView, Setting, WorkspaceLeaf } from "obsidian"; -import { type FormDefinition } from "../core/formDefinition"; export const MANAGE_FORMS_VIEW = "modal-form-manage-forms-view"; @@ -24,7 +23,7 @@ export class ManageFormsView extends ItemView { async onOpen() { // console.log('On open manage forms'); - const container = this.containerEl.children[1]; + const container = this.containerEl.children[1] || this.containerEl.createDiv(); container.empty(); container.createEl("h3", { text: "Manage forms" }); this.renderControls(container.createDiv()); diff --git a/src/views/components/MultiSelect.svelte b/src/views/components/MultiSelect.svelte index 84173e0d..8f390c5d 100644 --- a/src/views/components/MultiSelect.svelte +++ b/src/views/components/MultiSelect.svelte @@ -1,135 +1,141 @@
- -
- {#each selectedVales as value} -
- {value} - -
- {:else} - - {/each} -
+ +
+ {#each selectedVales as value} +
+ {value} + +
+ {:else} + + {/each} +