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