Setting Suggester 제작

setting에서 template file path를 설정할 떄, 파일들을 제안해주는 기능을 개발했다
This commit is contained in:
sechan100 2024-09-25 11:13:36 +09:00
parent caa872a830
commit 4a34bb443f
6 changed files with 2655 additions and 4 deletions

2353
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -25,6 +25,9 @@
"typescript": "4.7.4"
},
"dependencies": {
"@popperjs/core": "^2.11.8",
"i": "^0.3.7",
"npm": "^10.8.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}

View file

@ -1,19 +1,18 @@
import DailyRoutinePlugin from "main";
import { App, PluginSettingTab, Setting } from "obsidian";
import { FileSuggest } from "./suggesters/FileSuggester";
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DailyRoutinePluginSettings {
routineTemplatePath: string | null;
}
export const DEFAULT_SETTINGS: DailyRoutinePluginSettings = {
routineTemplatePath: null
}
export class DailyRoutineSettingTab extends PluginSettingTab {
plugin: DailyRoutinePlugin;
@ -25,5 +24,20 @@ export class DailyRoutineSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName("Routine Template Path")
.setDesc("The path to the daily routine template file.")
.addText(text => {
new FileSuggest(text.inputEl, "file");
text
.setPlaceholder("daily-routine-template.md")
.setValue(this.plugin.settings.routineTemplatePath??"")
.onChange(async (value) => {
this.plugin.settings.routineTemplatePath = value;
await this.plugin.saveSettings();
})
}
);
}
}

View file

@ -0,0 +1,52 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { TAbstractFile, TFile, TFolder } from "obsidian";
import { TextInputSuggest } from "./suggest";
import { plugin } from "src/shared/utils/plugin-service-locator";
export class FileSuggest extends TextInputSuggest<TAbstractFile> {
#isTypeRight: (file: TAbstractFile) => boolean;
constructor(inputEl: HTMLInputElement, type: "file" | "folder") {
super(inputEl);
this.#isTypeRight =
type === "file"
?
function (file: TAbstractFile): file is TFile {
return file instanceof TFile;
}
:
function (file: TAbstractFile): file is TFolder {
return file instanceof TFolder;
};
}
getSuggestions(inputStr: string): TAbstractFile[] {
const abstractFiles = plugin().app.vault.getAllLoadedFiles();
const files: TAbstractFile[] = [];
const lowerCaseInputStr = inputStr.toLowerCase();
abstractFiles.forEach((file: TAbstractFile) => {
if (
this.#isTypeRight(file) &&
file.path.toLowerCase().contains(lowerCaseInputStr)
) {
files.push(file);
}
});
return files.slice(0, 1000);
}
renderSuggestion(file: TAbstractFile, el: HTMLElement): void {
el.setText(file.path);
}
selectSuggestion(file: TAbstractFile): void {
this.inputEl.value = file.path;
this.inputEl.trigger("input");
this.close();
}
}

View file

@ -0,0 +1,201 @@
// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes
import { ISuggestOwner, Scope } from "obsidian";
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
import { plugin } from "src/shared/utils/plugin-service-locator";
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> {
protected inputEl: HTMLInputElement | HTMLTextAreaElement;
private popper: PopperInstance;
private scope: Scope;
private suggestEl: HTMLElement;
private suggest: Suggest<T>;
constructor(inputEl: HTMLInputElement | HTMLTextAreaElement) {
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);
this.open(plugin().app.workspace.containerEl, this.inputEl);
} else {
this.close();
}
}
open(container: HTMLElement, inputEl: HTMLElement): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
plugin().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 {
plugin().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;
}

28
src/view/activate-view.ts Normal file
View file

@ -0,0 +1,28 @@
import { WorkspaceLeaf } from "obsidian";
import { plugin } from "src/shared/utils/plugin-service-locator";
/**
* viewType에 .
* @param viewTypeName
* @param pos -1: left, 0: center(default), 1: right
*/
export const activateView = async (viewTypeName: string, pos = 0) => {
const app = plugin().app;
let leaf = app.workspace.getLeavesOfType(viewTypeName)[0];
let getLeaf;
if(pos === -1) {
getLeaf = () => app.workspace.getLeftLeaf(false)
} else if(pos === 0) {
getLeaf = () => app.workspace.getLeaf(false)
} else {
getLeaf = () => app.workspace.getRightLeaf(false)
}
if(!leaf) {
leaf = getLeaf() as WorkspaceLeaf;
await leaf.setViewState({ type: viewTypeName });
}
app.workspace.revealLeaf(leaf);
}