generated from tpl/obsidian-sample-plugin
			Remove old suggesters
This commit is contained in:
		
							parent
							
								
									ad0af3d369
								
							
						
					
					
						commit
						76de66ca80
					
				| 
						 | 
				
			
			@ -0,0 +1,43 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { App, TFile } from "obsidian";
 | 
			
		||||
	import TextInputSuggest, { type Item } from "./TextInputSuggest.svelte";
 | 
			
		||||
 | 
			
		||||
	type Props = {
 | 
			
		||||
		app: App;
 | 
			
		||||
		id: string;
 | 
			
		||||
		asString?: boolean;
 | 
			
		||||
		value?: TFile | string;
 | 
			
		||||
		onSelected?: (fileOrPath: TFile | string) => void;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	let {
 | 
			
		||||
		app,
 | 
			
		||||
		id,
 | 
			
		||||
		asString,
 | 
			
		||||
		value = $bindable(),
 | 
			
		||||
		onSelected,
 | 
			
		||||
	}: Props = $props();
 | 
			
		||||
 | 
			
		||||
	let items: Item<TFile | string>[] = $state([]);
 | 
			
		||||
 | 
			
		||||
	function handleChange(query: string) {
 | 
			
		||||
		items = app.vault
 | 
			
		||||
			.getMarkdownFiles()
 | 
			
		||||
			.filter((f) =>
 | 
			
		||||
				f.basename.toLowerCase().includes(query.toLowerCase()),
 | 
			
		||||
			)
 | 
			
		||||
			.map((f) => ({
 | 
			
		||||
				text: f.basename,
 | 
			
		||||
				value: asString ? f.basename : f,
 | 
			
		||||
			}));
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<TextInputSuggest
 | 
			
		||||
	{app}
 | 
			
		||||
	{id}
 | 
			
		||||
	{items}
 | 
			
		||||
	bind:value
 | 
			
		||||
	onChange={handleChange}
 | 
			
		||||
	{onSelected}
 | 
			
		||||
/>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import type { ReadingLogEntry } from "@src/types";
 | 
			
		||||
	import BookSuggest from "@ui/components/suggesters/BookSuggest.svelte";
 | 
			
		||||
	import type { App } from "obsidian";
 | 
			
		||||
	import { BookSuggest } from "@ui/suggesters";
 | 
			
		||||
	interface Props {
 | 
			
		||||
		app: App;
 | 
			
		||||
		entry?: ReadingLogEntry;
 | 
			
		||||
| 
						 | 
				
			
			@ -54,10 +54,6 @@
 | 
			
		|||
			createdAt: new Date(createdAt),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function bookSuggest(el: HTMLInputElement) {
 | 
			
		||||
		new BookSuggest(app, el);
 | 
			
		||||
	}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="obt-reading-log-entry-editor">
 | 
			
		||||
| 
						 | 
				
			
			@ -65,14 +61,7 @@
 | 
			
		|||
	<form {onsubmit}>
 | 
			
		||||
		<div class="fields">
 | 
			
		||||
			<label for="book">Book</label>
 | 
			
		||||
			<input
 | 
			
		||||
				type="text"
 | 
			
		||||
				name="book"
 | 
			
		||||
				id="book"
 | 
			
		||||
				bind:value={book}
 | 
			
		||||
				disabled={editMode}
 | 
			
		||||
				use:bookSuggest
 | 
			
		||||
			/>
 | 
			
		||||
			<BookSuggest id="book" {app} bind:value={book} />
 | 
			
		||||
			<label for="pagesRead">Pages Read</label>
 | 
			
		||||
			<input
 | 
			
		||||
				type="number"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
import { TAbstractFile, TFile } from "obsidian";
 | 
			
		||||
import { TextInputSuggest } from "./core";
 | 
			
		||||
 | 
			
		||||
export class BookSuggest extends TextInputSuggest<TFile> {
 | 
			
		||||
	getSuggestions(inputStr: string): TFile[] {
 | 
			
		||||
		const abstractFiles = this.app.vault.getAllLoadedFiles();
 | 
			
		||||
		const files: TFile[] = [];
 | 
			
		||||
		const lowerCaseInputStr = inputStr.toLowerCase();
 | 
			
		||||
 | 
			
		||||
		abstractFiles.forEach((file: TAbstractFile) => {
 | 
			
		||||
			if (
 | 
			
		||||
				file instanceof TFile &&
 | 
			
		||||
				file.extension === "md" &&
 | 
			
		||||
				file.basename.toLowerCase().contains(lowerCaseInputStr)
 | 
			
		||||
			) {
 | 
			
		||||
				files.push(file);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return files;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderSuggestion(file: TFile, el: HTMLElement): void {
 | 
			
		||||
		el.setText(file.basename);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectSuggestion(file: TFile): void {
 | 
			
		||||
		this.inputEl.value = file.basename;
 | 
			
		||||
		this.inputEl.trigger("input");
 | 
			
		||||
		this.close();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,39 +0,0 @@
 | 
			
		|||
import { App } from "obsidian";
 | 
			
		||||
import { TextInputSuggest } from "./core";
 | 
			
		||||
 | 
			
		||||
export class FieldSuggest extends TextInputSuggest<string> {
 | 
			
		||||
	constructor(
 | 
			
		||||
		app: App,
 | 
			
		||||
		inputEl: HTMLInputElement,
 | 
			
		||||
		private readonly accepts?: string[]
 | 
			
		||||
	) {
 | 
			
		||||
		super(app, inputEl);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async getSuggestions(inputStr: string): Promise<string[]> {
 | 
			
		||||
		const typesContent = await this.app.vault.adapter.read(
 | 
			
		||||
			this.app.vault.configDir + "/types.json"
 | 
			
		||||
		);
 | 
			
		||||
		const types = JSON.parse(typesContent).types;
 | 
			
		||||
 | 
			
		||||
		return Object.entries(types)
 | 
			
		||||
			.filter(([field, type]) => {
 | 
			
		||||
				if (this.accepts && !this.accepts.includes(type as string)) {
 | 
			
		||||
					return false;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return field.toLowerCase().includes(inputStr.toLowerCase());
 | 
			
		||||
			})
 | 
			
		||||
			.map(([field, _]) => field);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderSuggestion(field: string, el: HTMLElement): void {
 | 
			
		||||
		el.setText(field);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectSuggestion(field: string): void {
 | 
			
		||||
		this.inputEl.value = field;
 | 
			
		||||
		this.inputEl.trigger("input");
 | 
			
		||||
		this.close();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,34 +0,0 @@
 | 
			
		|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
 | 
			
		||||
 | 
			
		||||
import { TAbstractFile, TFile } from "obsidian";
 | 
			
		||||
import { TextInputSuggest } from "./core";
 | 
			
		||||
 | 
			
		||||
export class FileSuggest extends TextInputSuggest<TFile> {
 | 
			
		||||
	getSuggestions(inputStr: string): TFile[] {
 | 
			
		||||
		const abstractFiles = this.app.vault.getAllLoadedFiles();
 | 
			
		||||
		const files: TFile[] = [];
 | 
			
		||||
		const lowerCaseInputStr = inputStr.toLowerCase();
 | 
			
		||||
 | 
			
		||||
		abstractFiles.forEach((file: TAbstractFile) => {
 | 
			
		||||
			if (
 | 
			
		||||
				file instanceof TFile &&
 | 
			
		||||
				file.extension === "md" &&
 | 
			
		||||
				file.path.toLowerCase().contains(lowerCaseInputStr)
 | 
			
		||||
			) {
 | 
			
		||||
				files.push(file);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return files;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderSuggestion(file: TFile, el: HTMLElement): void {
 | 
			
		||||
		el.setText(file.path);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectSuggestion(file: TFile): void {
 | 
			
		||||
		this.inputEl.value = file.path;
 | 
			
		||||
		this.inputEl.trigger("input");
 | 
			
		||||
		this.close();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
 | 
			
		||||
 | 
			
		||||
import { TAbstractFile, TFolder } from "obsidian";
 | 
			
		||||
import { TextInputSuggest } from "./core";
 | 
			
		||||
 | 
			
		||||
export class FolderSuggest extends TextInputSuggest<TFolder> {
 | 
			
		||||
	getSuggestions(inputStr: string): TFolder[] {
 | 
			
		||||
		const abstractFiles = this.app.vault.getAllLoadedFiles();
 | 
			
		||||
		const folders: TFolder[] = [];
 | 
			
		||||
		const lowerCaseInputStr = inputStr.toLowerCase();
 | 
			
		||||
 | 
			
		||||
		abstractFiles.forEach((folder: TAbstractFile) => {
 | 
			
		||||
			if (
 | 
			
		||||
				folder instanceof TFolder &&
 | 
			
		||||
				folder.path.toLowerCase().contains(lowerCaseInputStr)
 | 
			
		||||
			) {
 | 
			
		||||
				folders.push(folder);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		return folders;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	renderSuggestion(file: TFolder, el: HTMLElement): void {
 | 
			
		||||
		el.setText(file.path);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	selectSuggestion(file: TFolder): void {
 | 
			
		||||
		this.inputEl.value = file.path;
 | 
			
		||||
		this.inputEl.trigger("input");
 | 
			
		||||
		this.close();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,205 +0,0 @@
 | 
			
		|||
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
 | 
			
		||||
 | 
			
		||||
import { App, type ISuggestOwner, Scope } from "obsidian";
 | 
			
		||||
import { createPopper, type Instance as PopperInstance } from "@popperjs/core";
 | 
			
		||||
 | 
			
		||||
const wrapAround = (value: number, size: number): number => {
 | 
			
		||||
	return ((value % size) + size) % size;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Suggest<T> {
 | 
			
		||||
	private owner: ISuggestOwner<T>;
 | 
			
		||||
	private values: T[];
 | 
			
		||||
	private suggestions: HTMLDivElement[];
 | 
			
		||||
	private selectedItem: number;
 | 
			
		||||
	private containerEl: HTMLElement;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		owner: ISuggestOwner<T>,
 | 
			
		||||
		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<T> implements ISuggestOwner<T> {
 | 
			
		||||
	private popper: PopperInstance;
 | 
			
		||||
	private scope: Scope;
 | 
			
		||||
	private suggestEl: HTMLElement;
 | 
			
		||||
	private suggest: Suggest<T>;
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		protected app: App,
 | 
			
		||||
		protected inputEl: HTMLInputElement | HTMLTextAreaElement
 | 
			
		||||
	) {
 | 
			
		||||
		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();
 | 
			
		||||
			}
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async onInputChanged(): Promise<void> {
 | 
			
		||||
		const inputStr = this.inputEl.value;
 | 
			
		||||
		let suggestions = this.getSuggestions(inputStr);
 | 
			
		||||
		if (suggestions instanceof Promise) {
 | 
			
		||||
			suggestions = await suggestions;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!suggestions) {
 | 
			
		||||
			this.close();
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (suggestions.length > 0) {
 | 
			
		||||
			this.suggest.setSuggestions(suggestions);
 | 
			
		||||
			// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
			this.open((<any>this.app).dom.appContainerEl, this.inputEl);
 | 
			
		||||
		} else {
 | 
			
		||||
			this.close();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	open(container: HTMLElement, inputEl: HTMLElement): void {
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
		(<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 {
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
		(<any>this.app).keymap.popScope(this.scope);
 | 
			
		||||
 | 
			
		||||
		this.suggest.setSuggestions([]);
 | 
			
		||||
		if (this.popper) this.popper.destroy();
 | 
			
		||||
		this.suggestEl.detach();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	abstract getSuggestions(inputStr: string): T[] | Promise<T[]>;
 | 
			
		||||
	abstract renderSuggestion(item: T, el: HTMLElement): void;
 | 
			
		||||
	abstract selectSuggestion(item: T): void;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export { BookSuggest } from "./BookSuggest";
 | 
			
		||||
export { FieldSuggest } from "./FieldSuggest";
 | 
			
		||||
export { FileSuggest } from "./FileSuggest";
 | 
			
		||||
export { FolderSuggest } from "./FolderSuggest";
 | 
			
		||||
		Loading…
	
		Reference in New Issue