From 0469221b90c42edd60ce105bd6c872aa9e5e3187 Mon Sep 17 00:00:00 2001 From: Lost Paul <70213368+LostPaul@users.noreply.github.com> Date: Wed, 15 Mar 2023 21:02:00 +0100 Subject: [PATCH] Added more setting options --- src/commands.ts | 12 ++ src/main.ts | 15 ++- src/settings.ts | 47 +++++++- src/suggesters/FileSuggester.ts | 54 +++++++++ src/suggesters/suggest.ts | 202 ++++++++++++++++++++++++++++++++ styles.css | 4 + 6 files changed, 327 insertions(+), 7 deletions(-) create mode 100644 src/commands.ts create mode 100644 src/suggesters/FileSuggester.ts create mode 100644 src/suggesters/suggest.ts diff --git a/src/commands.ts b/src/commands.ts new file mode 100644 index 0000000..5cde0c9 --- /dev/null +++ b/src/commands.ts @@ -0,0 +1,12 @@ +import { App } from 'obsidian'; +import FolderNotesPlugin from './main'; +export class Commands { + plugin: FolderNotesPlugin; + app: App; + constructor(app: App, plugin: FolderNotesPlugin) { + this.plugin = plugin; + this.app = app; + } + registerCommands() { + } +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 4999698..f9f476f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ export default class FolderNotesPlugin extends Plugin { await this.loadSettings(); this.settingsTab = new SettingsTab(this.app, this); this.addSettingTab(this.settingsTab); + document.body.classList.add('folder-notes-plugin') if (this.settings.hideFolderNote) { document.body.classList.add('hide-folder-note'); } else { @@ -24,13 +25,12 @@ export default class FolderNotesPlugin extends Plugin { if (!path) return; const folder = this.app.vault.getAbstractFileByPath(path); if (folder) return; - this.createFolderNote(path); - + this.createFolderNote(path, true); } })); this.registerEvent(this.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => { - if (!this.settings.autoRename) return; + if (!this.settings.syncFolderName) return; if (file instanceof TFolder) { const folder = this.app.vault.getAbstractFileByPath(file?.path); const oldName = oldPath.substring(oldPath.lastIndexOf('/' || '\\')); @@ -70,6 +70,11 @@ export default class FolderNotesPlugin extends Plugin { async handleFolderClick(event: MouseEvent) { if (!(event.target instanceof HTMLElement)) return; event.stopImmediatePropagation(); + if (!document.body.classList.contains('folder-notes-plugin')) { + event.target.onclick = null; + event.target.click(); + return; + } const folder = event.target.parentElement?.getAttribute('data-path'); const path = folder + '/' + event.target.innerText + '.md'; @@ -104,11 +109,12 @@ export default class FolderNotesPlugin extends Plugin { } } - async createFolderNote(path: string) { + async createFolderNote(path: string, useModal?: boolean) { const leaf = this.app.workspace.getLeaf(false); const file = await this.app.vault.create(path, ''); await leaf.openFile(file); if (!this.settings.autoCreate) return; + if (!useModal) return; const folder = this.app.vault.getAbstractFileByPath(path.substring(0, path.lastIndexOf('/' || '\\'))); if (!(folder instanceof TFolder)) return; const modal = new FolderNameModal(this.app, this, folder); @@ -126,6 +132,7 @@ export default class FolderNotesPlugin extends Plugin { onunload() { console.log('unloading folder notes plugin'); this.observer.disconnect(); + document.body.classList.remove('folder-notes-plugin'); } async loadSettings() { diff --git a/src/settings.ts b/src/settings.ts index 32af903..006bee5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -4,19 +4,19 @@ export interface FolderNotesSettings { syncFolderName: boolean; ctrlKey: boolean; altKey: boolean; - autoRename: boolean; hideFolderNote: boolean; templatePath: string; autoCreate: boolean; + excludeFolders: string[]; } export const DEFAULT_SETTINGS: FolderNotesSettings = { syncFolderName: true, ctrlKey: true, altKey: false, - autoRename: true, hideFolderNote: true, templatePath: '', - autoCreate: true, + autoCreate: false, + excludeFolders: [], }; export class SettingsTab extends PluginSettingTab { plugin: FolderNotesPlugin; @@ -48,5 +48,46 @@ export class SettingsTab extends PluginSettingTab { this.display(); }) ); + new Setting(containerEl) + .setName('Sync folder name') + .setDesc('Automatically rename the folder note when the folder name is changed') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.syncFolderName) + .onChange(async (value) => { + this.plugin.settings.syncFolderName = value; + await this.plugin.saveSettings(); + this.display(); + }) + ); + new Setting(containerEl) + .setName('Auto create folder note') + .setDesc('Automatically create a folder note when a new folder is created') + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.autoCreate) + .onChange(async (value) => { + this.plugin.settings.autoCreate = value; + await this.plugin.saveSettings(); + this.display(); + }) + ); + new Setting(containerEl) + .setName('Key for creating folder note') + .setDesc('The key combination to create a folder note') + .addDropdown((dropdown) => { + dropdown + .addOption('ctrl', 'Ctrl + Click') + .addOption('alt', 'Alt + Click') + .setValue(this.plugin.settings.ctrlKey ? 'ctrl' : 'alt') + .onChange(async (value) => { + this.plugin.settings.ctrlKey = value === 'ctrl'; + this.plugin.settings.altKey = value === 'alt'; + await this.plugin.saveSettings(); + this.display(); + }); + }); + + } } \ No newline at end of file diff --git a/src/suggesters/FileSuggester.ts b/src/suggesters/FileSuggester.ts new file mode 100644 index 0000000..c552642 --- /dev/null +++ b/src/suggesters/FileSuggester.ts @@ -0,0 +1,54 @@ +// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes and https://github.com/SilentVoid13/Templater + +import { TAbstractFile, TFile } from "obsidian"; +import { TextInputSuggest } from "./suggest"; +import FolderNotesPlugin from "../main"; +export enum FileSuggestMode { + TemplateFiles, + ScriptFiles, +} + +export class FileSuggest extends TextInputSuggest { + constructor( + public inputEl: HTMLInputElement, + private plugin: FolderNotesPlugin + ) { + super(inputEl); + } + + + get_error_msg(mode: FileSuggestMode): string { + switch (mode) { + case FileSuggestMode.TemplateFiles: + return `Templates folder doesn't exist`; + case FileSuggestMode.ScriptFiles: + return `User Scripts folder doesn't exist`; + } + } + + getSuggestions(input_str: string): TFile[] { + const files: TFile[] = []; + const lower_input_str = input_str.toLowerCase(); + + this.plugin.app.vault.getFiles().forEach((file: TAbstractFile) => { + if ( + file instanceof TFile && + file.path.toLowerCase().contains(lower_input_str) + ) { + 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(); + } +} \ No newline at end of file diff --git a/src/suggesters/suggest.ts b/src/suggesters/suggest.ts new file mode 100644 index 0000000..5e03bc9 --- /dev/null +++ b/src/suggesters/suggest.ts @@ -0,0 +1,202 @@ +// Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes and https://github.com/SilentVoid13/Templater + + +import { 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; + + 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); + // @ts-ignore + 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 + 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 { + 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; +} \ No newline at end of file diff --git a/styles.css b/styles.css index bb0c880..53265a7 100644 --- a/styles.css +++ b/styles.css @@ -10,4 +10,8 @@ .hide-folder-note .is-folder-note { display: none; +} + +.nav-folder-collapse-indicator:hover { + cursor: pointer; } \ No newline at end of file