diff --git a/manifest.json b/manifest.json index 98ec3b5..f1096ca 100644 --- a/manifest.json +++ b/manifest.json @@ -5,5 +5,5 @@ "author": "lazyloong", "minAppVersion": "1.0.0", - "version": "2.27.8" + "version": "2.27.9" } diff --git a/package.json b/package.json index c0843d0..cec7220 100644 --- a/package.json +++ b/package.json @@ -15,10 +15,10 @@ "builtin-modules": "^3.3.0", "obsidian": "github:obsidianmd/obsidian-api", "obsidian-plugin-cli": "^0.9.0", - "templater": "github:SilentVoid13/Templater", "typescript": "^5.4.3" }, "dependencies": { + "@popperjs/core": "^2.11.8", "lodash": "^4.17.21" } } diff --git a/src/modal/fileModal.ts b/src/modal/fileModal.ts index 5c3d6ba..4a39730 100644 --- a/src/modal/fileModal.ts +++ b/src/modal/fileModal.ts @@ -119,6 +119,7 @@ export default class FileModal extends FuzzyModal { }); } this.setInstructions(prompt); + this.addTagInput(); } addTagInput(): void { let inputContainerEl = this.modalEl.querySelector( @@ -130,16 +131,28 @@ export default class FileModal extends FuzzyModal { else this.tags = value.split(",").map((t) => t.trim()); this.onInput(); }); - if (this.plugin.settings.file.searchWithTag) this.tagInput.show(); } onOpen(): void { super.onOpen(); - if (!this.tagInput) this.addTagInput(); + + this.tags = []; + this.tagInput.setValue(""); + let inputContainerEl = this.modalEl.querySelector( + ".prompt-input-container" + ) as HTMLInputElement; + let clearButton = inputContainerEl.querySelector( + ".search-input-clear-button" + ) as HTMLDivElement; + if (this.plugin.settings.file.searchWithTag) { + this.tagInput.show(); + clearButton.style.marginRight = "25%"; + } else { + this.tagInput.hide(); + clearButton.style.marginRight = "0"; + } } onClose(): void { super.onClose(); - this.tags = []; - this.tagInput.setValue(""); } getEmptyInputSuggestions(): MatchData[] { if (this.tags.length == 0) { @@ -165,7 +178,7 @@ export default class FileModal extends FuzzyModal { return ( tagArray && tagArray.length != 0 && - tagArray.some((tag) => this.tags.some((t) => tag.startsWith(t))) + this.tags.every((t) => tagArray.some((tt) => tt.startsWith(t))) ); }) .map((p) => ({ @@ -276,7 +289,11 @@ export default class FileModal extends FuzzyModal { result = result.filter((matchData) => { if (!matchData.item.file) return; let tagArray = getFileTagArray(matchData.item.file); - return tagArray?.some((tag) => this.tags.some((t) => tag.startsWith(t))); + return ( + tagArray && + tagArray.length != 0 && + this.tags.every((t) => tagArray.some((tt) => tt.startsWith(t))) + ); }); } return result; @@ -614,7 +631,8 @@ class TagInput extends TextComponent { super(inputEl); this.hide(); this.setPlaceholder("标签"); - this.inputEl.classList.add("prompt-input"); + this.inputEl.addClasses(["prompt-input", "fz-tag-input"]); + this.inputEl.style.width = "30%"; this.inputEl.style.borderLeft = "2px solid var(--background-primary)"; let tagSuggest = new PinyinSuggest(this.inputEl, plugin); diff --git a/src/modal/modal.ts b/src/modal/modal.ts index d7061ed..08e18ae 100644 --- a/src/modal/modal.ts +++ b/src/modal/modal.ts @@ -15,6 +15,7 @@ export default abstract class FuzzyModal extends SuggestModal { if (this.plugin.settings.global.closeWithBackspace && this.inputEl.value === "") { diff --git a/src/utils/pinyinSuggest.ts b/src/utils/pinyinSuggest.ts index 0a84b26..bb2f5be 100644 --- a/src/utils/pinyinSuggest.ts +++ b/src/utils/pinyinSuggest.ts @@ -1,5 +1,5 @@ import ThePlugin from "@/main"; -import { TextInputSuggest } from "templater/src/settings/suggesters/suggest"; +import { TextInputSuggest } from "./suggest"; import { SuggestionRenderer } from "./suggestionRenderer"; import { MatchData, Item } from "./type"; @@ -7,7 +7,7 @@ export class PinyinSuggest extends TextInputSuggest> { getItemFunction: (query: string) => MatchData[]; plugin: ThePlugin; constructor(inputEl: HTMLInputElement | HTMLTextAreaElement, plugin: ThePlugin) { - super(inputEl); + super(inputEl, plugin.app); this.plugin = plugin; } getSuggestions(inputStr: string): MatchData[] { diff --git a/src/utils/suggest.ts b/src/utils/suggest.ts new file mode 100644 index 0000000..89a8ce6 --- /dev/null +++ b/src/utils/suggest.ts @@ -0,0 +1,183 @@ +// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes + +import { App, ISuggestOwner, Scope } from "obsidian"; +import { createPopper, Instance as PopperInstance } from "@popperjs/core"; + +const wrapAround = (value: number, size: number): number => { + return ((value % size) + size) % size; +}; + +class Suggest { + private owner: ISuggestOwner; + private values: T[]; + private suggestions: HTMLDivElement[]; + private selectedItem: number; + private containerEl: HTMLElement; + + constructor(owner: ISuggestOwner, containerEl: HTMLElement, scope: Scope) { + this.owner = owner; + this.containerEl = containerEl; + + containerEl.on("click", ".suggestion-item", this.onSuggestionClick.bind(this)); + containerEl.on("mousemove", ".suggestion-item", 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 inputEl: HTMLInputElement | HTMLTextAreaElement; + + private popper: PopperInstance; + private scope: Scope; + private suggestEl: HTMLElement; + private suggest: Suggest; + private app: App; + + constructor(inputEl: HTMLInputElement | HTMLTextAreaElement, app: App) { + this.inputEl = inputEl; + this.app = app; + 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); + this.open(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; + instance.update(); + }, + phase: "beforeWrite", + requires: ["computeStyles"], + }, + ], + }); + } + + close(): void { + 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; +}