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">
|
<script lang="ts">
|
||||||
import type { ReadingLogEntry } from "@src/types";
|
import type { ReadingLogEntry } from "@src/types";
|
||||||
|
import BookSuggest from "@ui/components/suggesters/BookSuggest.svelte";
|
||||||
import type { App } from "obsidian";
|
import type { App } from "obsidian";
|
||||||
import { BookSuggest } from "@ui/suggesters";
|
|
||||||
interface Props {
|
interface Props {
|
||||||
app: App;
|
app: App;
|
||||||
entry?: ReadingLogEntry;
|
entry?: ReadingLogEntry;
|
||||||
|
@ -54,10 +54,6 @@
|
||||||
createdAt: new Date(createdAt),
|
createdAt: new Date(createdAt),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function bookSuggest(el: HTMLInputElement) {
|
|
||||||
new BookSuggest(app, el);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="obt-reading-log-entry-editor">
|
<div class="obt-reading-log-entry-editor">
|
||||||
|
@ -65,14 +61,7 @@
|
||||||
<form {onsubmit}>
|
<form {onsubmit}>
|
||||||
<div class="fields">
|
<div class="fields">
|
||||||
<label for="book">Book</label>
|
<label for="book">Book</label>
|
||||||
<input
|
<BookSuggest id="book" {app} bind:value={book} />
|
||||||
type="text"
|
|
||||||
name="book"
|
|
||||||
id="book"
|
|
||||||
bind:value={book}
|
|
||||||
disabled={editMode}
|
|
||||||
use:bookSuggest
|
|
||||||
/>
|
|
||||||
<label for="pagesRead">Pages Read</label>
|
<label for="pagesRead">Pages Read</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
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