diff --git a/src/Commands.ts b/src/Commands.ts index 56f7dd1..a27b541 100644 --- a/src/Commands.ts +++ b/src/Commands.ts @@ -1,11 +1,35 @@ -import type { App, Menu, TAbstractFile, Editor, MarkdownView } from 'obsidian'; -import { TFolder, Notice, TFile, Platform } from 'obsidian'; +import { + TFolder, + Notice, + TFile, + Platform, + type App, + type Menu, + type TAbstractFile, + type Editor, + type MarkdownView, +} from 'obsidian'; import type FolderNotesPlugin from './main'; -import { getFolderNote, createFolderNote, deleteFolderNote, turnIntoFolderNote, openFolderNote, extractFolderName, detachFolderNote } from './functions/folderNoteFunctions'; +import { + getFolderNote, + createFolderNote, + deleteFolderNote, + turnIntoFolderNote, + openFolderNote, + extractFolderName, + detachFolderNote, +} from './functions/folderNoteFunctions'; import { ExcludedFolder } from './ExcludeFolders/ExcludeFolder'; import { getFolderPathFromString, getFileExplorerActiveFolder } from './functions/utils'; -import { deleteExcludedFolder, getDetachedFolder, getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; -import { hideFolderNoteInFileExplorer, showFolderNoteInFileExplorer } from './functions/styleFunctions'; +import { + deleteExcludedFolder, + getDetachedFolder, + getExcludedFolder, +} from './ExcludeFolders/functions/folderFunctions'; +import { + hideFolderNoteInFileExplorer, + showFolderNoteInFileExplorer, +} from './functions/styleFunctions'; @@ -16,12 +40,13 @@ export class Commands { this.plugin = plugin; this.app = app; } - registerCommands() { + registerCommands(): void { this.editorCommands(); this.fileCommands(); this.regularCommands(); } - regularCommands() { + + regularCommands(): void { this.plugin.addCommand({ id: 'turn-into-folder-note', name: 'Use this file as the folder note for its parent folder', @@ -52,7 +77,8 @@ export class Commands { if (this.plugin.app.vault.getAbstractFileByPath(newPath)) { return new Notice('Folder already exists'); } - const automaticallyCreateFolderNote = this.plugin.settings.autoCreate; + const automaticallyCreateFolderNote = + this.plugin.settings.autoCreate; this.plugin.settings.autoCreate = false; this.plugin.saveSettings(); await this.plugin.app.vault.createFolder(newPath); @@ -187,6 +213,7 @@ export class Commands { const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':']; for (const char of blacklist) { if (text.includes(char)) { + // eslint-disable-next-line max-len new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?'); return false; } @@ -207,13 +234,18 @@ export class Commands { createFolderNote(this.plugin, text, false); } else { - folder = this.plugin.app.vault.getAbstractFileByPath(folderPath + '/' + text); + const folderFullPath = folderPath + '/' + text; + folder = this.plugin.app.vault.getAbstractFileByPath(folderFullPath); if (folder instanceof TFolder) { new Notice('Folder note already exists'); return false; } if (this.plugin.settings.storageLocation === 'parentFolder') { - if (this.app.vault.getAbstractFileByPath(folderPath + '/' + text + this.plugin.settings.folderNoteType)) { + if ( + this.app.vault.getAbstractFileByPath( + folderPath + '/' + text + this.plugin.settings.folderNoteType, + ) + ) { new Notice('File already exists'); return false; } @@ -221,7 +253,9 @@ export class Commands { this.plugin.app.vault.createFolder(folderPath + '/' + text); createFolderNote(this.plugin, folderPath + '/' + text, false); } - const fileName = this.plugin.settings.folderNoteName.replace('{{folder_name}}', text); + + const { folderNoteName } = this.plugin.settings; + const fileName = folderNoteName.replace('{{folder_name}}', text); if (fileName !== text) { editor.replaceSelection(`[[${fileName}]]`); } else { @@ -234,213 +268,243 @@ export class Commands { }); } - fileCommands() { - this.plugin.registerEvent(this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => { - let folder: TAbstractFile | TFolder | null = file.parent; - if (file instanceof TFile) { - if (this.plugin.settings.storageLocation === 'insideFolder') { - folder = file.parent; - } else { - const fileName = extractFolderName(this.plugin.settings.folderNoteName, file.basename); - if (fileName) { - if (file.parent?.path === '' || file.parent?.path === '/') { - folder = this.plugin.app.vault.getAbstractFileByPath(fileName); - } else { - folder = this.plugin.app.vault.getAbstractFileByPath(file.parent?.path + '/' + fileName); - } - } - } - - if (folder instanceof TFolder) { - const folderNote = getFolderNote(this.plugin, folder.path); - const excludedFolder = getExcludedFolder(this.plugin, folder.path, true); - if (folderNote?.path === file.path && !excludedFolder?.detached) { return; } - } else if (file.parent instanceof TFolder) { - folder = file.parent; - } - } - - menu.addItem(async (item) => { - if (Platform.isDesktop && !Platform.isTablet && this.plugin.settings.useSubmenus) { - item - .setTitle('Folder Note Commands') - .setIcon('folder-edit'); - } - let subMenu: Menu; - if (!Platform.isDesktopApp || !Platform.isDesktop || Platform.isTablet || !this.plugin.settings.useSubmenus) { - subMenu = menu; - item.setDisabled(true); - } else { - // @ts-ignore - subMenu = item.setSubmenu() as Menu; - } + fileCommands(): void { + this.plugin.registerEvent( + this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => { + let folder: TAbstractFile | TFolder | null = file.parent; if (file instanceof TFile) { - // @ts-ignore - subMenu.addItem((item) => { - item.setTitle('Create folder note') - .setIcon('edit') - .onClick(async () => { - if (!folder) return; - let newPath = folder.path + '/' + file.basename; - if (folder.path === '' || folder.path === '/') { - newPath = file.basename; - } - if (this.plugin.app.vault.getAbstractFileByPath(newPath)) { - return new Notice('Folder already exists'); - } - const automaticallyCreateFolderNote = this.plugin.settings.autoCreate; - this.plugin.settings.autoCreate = false; - this.plugin.saveSettings(); - await this.plugin.app.vault.createFolder(newPath); - const newFolder = this.plugin.app.vault.getAbstractFileByPath(newPath); - if (!(newFolder instanceof TFolder)) return; - await createFolderNote(this.plugin, newFolder.path, true, '.' + file.extension, false, file); - this.plugin.settings.autoCreate = automaticallyCreateFolderNote; - this.plugin.saveSettings(); - }); - }); - if (getFolderPathFromString(file.path) === '') return; - if (!(folder instanceof TFolder)) return; - if (folder.path === '' || folder.path === '/') return; - subMenu.addItem((item) => { - item.setTitle(`Turn into folder note for ${folder?.name}`) - .setIcon('edit') - .onClick(() => { - if (!folder || !(folder instanceof TFolder)) return; - const folderNote = getFolderNote(this.plugin, folder.path); - turnIntoFolderNote(this.plugin, file, folder, folderNote); - }); - }); - } - if (!(file instanceof TFolder)) return; - const excludedFolder = getExcludedFolder(this.plugin, file.path, false); - const detachedExcludedFolder = getDetachedFolder(this.plugin, file.path); - if (excludedFolder && !excludedFolder.hideInSettings) { - // I'm not sure if I'm ever going to add this because of the possibility that a folder got more than one excluded - // subMenu.addItem((item) => { - // item.setTitle('Manage excluded folder') - // .setIcon('settings-2') - // .onClick(() => { - // if (excludedFolder instanceof ExcludedFolder) { - // new ExcludedFolderSettings(this.plugin.app, this.plugin, excludedFolder).open(); - // } else if (excludedFolder instanceof ExcludePattern) { - // new PatternSettings(this.plugin.app, this.plugin, excludedFolder).open(); - // } - // }) - // }) - subMenu.addItem((item) => { - item.setTitle('Remove folder from excluded folders') - .setIcon('trash') - .onClick(() => { - this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter( - (folder) => (folder.path !== file.path) || folder.detached); - this.plugin.saveSettings(true); - new Notice('Successfully removed folder from excluded folders'); - }); - }); - return; - } - if (detachedExcludedFolder) { - subMenu.addItem((item) => { - item.setTitle('Remove folder from detached folders') - .setIcon('trash') - .onClick(() => { - deleteExcludedFolder(this.plugin, detachedExcludedFolder); - }); - }); - } - if (detachedExcludedFolder) { return; } - subMenu.addItem((item) => { - item.setTitle('Exclude folder from folder notes') - .setIcon('x-circle') - .onClick(() => { - const excludedFolder = new ExcludedFolder(file.path, this.plugin.settings.excludeFolders.length, undefined, this.plugin); - this.plugin.settings.excludeFolders.push(excludedFolder); - this.plugin.saveSettings(true); - new Notice('Successfully excluded folder from folder notes'); - }); - }); - if (!(file instanceof TFolder)) return; - const folderNote = getFolderNote(this.plugin, file.path); - if (folderNote instanceof TFile && !detachedExcludedFolder) { - subMenu.addItem((item) => { - item.setTitle('Delete folder note') - .setIcon('trash') - .onClick(() => { - deleteFolderNote(this.plugin, folderNote, true); - }); - }); - - subMenu.addItem((item) => { - item.setTitle('Open folder note') - .setIcon('chevron-right-square') - .onClick(() => { - openFolderNote(this.plugin, folderNote); - }); - }); - - subMenu.addItem((item) => { - item.setTitle('Detach folder note') - .setIcon('unlink') - .onClick(() => { - detachFolderNote(this.plugin, folderNote); - }); - }); - - subMenu.addItem((item) => { - item.setTitle('Copy Obsidian URL') - .setIcon('link') - .onClick(() => { - // @ts-ignore - this.app.copyObsidianUrl(folderNote); - }); - }); - - if (this.plugin.settings.hideFolderNote) { - if (excludedFolder?.showFolderNote) { - subMenu.addItem((item) => { - item.setTitle('Hide folder note in explorer') - .setIcon('eye-off') - .onClick(() => { - hideFolderNoteInFileExplorer(file.path, this.plugin); - }); - }); - } else { - subMenu.addItem((item) => { - item.setTitle('Show folder note in explorer') - .setIcon('eye') - .onClick(() => { - showFolderNoteInFileExplorer(file.path, this.plugin); - }); - }); + if (this.plugin.settings.storageLocation === 'insideFolder') { + folder = file.parent; + } else { + const { folderNoteName } = this.plugin.settings; + const fileName = extractFolderName(folderNoteName, file.basename); + if (fileName) { + if (file.parent?.path === '' || file.parent?.path === '/') { + folder = this.plugin.app.vault.getAbstractFileByPath(fileName); + } else { + folder = this.plugin.app.vault.getAbstractFileByPath( + file.parent?.path + '/' + fileName, + ); + } } } - } else { - subMenu.addItem((item) => { - item.setTitle('Create markdown folder note') - .setIcon('edit') - .onClick(() => { - createFolderNote(this.plugin, file.path, true, '.md'); - }); - }); + if (folder instanceof TFolder) { + const folderNote = getFolderNote(this.plugin, folder.path); + const excludedFolder = getExcludedFolder(this.plugin, folder.path, true); + if (folderNote?.path === file.path && !excludedFolder?.detached) { return; } + } else if (file.parent instanceof TFolder) { + folder = file.parent; + } + } - this.plugin.settings.supportedFileTypes.forEach((fileType) => { - if (fileType === 'md') return; - subMenu.addItem((item) => { - item.setTitle(`Create ${fileType} folder note`) + // eslint-disable-next-line complexity + menu.addItem(async (menuItem) => { + if ( + Platform.isDesktop && + !Platform.isTablet && + this.plugin.settings.useSubmenus + ) { + menuItem + .setTitle('Folder Note Commands') + .setIcon('folder-edit'); + } + let subMenu: Menu; + if ( + !Platform.isDesktopApp || + !Platform.isDesktop || + Platform.isTablet || + !this.plugin.settings.useSubmenus + ) { + subMenu = menu; + menuItem.setDisabled(true); + } else { + subMenu = menuItem.setSubmenu() as Menu; + } + if (file instanceof TFile) { + subMenu.addItem((subItem) => { + subItem.setTitle('Create folder note') .setIcon('edit') - .onClick(() => { - createFolderNote(this.plugin, file.path, true, '.' + fileType); + .onClick(async () => { + if (!folder) return; + let newPath = folder.path + '/' + file.basename; + if (folder.path === '' || folder.path === '/') { + newPath = file.basename; + } + if (this.plugin.app.vault.getAbstractFileByPath(newPath)) { + return new Notice('Folder already exists'); + } + const automaticallyCreateFolderNote = + this.plugin.settings.autoCreate; + this.plugin.settings.autoCreate = false; + this.plugin.saveSettings(); + await this.plugin.app.vault.createFolder(newPath); + const newFolder = this.plugin.app.vault + .getAbstractFileByPath(newPath); + if (!(newFolder instanceof TFolder)) return; + await createFolderNote( + this.plugin, + newFolder.path, + true, + '.' + file.extension, + false, + file, + ); + this.plugin.settings.autoCreate = automaticallyCreateFolderNote; + this.plugin.saveSettings(); }); }); + if (getFolderPathFromString(file.path) === '') return; + if (!(folder instanceof TFolder)) return; + if (folder.path === '' || folder.path === '/') return; + subMenu.addItem((item) => { + item.setTitle(`Turn into folder note for ${folder?.name}`) + .setIcon('edit') + .onClick(() => { + if (!folder || !(folder instanceof TFolder)) return; + const folderNote = getFolderNote(this.plugin, folder.path); + turnIntoFolderNote(this.plugin, file, folder, folderNote); + }); + }); + } + if (!(file instanceof TFolder)) return; + const excludedFolder = getExcludedFolder(this.plugin, file.path, false); + const detachedExcludedFolder = getDetachedFolder(this.plugin, file.path); + if (excludedFolder && !excludedFolder.hideInSettings) { + // I'm not sure if I'm ever going to add this because of the possibility that a folder got more than one excluded + // subMenu.addItem((item) => { + // item.setTitle('Manage excluded folder') + // .setIcon('settings-2') + // .onClick(() => { + // if (excludedFolder instanceof ExcludedFolder) { + // new ExcludedFolderSettings(this.plugin.app, this.plugin, excludedFolder).open(); + // } else if (excludedFolder instanceof ExcludePattern) { + // new PatternSettings(this.plugin.app, this.plugin, excludedFolder).open(); + // } + // }) + // }) + subMenu.addItem((item) => { + item.setTitle('Remove folder from excluded folders') + .setIcon('trash') + .onClick(() => { + this.plugin.settings.excludeFolders = + this.plugin.settings.excludeFolders.filter( + (excluded) => + (excluded.path !== file.path) || excluded.detached, + ); + this.plugin.saveSettings(true); + new Notice('Successfully removed folder from excluded folders'); + }); + }); + return; + } + if (detachedExcludedFolder) { + subMenu.addItem((item) => { + item.setTitle('Remove folder from detached folders') + .setIcon('trash') + .onClick(() => { + deleteExcludedFolder(this.plugin, detachedExcludedFolder); + }); + }); + } + if (detachedExcludedFolder) { return; } + subMenu.addItem((item) => { + item.setTitle('Exclude folder from folder notes') + .setIcon('x-circle') + .onClick(() => { + const newExcludedFolder = new ExcludedFolder( + file.path, + this.plugin.settings.excludeFolders.length, + undefined, + this.plugin, + ); + this.plugin.settings.excludeFolders.push(newExcludedFolder); + this.plugin.saveSettings(true); + new Notice('Successfully excluded folder from folder notes'); + }); }); - } - }); - })); + if (!(file instanceof TFolder)) return; + const folderNote = getFolderNote(this.plugin, file.path); + if (folderNote instanceof TFile && !detachedExcludedFolder) { + subMenu.addItem((item) => { + item.setTitle('Delete folder note') + .setIcon('trash') + .onClick(() => { + deleteFolderNote(this.plugin, folderNote, true); + }); + }); + + subMenu.addItem((item) => { + item.setTitle('Open folder note') + .setIcon('chevron-right-square') + .onClick(() => { + openFolderNote(this.plugin, folderNote); + }); + }); + + subMenu.addItem((item) => { + item.setTitle('Detach folder note') + .setIcon('unlink') + .onClick(() => { + detachFolderNote(this.plugin, folderNote); + }); + }); + + subMenu.addItem((item) => { + item.setTitle('Copy Obsidian URL') + .setIcon('link') + .onClick(() => { + this.app.copyObsidianUrl(folderNote); + }); + }); + + if (this.plugin.settings.hideFolderNote) { + if (excludedFolder?.showFolderNote) { + subMenu.addItem((item) => { + item.setTitle('Hide folder note in explorer') + .setIcon('eye-off') + .onClick(() => { + hideFolderNoteInFileExplorer(file.path, this.plugin); + }); + }); + } else { + subMenu.addItem((item) => { + item.setTitle('Show folder note in explorer') + .setIcon('eye') + .onClick(() => { + showFolderNoteInFileExplorer(file.path, this.plugin); + }); + }); + } + } + + } else { + subMenu.addItem((item) => { + item.setTitle('Create markdown folder note') + .setIcon('edit') + .onClick(() => { + createFolderNote(this.plugin, file.path, true, '.md'); + }); + }); + + this.plugin.settings.supportedFileTypes.forEach((fileType) => { + if (fileType === 'md') return; + subMenu.addItem((item) => { + item.setTitle(`Create ${fileType} folder note`) + .setIcon('edit') + .onClick(() => { + // eslint-disable-next-line max-len + createFolderNote(this.plugin, file.path, true, '.' + fileType); + }); + }); + }); + } + }); + })); } - editorCommands() { + editorCommands(): void { + // eslint-disable-next-line max-len this.plugin.registerEvent(this.plugin.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => { const text = editor.getSelection().trim(); if (!text || text.trim() === '') return; @@ -453,6 +517,7 @@ export class Commands { const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':']; for (const char of blacklist) { if (text.includes(char)) { + // eslint-disable-next-line max-len new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?'); return; } @@ -461,9 +526,11 @@ export class Commands { new Notice('File name cannot end with a dot'); return; } + let folder: TAbstractFile | null; const folderPath = getFolderPathFromString(file.path); - const fileName = this.plugin.settings.folderNoteName.replace('{{folder_name}}', text); + const { folderNoteName } = this.plugin.settings; + const fileName = folderNoteName.replace('{{folder_name}}', text); if (folderPath === '') { folder = this.plugin.app.vault.getAbstractFileByPath(text); if (folder instanceof TFolder) { @@ -473,12 +540,21 @@ export class Commands { createFolderNote(this.plugin, text, false); } else { - folder = this.plugin.app.vault.getAbstractFileByPath(folderPath + '/' + text); + folder = this.plugin.app.vault.getAbstractFileByPath( + folderPath + '/' + text, + ); if (folder instanceof TFolder) { return new Notice('Folder note already exists'); } if (this.plugin.settings.storageLocation === 'parentFolder') { - if (this.app.vault.getAbstractFileByPath(folderPath + '/' + fileName + this.plugin.settings.folderNoteType)) { + if ( + this.app.vault.getAbstractFileByPath( + folderPath + + '/' + + fileName + + this.plugin.settings.folderNoteType, + ) + ) { return new Notice('File already exists'); } } diff --git a/src/functions/ListComponent.ts b/src/functions/ListComponent.ts index 0c8e21e..f6486a8 100644 --- a/src/functions/ListComponent.ts +++ b/src/functions/ListComponent.ts @@ -18,19 +18,19 @@ export class ListComponent { this.defaultValues = defaultValues; } - on(event: string, listener: (data?: any) => void) { + on(event: string, listener: (data?: unknown) => void): void { this.emitter.on(event, listener); } - off(event: string, listener: (data?: any) => void) { + off(event: string, listener: (data?: unknown) => void): void { this.emitter.off(event, listener); } - private emit(event: string, data?: any) { + private emit(event: string, data?: unknown): void { this.emitter.emit(event, data); } - setValues(values: string[]) { + setValues(values: string[]): void { this.removeElements(); this.values = values; if (values.length !== 0) { @@ -41,11 +41,11 @@ export class ListComponent { this.emit('update', this.values); } - removeElements() { + removeElements(): void { this.listEl.empty(); } - addElement(value: string) { + addElement(value: string): void { this.listEl.createSpan('setting-hotkey', (span) => { if (value.toLocaleLowerCase() === 'md') { span.innerText = 'markdown'; @@ -53,35 +53,39 @@ export class ListComponent { span.innerText = value; } span.setAttribute('extension', value); + // eslint-disable-next-line max-len const removeSpan = span.createEl('span', { cls: 'ofn-list-item-remove setting-hotkey-icon' }); + // eslint-disable-next-line max-len const svg = ''; const svgElement = removeSpan.createEl('span', { cls: 'ofn-list-item-remove-icon' }); svgElement.innerHTML = svg; - removeSpan.onClickEvent((e) => { + removeSpan.onClickEvent(() => { this.removeValue(value); span.remove(); }); }); } - async addValue(value: string) { + async addValue(value: string): Promise { this.values.push(value); this.addElement(value); this.emit('add', value); this.emit('update', this.values); } - addResetButton() { + addResetButton(): this { + // eslint-disable-next-line max-len const resetButton = this.controlEl.createEl('span', { cls: 'clickable-icon setting-restore-hotkey-button' }); + // eslint-disable-next-line max-len const svg = ''; resetButton.innerHTML = svg; - resetButton.onClickEvent((e) => { + resetButton.onClickEvent(() => { this.setValues(this.defaultValues); }); return this; } - removeValue(value: string) { + removeValue(value: string): void { this.values = this.values.filter((v) => v !== value); this.listEl.find(`[extension='${value}']`).remove(); this.emit('remove', value); diff --git a/src/functions/excalidraw.ts b/src/functions/excalidraw.ts index 81eb8c8..8b0286e 100644 --- a/src/functions/excalidraw.ts +++ b/src/functions/excalidraw.ts @@ -1,19 +1,36 @@ import type { WorkspaceLeaf, App } from 'obsidian'; -export async function openExcalidrawView(leaf: WorkspaceLeaf) { - const { excalidraw, excalidrawEnabled } = await getExcalidrawPlugin(this.app); - if (excalidrawEnabled) { +interface ExcalidrawPlugin { + setExcalidrawView(leaf: WorkspaceLeaf): void; +} + +export async function openExcalidrawView( + app: App, + leaf: WorkspaceLeaf, +): Promise { + const { excalidraw, excalidrawEnabled } = await getExcalidrawPlugin(app); + if (excalidrawEnabled && excalidraw) { excalidraw.setExcalidrawView(leaf); } } -export async function getExcalidrawPlugin(app: App) { - const excalidraw = (app as any).plugins.plugins['obsidian-excalidraw-plugin']; - const excalidrawEnabled = (app as any).plugins.enabledPlugins.has( - 'obsidian-excalidraw-plugin', +export async function getExcalidrawPlugin( + app: App, +): Promise<{ excalidraw: ExcalidrawPlugin | null; excalidrawEnabled: boolean }> { + const { plugins: pluginManager } = app as App & { + plugins: { + plugins: Record; + enabledPlugins: Set; + }; + }; + const excalidraw = ( + pluginManager.plugins[ + 'obsidian-excalidraw-plugin' + ] as unknown as ExcalidrawPlugin | undefined ); + const excalidrawEnabled = pluginManager.enabledPlugins.has('obsidian-excalidraw-plugin'); return { - excalidraw, + excalidraw: excalidraw ?? null, excalidrawEnabled, }; } diff --git a/src/functions/folderNoteFunctions.ts b/src/functions/folderNoteFunctions.ts index 17f6944..69e1830 100644 --- a/src/functions/folderNoteFunctions.ts +++ b/src/functions/folderNoteFunctions.ts @@ -1,15 +1,39 @@ +/* eslint-disable complexity */ +/* eslint-disable max-len */ import type FolderNotesPlugin from '../main'; import ExistingFolderNoteModal from '../modals/ExistingNote'; import { applyTemplate } from '../template'; -import type { TAbstractFile } from 'obsidian'; -import { TFolder, TFile, Keymap } from 'obsidian'; +import { + TFolder, + TFile, + Keymap, + type TAbstractFile, + type MarkdownView, + type WorkspaceLeaf, +} from 'obsidian'; import DeleteConfirmationModal from '../modals/DeleteConfirmation'; -import { addExcludedFolder, deleteExcludedFolder, getDetachedFolder, getExcludedFolder, updateExcludedFolder } from '../ExcludeFolders/functions/folderFunctions'; +import { + addExcludedFolder, + deleteExcludedFolder, + getDetachedFolder, + getExcludedFolder, + updateExcludedFolder, +} from '../ExcludeFolders/functions/folderFunctions'; import { ExcludedFolder } from '../ExcludeFolders/ExcludeFolder'; import { openExcalidrawView } from './excalidraw'; import { AskForExtensionModal } from 'src/modals/AskForExtension'; -import { addCSSClassToFileExplorerEl, removeCSSClassFromFileExplorerEL, removeActiveFolder, setActiveFolder } from 'src/functions/styleFunctions'; -import { getFolderNameFromPathString, getFolderPathFromString, removeExtension } from 'src/functions/utils'; +import { + addCSSClassToFileExplorerEl, + removeCSSClassFromFileExplorerEL, + removeActiveFolder, + setActiveFolder, +} from 'src/functions/styleFunctions'; +import { + getFolderNameFromPathString, + getFolderPathFromString, + removeExtension, +} from 'src/functions/utils'; + const defaultExcalidrawTemplate = `--- @@ -27,24 +51,36 @@ tags: [excalidraw] \`\`\` %%`; -export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: string, openFile: boolean, extension?: string, displayModal?: boolean, preexistingNote?: TFile) { - const leaf = plugin.app.workspace.getLeaf(false); - const folderName = getFolderNameFromPathString(folderPath); - const fileName = plugin.settings.folderNoteName.replace('{{folder_name}}', folderName); - let folderNote = getFolderNote(plugin, folderPath); - if (preexistingNote) { - folderNote = preexistingNote; - } - let folderNoteType = extension ?? plugin.settings.folderNoteType; - const detachedFolder = getDetachedFolder(plugin, folderPath); - let path = ''; +export async function createFolderNote( + plugin: FolderNotesPlugin, + folderPath: string, + openFile: boolean, + extension?: string, + displayModal?: boolean, + preexistingNote?: TFile, +): Promise { + let { + leaf, + fileName, + folderNote, + folderNoteType, + detachedFolder, + path, + } = getArgs(plugin, folderPath, extension, preexistingNote); if (folderNoteType === '.excalidraw') { folderNoteType = '.md'; extension = '.excalidraw'; } else if (folderNoteType === '.ask') { if (plugin.askModalCurrentlyOpen) return; - return new AskForExtensionModal(plugin, folderPath, openFile, folderNoteType, displayModal, preexistingNote).open(); + return new AskForExtensionModal( + plugin, + folderPath, + openFile, + folderNoteType, + displayModal, + preexistingNote, + ).open(); } if (plugin.settings.storageLocation === 'parentFolder') { @@ -61,27 +97,7 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st } if (detachedFolder && folderNote?.extension !== extension && folderNote) { - deleteExcludedFolder(plugin, detachedFolder); - removeCSSClassFromFileExplorerEL(folderNote?.path, 'is-folder-note', false, plugin); - const folder = plugin.app.vault.getAbstractFileByPath(folderPath) as TFolder; - if (!folderNote || folderNote.basename !== fileName) return; - let count = 1; - let newName = removeExtension(folderNote.path) + ` (${count}).${folderNote.path.split('.').pop()}`; - while (count < 100 && plugin.app.vault.getAbstractFileByPath(newName)) { - count++; - newName = removeExtension(folderNote.path) + ` (${count}).${folderNote.path.split('.').pop()}`; - } - const [excludedFolder, excludedFolderExisted, disabledSync] = await tempDisableSync(plugin, folder); - - await plugin.app.fileManager.renameFile(folderNote, newName).then(() => { - if (!excludedFolder) return; - if (!excludedFolderExisted) { - deleteExcludedFolder(plugin, excludedFolder); - } else if (!disabledSync) { - excludedFolder.disableSync = false; - updateExcludedFolder(plugin, excludedFolder, excludedFolder); - } - }); + await handleTurnNoteIntoFolderNote(plugin, folderNote, detachedFolder, folderPath, fileName); } if (!extension) { @@ -89,33 +105,7 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st } if (!folderNote) { - let content = ''; - if (extension !== '.md' && extension) { - if (plugin.settings.templatePath && folderNoteType.split('.').pop() === plugin.settings.templatePath.split('.').pop()) { - const templateFile = plugin.app.vault.getAbstractFileByPath(plugin.settings.templatePath); - if (templateFile instanceof TFile) { - if (['md', 'canvas', 'txt'].includes(templateFile.extension)) { - content = await plugin.app.vault.read(templateFile); - if (extension === '.excalidraw' && !content.includes('==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==')) { - content = defaultExcalidrawTemplate; - } - } else { - return plugin.app.vault.readBinary(templateFile).then(async (data) => { - folderNote = await plugin.app.vault.createBinary(path, data); - if (openFile) { - await leaf.openFile(folderNote); - } - }); - } - } - } else if (plugin.settings.folderNoteType === '.excalidraw' || extension === '.excalidraw') { - content = defaultExcalidrawTemplate; - } else if (plugin.settings.folderNoteType === '.canvas') { - content = '{}'; - } - } - - folderNote = await plugin.app.vault.create(path, content); + folderNote = await handleCreateFolderNote(plugin, folderNoteType, openFile, leaf, folderNote, path, extension); } else { await plugin.app.fileManager.renameFile(folderNote, path); } @@ -131,11 +121,13 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st } await leaf.openFile(folderNote); if (plugin.settings.folderNoteType === '.excalidraw' || extension === '.excalidraw') { - openExcalidrawView(leaf); + openExcalidrawView(plugin.app, leaf); } } - const matchingExtension = extension?.split('.').pop() === plugin.settings.templatePath.split('.').pop(); + const matchingExtension = + extension?.split('.').pop() === + plugin.settings.templatePath.split('.').pop(); if (folderNote && matchingExtension && plugin.settings.folderNoteType !== '.excalidraw') { applyTemplate(plugin, folderNote, leaf, plugin.settings.templatePath); } @@ -146,19 +138,159 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); } -export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, folder: TFolder, folderNote?: TFile | null | TAbstractFile, skipConfirmation?: boolean) { +function getArgs( + plugin: FolderNotesPlugin, + folderPath: string, + extension?: string, + preexistingNote?: TFile, +): { + leaf: WorkspaceLeaf; + fileName: string; + folderNote: TFile | null | undefined; + folderNoteType: string; + detachedFolder: ExcludedFolder | undefined; + path: string | ''; +} { + const leaf = plugin.app.workspace.getLeaf(false); + const folderName = getFolderNameFromPathString(folderPath); + const fileName = plugin.settings.folderNoteName.replace('{{folder_name}}', folderName); + let folderNote = getFolderNote(plugin, folderPath); + if (preexistingNote) { + folderNote = preexistingNote; + } + let folderNoteType = extension ?? plugin.settings.folderNoteType; + const detachedFolder = getDetachedFolder(plugin, folderPath); + let path = ''; + + return { + leaf, + fileName, + folderNote, + folderNoteType, + detachedFolder, + path, + }; +} + +async function handleCreateFolderNote(plugin: FolderNotesPlugin, folderNoteType: string, openFile: boolean, leaf: WorkspaceLeaf, folderNote: TFile | null | undefined, path: string, extension?: string): Promise { + let content = ''; + if (extension !== '.md' && extension) { + if ( + plugin.settings.templatePath && + folderNoteType.split('.').pop() === + plugin.settings.templatePath.split('.').pop() + ) { + const templateFile = plugin.app.vault.getAbstractFileByPath( + plugin.settings.templatePath, + ); + if (templateFile instanceof TFile) { + if (['md', 'canvas', 'txt'].includes(templateFile.extension)) { + content = await plugin.app.vault.read(templateFile); + if ( + extension === '.excalidraw' && + !content.includes( + '==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==', + ) + ) { + content = defaultExcalidrawTemplate; + } + } else { + plugin.app.vault.readBinary(templateFile).then(async (data) => { + folderNote = await plugin.app.vault.createBinary(path, data); + if (openFile) { + await leaf.openFile(folderNote); + } + return folderNote; + }); + } + } + } else if ( + plugin.settings.folderNoteType === '.excalidraw' || + extension === '.excalidraw' + ) { + content = defaultExcalidrawTemplate; + } else if (plugin.settings.folderNoteType === '.canvas') { + content = '{}'; + } + } + + folderNote = await plugin.app.vault.create(path, content); + return folderNote; +} + +async function handleTurnNoteIntoFolderNote( + plugin: FolderNotesPlugin, + folderNote: TFile, + detachedFolder: ExcludedFolder, + folderPath: string, + fileName: string, +): Promise { + deleteExcludedFolder(plugin, detachedFolder); + removeCSSClassFromFileExplorerEL(folderNote?.path, 'is-folder-note', false, plugin); + const folder = plugin.app.vault.getAbstractFileByPath(folderPath) as TFolder; + if (!folderNote || folderNote.basename !== fileName) return; + let count = 1; + const baseName = removeExtension(folderNote.path); + const ext = folderNote.path.split('.').pop(); + let newName = `${baseName} (${count}).${ext}`; + const MAX_FOLDER_NOTE_RENAME_ATTEMPTS = 100; + + while ( + count < MAX_FOLDER_NOTE_RENAME_ATTEMPTS && + plugin.app.vault.getAbstractFileByPath(newName) + ) { + count++; + newName = `${baseName} (${count}).${ext}`; + } + const [ + excludedFolder, + excludedFolderExisted, + disabledSync, + ] = await tempDisableSync(plugin, folder); + + await plugin.app.fileManager.renameFile(folderNote, newName).then(() => { + if (!excludedFolder) return; + if (!excludedFolderExisted) { + deleteExcludedFolder(plugin, excludedFolder); + } else if (!disabledSync) { + excludedFolder.disableSync = false; + updateExcludedFolder(plugin, excludedFolder, excludedFolder); + } + }); +} + +export async function turnIntoFolderNote( + plugin: FolderNotesPlugin, + file: TFile, + folder: TFolder, + folderNote?: TFile | null | TAbstractFile, + skipConfirmation?: boolean, +): Promise { const { extension } = file; const detachedExcludedFolder = getDetachedFolder(plugin, folder.path); if (folderNote) { - if (plugin.settings.showRenameConfirmation && !skipConfirmation && !detachedExcludedFolder) { + if ( + plugin.settings.showRenameConfirmation && + !skipConfirmation && + !detachedExcludedFolder + ) { return new ExistingFolderNoteModal(plugin.app, plugin, file, folder, folderNote).open(); } removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin); - const [excludedFolder, excludedFolderExisted, disabledSync] = await tempDisableSync(plugin, folder); + const [ + excludedFolder, + excludedFolderExisted, + disabledSync, + ] = await tempDisableSync(plugin, folder); + + const CTIME_SLICE_START = 10; + const RANDOM_SUFFIX_MAX = 1000; + const randomSuffix = Math.floor(Math.random() * RANDOM_SUFFIX_MAX); + const ctimeSuffix = file.stat.ctime.toString().slice(CTIME_SLICE_START); + const newPath = `${folder.path}/${folder.name} (${ctimeSuffix}${randomSuffix}).${extension}`; - const newPath = `${folder.path}/${folder.name} (${file.stat.ctime.toString().slice(10) + Math.floor(Math.random() * 1000)}).${extension}`; plugin.app.fileManager.renameFile(folderNote, newPath).then(() => { if (!excludedFolder) return; if (!excludedFolderExisted) { @@ -195,14 +327,28 @@ export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, setActiveFolder(folder.path, plugin); } -export async function tempDisableSync(plugin: FolderNotesPlugin, folder: TFolder): Promise<[excludedFolder: ExcludedFolder | undefined, excludedFolderExisted: boolean, disabledSync: boolean]> { +export async function tempDisableSync( + plugin: FolderNotesPlugin, + folder: TFolder, +): Promise< + [ + excludedFolder: ExcludedFolder | undefined, + excludedFolderExisted: boolean, + disabledSync: boolean + ] +> { let excludedFolder = getExcludedFolder(plugin, folder.path, false); let excludedFolderExisted = true; let disabledSync = false; if (!excludedFolder) { excludedFolderExisted = false; - excludedFolder = new ExcludedFolder(folder.path, plugin.settings.excludeFolders.length, undefined, plugin); + excludedFolder = new ExcludedFolder( + folder.path, + plugin.settings.excludeFolders.length, + undefined, + plugin, + ); excludedFolder.disableSync = true; addExcludedFolder(plugin, excludedFolder); } else if (!excludedFolder.disableSync) { @@ -214,7 +360,11 @@ export async function tempDisableSync(plugin: FolderNotesPlugin, folder: TFolder return [excludedFolder, excludedFolderExisted, disabledSync]; } -export async function openFolderNote(plugin: FolderNotesPlugin, file: TAbstractFile, evt?: MouseEvent) { +export async function openFolderNote( + plugin: FolderNotesPlugin, + file: TAbstractFile, + evt?: MouseEvent, +): Promise { const { path } = file; const focusExistingTab = plugin.settings.focusExistingTab && plugin.settings.openInNewTab; const activeFilePath = plugin.app.workspace.getActiveFile()?.path; @@ -230,7 +380,7 @@ export async function openFolderNote(plugin: FolderNotesPlugin, file: TAbstractF plugin.app.workspace.iterateAllLeaves((leaf) => { if ( leaf.getViewState().type === 'markdown' && - (leaf.view as import('obsidian').MarkdownView).file?.path === path + (leaf.view as MarkdownView).file?.path === path ) { foundLeaf = leaf; } @@ -240,14 +390,19 @@ export async function openFolderNote(plugin: FolderNotesPlugin, file: TAbstractF if (foundLeaf) { plugin.app.workspace.setActiveLeaf(foundLeaf, { focus: true }); } else { - const leaf = plugin.app.workspace.getLeaf(Keymap.isModEvent(evt) || plugin.settings.openInNewTab); + const shouldOpenInNewTab = Keymap.isModEvent(evt) || plugin.settings.openInNewTab; + const leaf = plugin.app.workspace.getLeaf(shouldOpenInNewTab); if (file instanceof TFile) { await leaf.openFile(file); } } } -export async function deleteFolderNote(plugin: FolderNotesPlugin, file: TFile, displayModal: boolean) { +export async function deleteFolderNote( + plugin: FolderNotesPlugin, + file: TFile, + displayModal: boolean, +): Promise { if (plugin.settings.showDeleteConfirmation && displayModal) { return new DeleteConfirmationModal(plugin.app, plugin, file).open(); } @@ -272,7 +427,7 @@ export async function deleteFolderNote(plugin: FolderNotesPlugin, file: TFile, d } } -export function extractFolderName(template: string, changedFileName: string) { +export function extractFolderName(template: string, changedFileName: string): string | null { const [prefix, suffix] = template.split('{{folder_name}}'); if (prefix.trim() === '' && suffix.trim() === '') { return changedFileName; @@ -288,39 +443,23 @@ export function extractFolderName(template: string, changedFileName: string) { return null; } -export function getFolderNote(plugin: FolderNotesPlugin, folderPath: string, storageLocation?: string, file?: TFile, oldFolderNoteName?: string) { - if (!folderPath) return null; - const folder = { - path: folderPath, - name: getFolderNameFromPathString(folderPath), - }; - const folderNoteName = oldFolderNoteName ?? plugin.settings.folderNoteName; - - let fileName = folderNoteName.replace('{{folder_name}}', folder.name); - if (file) { - fileName = folderNoteName.replace('{{folder_name}}', file.basename); - } - if (!fileName) return null; - - if ((plugin.settings.storageLocation === 'parentFolder' || storageLocation === 'parentFolder') && storageLocation !== 'insideFolder') { - folder.path = getFolderPathFromString(folderPath); - } - - - let path = `${folder.path}/${fileName}`; - folder.path === '/' ? path = fileName : path = `${folder.path}/${fileName}`; - - - let { folderNoteType } = plugin.settings; - if (folderNoteType === '.excalidraw') { - folderNoteType = '.md'; - } - - let folderNote = plugin.app.vault.getAbstractFileByPath(path + folderNoteType); - if (folderNote instanceof TFile && plugin.settings.supportedFileTypes.includes(plugin.settings.folderNoteType.replace('.', ''))) { +function findFolderNoteFile( + plugin: FolderNotesPlugin, + path: string, + primaryType: string, +): TFile | null { + let folderNote = plugin.app.vault.getAbstractFileByPath(path + primaryType); + if ( + folderNote instanceof TFile && + plugin.settings.supportedFileTypes.includes(primaryType.replace('.', '')) + ) { return folderNote; } - const supportedFileTypes = plugin.settings.supportedFileTypes.filter((type) => type !== plugin.settings.folderNoteType.replace('.', '')); + + const supportedFileTypes = plugin.settings.supportedFileTypes.filter( + (type) => type !== primaryType.replace('.', ''), + ); + for (let type of supportedFileTypes) { if (type === 'excalidraw' || type === '.excalidraw') { type = '.md'; @@ -333,13 +472,40 @@ export function getFolderNote(plugin: FolderNotesPlugin, folderPath: string, sto return folderNote; } } - + return null; } -export function detachFolderNote(plugin: FolderNotesPlugin, file: TFile) { +export function getFolderNote( + plugin: FolderNotesPlugin, + folderPath: string, + storageLocation?: string, + file?: TFile, + oldFolderNoteName?: string, +): TFile | null | undefined { + const folder = getFolderInfo(folderPath); + if (!folder) return null; + + let fileName = resolveFileName(plugin, folder, file, oldFolderNoteName); + if (!fileName) return null; + + adjustFolderPathForStorage(folder, folderPath, plugin, storageLocation); + + const path = buildFullPath(folder, fileName); + const primaryType = normalizeFolderNoteType(plugin.settings.folderNoteType); + + return findFolderNoteFile(plugin, path, primaryType); +} + + +export function detachFolderNote(plugin: FolderNotesPlugin, file: TFile): void { const folder = getFolder(plugin, file); if (!folder) return; - const excludedFolder = new ExcludedFolder(folder.path, plugin.settings.excludeFolders.length, undefined, plugin); + const excludedFolder = new ExcludedFolder( + folder.path, + plugin.settings.excludeFolders.length, + undefined, + plugin, + ); excludedFolder.hideInSettings = true; excludedFolder.disableFolderNote = true; excludedFolder.disableSync = true; @@ -351,16 +517,28 @@ export function detachFolderNote(plugin: FolderNotesPlugin, file: TFile) { } -export function getFolder(plugin: FolderNotesPlugin, file: TFile, storageLocation?: string) { +export function getFolder( + plugin: FolderNotesPlugin, + file: TFile, + storageLocation?: string, +): TFolder | TAbstractFile | null { if (!file) return null; let folderName = extractFolderName(plugin.settings.folderNoteName, file.basename); - if (plugin.settings.folderNoteName === file.basename && plugin.settings.storageLocation === 'insideFolder') { + if ( + plugin.settings.folderNoteName === file.basename && + plugin.settings.storageLocation === 'insideFolder' + ) { folderName = file.parent?.name ?? ''; } if (!folderName) return null; let folderPath = getFolderPathFromString(file.path); let folder: TFolder | TAbstractFile | null = null; - if ((plugin.settings.storageLocation === 'parentFolder' || storageLocation === 'parentFolder') && storageLocation !== 'insideFolder') { + + if ( + (plugin.settings.storageLocation === 'parentFolder' || + storageLocation === 'parentFolder') && + storageLocation !== 'insideFolder' + ) { if (folderPath.trim() === '' || folderPath === '/') { folderPath = folderName; } else { @@ -370,11 +548,16 @@ export function getFolder(plugin: FolderNotesPlugin, file: TFile, storageLocatio } else { folder = plugin.app.vault.getAbstractFileByPath(folderPath); } + if (!folder) { return null; } return folder; } -export function getFolderNoteFolder(plugin: FolderNotesPlugin, folderNote: TFile | string, fileName: string) { +export function getFolderNoteFolder( + plugin: FolderNotesPlugin, + folderNote: TFile | string, + fileName: string, +): TFolder | TAbstractFile | null { if (!folderNote) return null; let filePath = ''; if (typeof folderNote === 'string') { @@ -399,3 +582,46 @@ export function getFolderNoteFolder(plugin: FolderNotesPlugin, folderNote: TFile if (!folder) { return null; } return folder; } + +function getFolderInfo(folderPath: string): { path: string; name: string } | null { + if (!folderPath) return null; + return { + path: folderPath, + name: getFolderNameFromPathString(folderPath), + }; +} + +function resolveFileName( + plugin: FolderNotesPlugin, + folder: { path: string; name: string }, + file?: TFile, + oldFolderNoteName?: string, +): string | null { + const templateName = oldFolderNoteName ?? plugin.settings.folderNoteName; + if (!templateName) return null; + const nameSource = file ? file.basename : folder.name; + return templateName.replace('{{folder_name}}', nameSource); +} + +function adjustFolderPathForStorage( + folder: { path: string; name: string }, + folderPath: string, + plugin: FolderNotesPlugin, + storageLocation?: string, +): void { + if ( + (plugin.settings.storageLocation === 'parentFolder' || + storageLocation === 'parentFolder') && + storageLocation !== 'insideFolder' + ) { + folder.path = getFolderPathFromString(folderPath); + } +} + +function buildFullPath(folder: { path: string }, fileName: string): string { + return folder.path === '/' ? fileName : `${folder.path}/${fileName}`; +} + +function normalizeFolderNoteType(type: string): string { + return type === '.excalidraw' ? '.md' : type; +} diff --git a/src/functions/styleFunctions.ts b/src/functions/styleFunctions.ts index d65d886..a99c6b8 100644 --- a/src/functions/styleFunctions.ts +++ b/src/functions/styleFunctions.ts @@ -1,6 +1,10 @@ import { TFile, TFolder } from 'obsidian'; import type FolderNotesPlugin from '../main'; -import { getDetachedFolder, getExcludedFolder, addExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; +import { + getDetachedFolder, + getExcludedFolder, + addExcludedFolder, +} from 'src/ExcludeFolders/functions/folderFunctions'; import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions'; import { getFileExplorer } from './utils'; import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder'; @@ -9,7 +13,7 @@ import type FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main'; /** * @description Refreshes the CSS classes for all folder notes in the file explorer. */ -export function refreshAllFolderStyles(forceReload = false, plugin: FolderNotesPlugin) { +export function refreshAllFolderStyles(forceReload = false, plugin: FolderNotesPlugin): void { if (plugin.activeFileExplorer === getFileExplorer(plugin) && !forceReload) { return; } plugin.activeFileExplorer = getFileExplorer(plugin); plugin.app.vault.getAllLoadedFiles().forEach(async (file) => { @@ -22,7 +26,10 @@ export function refreshAllFolderStyles(forceReload = false, plugin: FolderNotesP /** * @description Updates the CSS classes for a specific folder in the file explorer. */ -export async function updateCSSClassesForFolder(folderPath: string, plugin: FolderNotesPlugin) { +export async function updateCSSClassesForFolder( + folderPath: string, + plugin: FolderNotesPlugin, +): Promise { const folder = plugin.app.vault.getAbstractFileByPath(folderPath); if (!folder || !(folder instanceof TFolder)) { return; } @@ -64,7 +71,10 @@ export async function updateCSSClassesForFolder(folderPath: string, plugin: Fold /** * @description Updates the CSS classes for a folder note file in the file explorer and then also updates the folder it belongs to. */ -export async function updateCSSClassesForFolderNote(filePath: string, plugin: FolderNotesPlugin) { +export async function updateCSSClassesForFolderNote( + filePath: string, + plugin: FolderNotesPlugin, +): Promise { const file = plugin.app.vault.getAbstractFileByPath(filePath); if (!file || !(file instanceof TFile)) { return; } @@ -74,17 +84,25 @@ export async function updateCSSClassesForFolderNote(filePath: string, plugin: Fo updateCSSClassesForFolder(folder.path, plugin); } -export function markFolderAndNoteWithClasses(file: TFile, folder: TFolder, plugin: FolderNotesPlugin) { +export function markFolderAndNoteWithClasses( + file: TFile, + folder: TFolder, + plugin: FolderNotesPlugin, +): void { markFileAsFolderNote(file, plugin); markFolderWithFolderNoteClasses(folder, plugin); } -export function clearFolderAndNoteClasses(folder: TFolder, file: TFile, plugin: FolderNotesPlugin) { +export function clearFolderAndNoteClasses( + folder: TFolder, + file: TFile, + plugin: FolderNotesPlugin, +): void { unmarkFileAsFolderNote(file, plugin); clearFolderNoteClassesFromFolder(folder, plugin); } -export function markFolderWithFolderNoteClasses(folder: TFolder, plugin: FolderNotesPlugin) { +export function markFolderWithFolderNoteClasses(folder: TFolder, plugin: FolderNotesPlugin): void { addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) { addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin); @@ -93,20 +111,20 @@ export function markFolderWithFolderNoteClasses(folder: TFolder, plugin: FolderN } } -export function markFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin) { +export function markFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin): void { addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); } -export function unmarkFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin) { +export function unmarkFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin): void { removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin); } -export function unmarkFolderAsFolderNote(folder: TFolder, plugin: FolderNotesPlugin) { +export function unmarkFolderAsFolderNote(folder: TFolder, plugin: FolderNotesPlugin): void { removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } -export function clearFolderNoteClassesFromFolder(folder: TFolder, plugin: FolderNotesPlugin) { +export function clearFolderNoteClassesFromFolder(folder: TFolder, plugin: FolderNotesPlugin): void { removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } @@ -115,11 +133,21 @@ export function clearFolderNoteClassesFromFolder(folder: TFolder, plugin: Folder * @param path Can be a folder or file path * @returns nothing */ -export async function addCSSClassToFileExplorerEl(path: string, cssClass: string, parent = false, plugin: FolderNotesPlugin, waitForCreate = false, count = 0) { +export async function addCSSClassToFileExplorerEl( + path: string, + cssClass: string, + parent = false, + plugin: FolderNotesPlugin, + waitForCreate = false, + count = 0, +): Promise { const fileExplorerItem = getFileExplorerElement(path, plugin); + const MAX_RETRIES = 5; + const RETRY_DELAY = 500; + if (!fileExplorerItem) { - if (waitForCreate && count < 5) { - await new Promise((r) => setTimeout(r, 500)); + if (waitForCreate && count < MAX_RETRIES) { + await new Promise((r) => setTimeout(r, RETRY_DELAY)); addCSSClassToFileExplorerEl(path, cssClass, parent, plugin, waitForCreate, count + 1); return; } @@ -143,7 +171,12 @@ export async function addCSSClassToFileExplorerEl(path: string, cssClass: string * @param cssClass The CSS class to remove from the file explorer element * @returns nothing */ -export function removeCSSClassFromFileExplorerEL(path: string | undefined, cssClass: string, parent: boolean, plugin: FolderNotesPlugin) { +export function removeCSSClassFromFileExplorerEL( + path: string | undefined, + cssClass: string, + parent: boolean, + plugin: FolderNotesPlugin, +): void { if (!path) return; const fileExplorerItem = getFileExplorerElement(path, plugin); document.querySelectorAll(`[data-path='${CSS.escape(path)}']`).forEach((item) => { @@ -161,15 +194,23 @@ export function removeCSSClassFromFileExplorerEL(path: string | undefined, cssCl } -export function getFileExplorerElement(path: string, plugin: FolderNotesPlugin | FolderOverviewPlugin): HTMLElement | null { +export function getFileExplorerElement( + path: string, + plugin: FolderNotesPlugin | FolderOverviewPlugin, +): HTMLElement | null { const fileExplorer = getFileExplorer(plugin); if (!fileExplorer?.view?.fileItems) { return null; } const fileExplorerItem = fileExplorer.view.fileItems?.[path]; return fileExplorerItem?.selfEl ?? fileExplorerItem?.titleEl ?? null; } -export function showFolderNoteInFileExplorer(path: string, plugin: FolderNotesPlugin) { - const excludedFolder = new ExcludedFolder(path, plugin.settings.excludeFolders.length, undefined, plugin); +export function showFolderNoteInFileExplorer(path: string, plugin: FolderNotesPlugin): void { + const excludedFolder = new ExcludedFolder( + path, + plugin.settings.excludeFolders.length, + undefined, + plugin, + ); excludedFolder.subFolders = false; excludedFolder.disableSync = false; excludedFolder.disableAutoCreate = false; @@ -183,7 +224,7 @@ export function showFolderNoteInFileExplorer(path: string, plugin: FolderNotesPl updateCSSClassesForFolder(path, plugin); } -export function hideFolderNoteInFileExplorer(folderPath: string, plugin: FolderNotesPlugin) { +export function hideFolderNoteInFileExplorer(folderPath: string, plugin: FolderNotesPlugin): void { plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter( (folder) => (folder.path !== folderPath) && folder.showFolderNote); plugin.saveSettings(false); @@ -191,7 +232,7 @@ export function hideFolderNoteInFileExplorer(folderPath: string, plugin: FolderN updateCSSClassesForFolder(folderPath, plugin); } -export function setActiveFolder(folderPath: string, plugin: FolderNotesPlugin) { +export function setActiveFolder(folderPath: string, plugin: FolderNotesPlugin): void { const fileExplorerItem = getFileExplorerElement(folderPath, plugin); if (fileExplorerItem) { fileExplorerItem.addClass('fn-is-active'); @@ -199,7 +240,7 @@ export function setActiveFolder(folderPath: string, plugin: FolderNotesPlugin) { } } -export function removeActiveFolder(plugin: FolderNotesPlugin) { +export function removeActiveFolder(plugin: FolderNotesPlugin): void { if (plugin.activeFolderDom) { plugin.activeFolderDom.removeClass('fn-is-active'); plugin.activeFolderDom?.removeClass('has-focus'); diff --git a/src/functions/utils.ts b/src/functions/utils.ts index 468c5de..ec9f2ff 100644 --- a/src/functions/utils.ts +++ b/src/functions/utils.ts @@ -2,7 +2,7 @@ import { TFolder, TFile, View } from 'obsidian'; import type { FileExplorerWorkspaceLeaf, FileExplorerView } from 'src/globals'; import { getFolderNote } from './folderNoteFunctions'; import type FolderNotesPlugin from 'src/main'; -import type { FileExplorerLeaf } from 'obsidian-typings'; +import type { FileExplorerLeaf, FileTreeItem, TreeNode } from 'obsidian-typings'; import type FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main'; export function getFileNameFromPathString(path: string): string { @@ -10,11 +10,12 @@ export function getFileNameFromPathString(path: string): string { } export function getFolderNameFromPathString(path: string): string { + const PARENT_FOLDER_INDEX = -2; + const LAST_FOLDER_INDEX = -1; if (path.endsWith('.md') || path.endsWith('.canvas')) { - return path.split('/').slice(-2)[0]; + return path.split('/').slice(PARENT_FOLDER_INDEX)[0]; } - return path.split('/').slice(-1)[0]; - + return path.split('/').slice(LAST_FOLDER_INDEX)[0]; } export function removeExtension(name: string): string { @@ -39,16 +40,20 @@ export function getParentFolderPath(path: string): string { return this.getFolderPathFromString(this.getFolderPathFromString(path)); } -export function getFileExplorer(plugin: FolderNotesPlugin | FolderOverviewPlugin) { - return plugin.app.workspace.getLeavesOfType('file-explorer')[0] as any as FileExplorerWorkspaceLeaf; +export function getFileExplorer( + plugin: FolderNotesPlugin | FolderOverviewPlugin, +): FileExplorerWorkspaceLeaf { + // eslint-disable-next-line max-len + const leaf = plugin.app.workspace.getLeavesOfType('file-explorer')[0] as unknown as FileExplorerWorkspaceLeaf; + return leaf; } -export function getFileExplorerView(plugin: FolderNotesPlugin) { +export function getFileExplorerView(plugin: FolderNotesPlugin): FileExplorerView { return getFileExplorer(plugin).view; } -export function getFocusedItem(plugin: FolderNotesPlugin) { - const fileExplorer = getFileExplorer(plugin) as any as FileExplorerLeaf; +export function getFocusedItem(plugin: FolderNotesPlugin): TreeNode | null { + const fileExplorer = getFileExplorer(plugin) as unknown as FileExplorerLeaf; const { focusedItem } = fileExplorer.view.tree; return focusedItem; } diff --git a/src/globals.d.ts b/src/globals.d.ts index 1229822..53fff77 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -40,7 +40,6 @@ interface FileItem { el: HTMLDivElement; file: TFile; fileExplorer: FileExplorerView; - info: any; selfEl: HTMLDivElement; innerEl: HTMLDivElement; } @@ -48,7 +47,6 @@ interface FileItem { interface FolderItem { el: HTMLDivElement; fileExplorer: FileExplorerView; - info: any; selfEl: HTMLDivElement; innerEl: HTMLDivElement; file: TFolder; diff --git a/src/main.ts b/src/main.ts index 6f18134..ee94694 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,23 @@ - -import type { TAbstractFile, MarkdownPostProcessorContext, WorkspaceLeaf } from 'obsidian'; -import { Plugin, TFile, TFolder, parseYaml, Notice, Keymap, requireApiVersion, Platform, debounce } from 'obsidian'; -import type { FolderNotesSettings } from './settings/SettingsTab'; -import { DEFAULT_SETTINGS, SettingsTab } from './settings/SettingsTab'; +import { + type TAbstractFile, + type MarkdownPostProcessorContext, + type WorkspaceLeaf, + Plugin, TFile, TFolder, + parseYaml, Notice, Keymap, + requireApiVersion, Platform, debounce, +} from 'obsidian'; +import { + type FolderNotesSettings, DEFAULT_SETTINGS, SettingsTab, +} from './settings/SettingsTab'; import { Commands } from './Commands'; import type { FileExplorerWorkspaceLeaf } from './globals'; -import { registerFileExplorerObserver, unregisterFileExplorerObserver } from './events/MutationObserver'; +import { + registerFileExplorerObserver, unregisterFileExplorerObserver, +} from './events/MutationObserver'; import { handleRename } from './events/handleRename'; -import { getFolderNote, getFolder, openFolderNote, createFolderNote } from './functions/folderNoteFunctions'; +import { + getFolderNote, getFolder, openFolderNote, createFolderNote, +} from './functions/folderNoteFunctions'; import { handleCreate } from './events/handleCreate'; import { FrontMatterTitlePluginHandler } from './events/FrontMatterTitle'; import { FolderOverviewSettings } from './obsidian-folder-overview/src/modals/Settings'; @@ -15,10 +25,12 @@ import { FolderOverview } from './obsidian-folder-overview/src/FolderOverview'; import { TabManager } from './events/TabManager'; import './functions/ListComponent'; import { handleDelete } from './events/handleDelete'; -import { addCSSClassToFileExplorerEl, getFileExplorerElement, removeCSSClassFromFileExplorerEL, refreshAllFolderStyles, setActiveFolder, removeActiveFolder } from './functions/styleFunctions'; +import { + addCSSClassToFileExplorerEl, getFileExplorerElement, removeCSSClassFromFileExplorerEL, + refreshAllFolderStyles, setActiveFolder, removeActiveFolder, +} from './functions/styleFunctions'; import { getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; import type { FileExplorerView, InternalPlugin } from 'obsidian-typings'; -// import { getFocusedItem } from './functions/utils'; import { FOLDER_OVERVIEW_VIEW, FolderOverviewView } from './obsidian-folder-overview/src/view'; import { registerOverviewCommands } from './obsidian-folder-overview/src/Commands'; import { updateOverviewView, updateViewDropdown } from './obsidian-folder-overview/src/main'; @@ -26,7 +38,6 @@ import { FvIndexDB } from './obsidian-folder-overview/src/utils/IndexDB'; import { updateAllOverviews } from './obsidian-folder-overview/src/utils/functions'; export default class FolderNotesPlugin extends Plugin { - observer: MutationObserver; settings: FolderNotesSettings; settingsTab: SettingsTab; activeFolderDom: HTMLElement | null; @@ -43,7 +54,7 @@ export default class FolderNotesPlugin extends Plugin { private fileExplorerPlugin!: InternalPlugin; private fileExplorerView!: FileExplorerView; - async onload() { + async onload(): Promise { console.log('loading folder notes plugin'); await this.loadSettings(); this.settingsTab = new SettingsTab(this.app, this); @@ -54,17 +65,29 @@ export default class FolderNotesPlugin extends Plugin { // Add CSS Classes document.body.classList.add('folder-notes-plugin'); if (this.settings.hideFolderNote) { document.body.classList.add('hide-folder-note'); } - if (this.settings.hideCollapsingIconForEmptyFolders) { document.body.classList.add('fn-hide-empty-collapse-icon'); } + if (this.settings.hideCollapsingIconForEmptyFolders) { + document.body.classList.add('fn-hide-empty-collapse-icon'); + } if (this.settings.underlineFolder) { document.body.classList.add('folder-note-underline'); } if (this.settings.boldName) { document.body.classList.add('folder-note-bold'); } if (this.settings.cursiveName) { document.body.classList.add('folder-note-cursive'); } if (this.settings.boldNameInPath) { document.body.classList.add('folder-note-bold-path'); } - if (this.settings.cursiveNameInPath) { document.body.classList.add('folder-note-cursive-path'); } - if (this.settings.underlineFolderInPath) { document.body.classList.add('folder-note-underline-path'); } - if (this.settings.stopWhitespaceCollapsing) { document.body.classList.add('fn-whitespace-stop-collapsing'); } - if (this.settings.hideCollapsingIcon) { document.body.classList.add('fn-hide-collapse-icon'); } - if (!this.settings.highlightFolder) { document.body.classList.add('disable-folder-highlight'); } - // document.body.classList.add('fv-hide-link-list'); + if (this.settings.cursiveNameInPath) { + document.body.classList.add('folder-note-cursive-path'); + } + if (this.settings.underlineFolderInPath) { + document.body.classList.add('folder-note-underline-path'); + } + if (this.settings.stopWhitespaceCollapsing) { + document.body.classList.add('fn-whitespace-stop-collapsing'); + } + if (this.settings.hideCollapsingIcon) { + document.body.classList.add('fn-hide-collapse-icon'); + } + if (!this.settings.highlightFolder) { + document.body.classList.add('disable-folder-highlight'); + } + if (requireApiVersion('1.7.2')) { document.body.classList.add('version-1-7-2'); } @@ -74,23 +97,11 @@ export default class FolderNotesPlugin extends Plugin { this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this)); - // this.observer.observe(document.body, { - // childList: true, - // subtree: true, - // }); - if (!this.settings.persistentSettingsTab.afterRestart) { this.settings.settingsTab = 'general'; } this.registerDomEvent(window, 'keydown', (event: KeyboardEvent) => { - // Was a bit too buggy - // if (event.key === 'Enter') { - // const folderNote = getFolderNote(this, getFocusedItem(this)?.file?.path || ''); - // if (!folderNote) return; - // openFolderNote(this, folderNote); - // } - const { hoveredElement } = this; if (this.hoverLinkTriggered) return; if (!hoveredElement) return; @@ -143,12 +154,15 @@ export default class FolderNotesPlugin extends Plugin { this.handleVaultChange(); })); - this.registerMarkdownCodeBlockProcessor('folder-overview', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { - this.handleOverviewBlock(source, el, ctx); - }); + this.registerMarkdownCodeBlockProcessor( + 'folder-overview', + (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + this.handleOverviewBlock(source, el, ctx); + }, + ); } - onLayoutReady() { + onLayoutReady(): void { if (!this._loaded) { return; } @@ -175,8 +189,9 @@ export default class FolderNotesPlugin extends Plugin { const fileExplorerPlugin = this.app.internalPlugins.getEnabledPluginById('file-explorer'); if (fileExplorerPlugin) { - const originalRevealInFolder = fileExplorerPlugin.revealInFolder.bind(fileExplorerPlugin); - fileExplorerPlugin.revealInFolder = (file: TAbstractFile) => { + const originalRevealInFolder = fileExplorerPlugin.revealInFolder + .bind(fileExplorerPlugin); + fileExplorerPlugin.revealInFolder = (file: TAbstractFile): void => { if (file instanceof TFile) { const folder = getFolder(this, file); if (folder instanceof TFolder) { @@ -186,9 +201,10 @@ export default class FolderNotesPlugin extends Plugin { } document.body.classList.remove('hide-folder-note'); originalRevealInFolder.call(fileExplorerPlugin, folder); + const FOLDER_REVEAL_DELAY = 100; setTimeout(() => { document.body.classList.add('hide-folder-note'); - }, 100); + }, FOLDER_REVEAL_DELAY); return; } } @@ -201,26 +217,29 @@ export default class FolderNotesPlugin extends Plugin { if (!view) { return; } - // @ts-ignore - const editMode = view.editMode ?? view.sourceMode ?? this.app.workspace.activeEditor?.editMode; + // @ts-expect-error use internal API + const editMode = view.editMode ?? view.sourceMode + // @ts-expect-error use internal API + ?? this.app.workspace.activeEditor?.editMode; const plugin = this; if (!editMode) { return; } - // @ts-ignore const clipboardProto = editMode.clipboardManager.constructor.prototype; const originalHandleDragOver = clipboardProto.handleDragOver; const originalHandleDrop = clipboardProto.handleDrop; - clipboardProto.handleDragOver = function (evt: DragEvent, ...args: any[]) { + clipboardProto.handleDragOver = function (evt: DragEvent, ...args: unknown[]): void { const { dragManager } = this.app; const draggable = dragManager?.draggable; if (draggable?.file instanceof TFolder) { const folderNote = getFolderNote(plugin, draggable.file.path); if (folderNote) { - dragManager.setAction(window.i18next.t('interface.drag-and-drop.insert-link-here')); + dragManager.setAction( + window.i18next.t('interface.drag-and-drop.insert-link-here'), + ); return; } } @@ -228,7 +247,7 @@ export default class FolderNotesPlugin extends Plugin { return originalHandleDragOver.call(this, evt, ...args); }; - clipboardProto.handleDrop = function (evt: DragEvent, ...args: any[]) { + clipboardProto.handleDrop = function (evt: DragEvent, ...args: unknown[]): void { const { dragManager } = this.app; const draggable = dragManager?.draggable; @@ -248,57 +267,35 @@ export default class FolderNotesPlugin extends Plugin { } } - handleVaultChange() { + handleVaultChange(): void { if (!this.settings.fvGlobalSettings.autoUpdateLinks) return; + const DEBOUNCE_DELAY = 2000; debounce(() => { updateAllOverviews(this); - }, 2000, true)(); + }, DEBOUNCE_DELAY, true)(); } - handleFileExplorerClick(evt: MouseEvent) { + handleFileExplorerClick(evt: MouseEvent): void { const target = evt.target as HTMLElement; if (evt.shiftKey) return; + if (this.isMobileClickDisabled()) return; - if (Platform.isMobile && this.settings.disableOpenFolderNoteOnClick) return; - - // Check if the click is on a folder - const folderTitleEl = target.closest('.nav-folder-title') as HTMLElement; + const { folderTitleEl, onlyClickedOnFolderTitle } = this.getFolderTitleInfo(target); if (!folderTitleEl) return; + if (this.shouldIgnoreClickByWhitespaceOrCollapse(target, onlyClickedOnFolderTitle)) return; - const onlyClickedOnFolderTitle = !!target.closest('.nav-folder-title-content'); - // const folderTitleContentEl = target.closest('.nav-folder-title-content') as HTMLElement; - // if (folderTitleContentEl) { - // const rect = folderTitleContentEl.getBoundingClientRect(); - // const clickOffsetX = evt.clientX - rect.left; - // // Ignore clicks within the first N pixels, e.g., 20px where the icon is displayed - // if (clickOffsetX < 20) return; - // } - if (!this.settings.stopWhitespaceCollapsing && !onlyClickedOnFolderTitle) return; - - // Ignore clicks on the collapse icon - if (target.closest('.collapse-icon')) return; - - const folderPath = folderTitleEl.getAttribute('data-path'); + const folderPath = this.getValidFolderPath(folderTitleEl); if (!folderPath) return; - const excludedFolder = getExcludedFolder(this, folderPath, true); - if (excludedFolder?.disableFolderNote) return; - - const usedCtrl = Platform.isMacOS ? evt.metaKey : evt.ctrlKey; - + const usedCtrl = this.isCtrlUsed(evt); const folderNote = getFolderNote(this, folderPath); - if (!folderNote && (evt.altKey || Keymap.isModEvent(evt) === 'tab')) { - if ((this.settings.altKey && evt.altKey) || (usedCtrl && this.settings.ctrlKey)) { - createFolderNote(this, folderPath, true, undefined, true); - addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, this); - removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, this); - return; - } + + if (!folderNote && this.shouldCreateNote(evt, usedCtrl)) { + this.createNoteAndMark(folderPath); + return; } if (!(folderNote instanceof TFile)) return; - - if (this.settings.openWithCtrl && !usedCtrl) return; - if (this.settings.openWithAlt && !evt.altKey) return; + if (!this.shouldOpenNote(usedCtrl, evt)) return; if (!this.settings.enableCollapsing || usedCtrl) { evt.preventDefault(); @@ -308,6 +305,58 @@ export default class FolderNotesPlugin extends Plugin { openFolderNote(this, folderNote, evt); } + private isMobileClickDisabled(): boolean { + return Platform.isMobile && this.settings.disableOpenFolderNoteOnClick; + } + + private getFolderTitleInfo(target: HTMLElement): { + folderTitleEl: HTMLElement | null; + onlyClickedOnFolderTitle: boolean; + } { + const folderTitleEl = target.closest('.nav-folder-title') as HTMLElement | null; + const onlyClickedOnFolderTitle = !!target.closest('.nav-folder-title-content'); + return { folderTitleEl, onlyClickedOnFolderTitle }; + } + + private shouldIgnoreClickByWhitespaceOrCollapse( + target: HTMLElement, + onlyClickedOnFolderTitle: boolean, + ): boolean { + if (!this.settings.stopWhitespaceCollapsing && !onlyClickedOnFolderTitle) return true; + if (target.closest('.collapse-icon')) return true; + return false; + } + + private getValidFolderPath(folderTitleEl: HTMLElement): string | null { + const folderPath = folderTitleEl.getAttribute('data-path'); + if (!folderPath) return null; + const excludedFolder = getExcludedFolder(this, folderPath, true); + if (excludedFolder?.disableFolderNote) return null; + return folderPath; + } + + private isCtrlUsed(evt: MouseEvent): boolean { + return Platform.isMacOS ? evt.metaKey : evt.ctrlKey; + } + + private shouldCreateNote(evt: MouseEvent, usedCtrl: boolean): boolean { + const isTabMod = Keymap.isModEvent(evt) === 'tab'; + if (!(evt.altKey || isTabMod)) return false; + return (this.settings.altKey && evt.altKey) || (usedCtrl && this.settings.ctrlKey); + } + + private createNoteAndMark(folderPath: string): void { + createFolderNote(this, folderPath, true, undefined, true); + addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, this); + removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, this); + } + + private shouldOpenNote(usedCtrl: boolean, evt: MouseEvent): boolean { + if (this.settings.openWithCtrl && !usedCtrl) return false; + if (this.settings.openWithAlt && !evt.altKey) return false; + return true; + } + handleOverviewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): void { const observer = new MutationObserver(() => { const editButton = el.parentElement?.childNodes.item(1); @@ -317,8 +366,12 @@ export default class FolderNotesPlugin extends Plugin { e.preventDefault(); e.stopPropagation(); new FolderOverviewSettings( - this.app, this, parseYaml(source), - ctx, el, this.settings.defaultOverview, + this.app, + this, + parseYaml(source), + ctx, + el, + this.settings.defaultOverview, ).open(); }, { capture: true }); } @@ -331,21 +384,35 @@ export default class FolderNotesPlugin extends Plugin { try { if (this.app.workspace.layoutReady) { - const folderOverview = new FolderOverview(this, ctx, source, el, this.settings.defaultOverview); + const { defaultOverview } = this.settings; + const folderOverview = new FolderOverview( + this, + ctx, + source, + el, + defaultOverview, + ); folderOverview.create(this, el, ctx); } else { this.app.workspace.onLayoutReady(() => { - const folderOverview = new FolderOverview(this, ctx, source, el, this.settings.defaultOverview); + const folderOverview = new FolderOverview( + this, + ctx, + source, + el, + this.settings.defaultOverview, + ); folderOverview.create(this, el, ctx); }); } } catch (e) { + // eslint-disable-next-line max-len new Notice('Error creating folder overview (folder notes plugin) - check console for more details'); console.error(e); } } - async activateOverviewView() { + async activateOverviewView(): Promise { const { workspace } = this.app; let leaf: WorkspaceLeaf | null = null; @@ -362,13 +429,14 @@ export default class FolderNotesPlugin extends Plugin { workspace.revealLeaf(leaf); } - updateOverviewView = updateOverviewView; - updateViewDropdown = updateViewDropdown; + updateOverviewView: typeof updateOverviewView = updateOverviewView; + updateViewDropdown: typeof updateViewDropdown = updateViewDropdown; isEmptyFolderNoteFolder(folder: TFolder): boolean { const attachmentFolderPath = this.app.vault.getConfig('attachmentFolderPath') as string; const cleanAttachmentFolderPath = attachmentFolderPath?.replace('./', '') || ''; - const attachmentsAreInRootFolder = attachmentFolderPath === './' || attachmentFolderPath === ''; + const attachmentsAreInRootFolder = attachmentFolderPath === './' + || attachmentFolderPath === ''; const threshold = this.settings.storageLocation === 'insideFolder' ? 1 : 0; if (folder.children.length === 0) { addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, this); @@ -379,10 +447,13 @@ export default class FolderNotesPlugin extends Plugin { } else if (folder.children.length > threshold) { if (attachmentsAreInRootFolder) { return false; - } else if (this.settings.ignoreAttachmentFolder && this.app.vault.getAbstractFileByPath(`${folder.path}/${cleanAttachmentFolderPath}`)) { + } else if (this.settings.ignoreAttachmentFolder + && this.app.vault.getAbstractFileByPath( + `${folder.path}/${cleanAttachmentFolderPath}`)) { const folderPath = `${folder.path}/${cleanAttachmentFolderPath}`; const attachmentFolder = this.app.vault.getAbstractFileByPath(folderPath); - if (attachmentFolder instanceof TFolder && folder.children.length <= threshold + 1) { + if (attachmentFolder instanceof TFolder + && folder.children.length <= threshold + 1) { if (!folder.collapsed) { getFileExplorerElement(folder.path, this)?.click(); } @@ -395,13 +466,20 @@ export default class FolderNotesPlugin extends Plugin { return true; } - async changeFolderNameInExplorer(folder: TFolder, newName: string | null | undefined, waitForCreate = false, count = 0) { + async changeFolderNameInExplorer( + folder: TFolder, + newName: string | null | undefined, + waitForCreate = false, + count = 0, + ): Promise { + const MAX_RETRY_COUNT = 5; + const RETRY_DELAY_MS = 500; if (!newName) newName = folder.name; let fileExplorerItem = getFileExplorerElement(folder.path, this); if (!fileExplorerItem) { - if (waitForCreate && count < 5) { - await new Promise((r) => setTimeout(r, 500)); - this.changeFolderNameInExplorer(folder, newName, waitForCreate, count + 1); + if (waitForCreate && count < MAX_RETRY_COUNT) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); + void this.changeFolderNameInExplorer(folder, newName, waitForCreate, count + 1); return; } return; @@ -410,15 +488,19 @@ export default class FolderNotesPlugin extends Plugin { fileExplorerItem = fileExplorerItem?.querySelector('div.nav-folder-title-content'); if (!fileExplorerItem) { return; } if (this.settings.frontMatterTitle.explorer && this.settings.frontMatterTitle.enabled) { - fileExplorerItem.innerText = newName; - fileExplorerItem.setAttribute('old-name', folder.name); + (fileExplorerItem as HTMLElement).innerText = newName; + (fileExplorerItem as HTMLElement).setAttribute('old-name', folder.name); } else { - fileExplorerItem.innerText = folder.name; - fileExplorerItem.removeAttribute('old-name'); + (fileExplorerItem as HTMLElement).innerText = folder.name; + (fileExplorerItem as HTMLElement).removeAttribute('old-name'); } } - async changeFolderNameInPath(folder: TFolder, newName: string | null | undefined, breadcrumb: HTMLElement) { + async changeFolderNameInPath( + folder: TFolder, + newName: string | null | undefined, + breadcrumb: HTMLElement, + ): Promise { if (!newName) newName = folder.name; breadcrumb.textContent = folder.newName || folder.name; @@ -429,7 +511,7 @@ export default class FolderNotesPlugin extends Plugin { /** * Updates all folder names in the path above the note editor */ - updateAllBreadcrumbs(remove?: boolean) { + updateAllBreadcrumbs(remove?: boolean): void { if (!this.settings.frontMatterTitle.path && !remove) { return; } const viewHeaderItems = document.querySelectorAll('span.view-header-breadcrumb'); const files = this.app.vault.getAllLoadedFiles().filter((file) => file instanceof TFolder); @@ -449,7 +531,7 @@ export default class FolderNotesPlugin extends Plugin { }); } - onunload() { + onunload(): void { unregisterFileExplorerObserver(); document.body.classList.remove('folder-notes-plugin'); document.body.classList.remove('folder-note-underline'); @@ -461,7 +543,7 @@ export default class FolderNotesPlugin extends Plugin { } } - async loadSettings() { + async loadSettings(): Promise { const data = await this.loadData(); if (data) { if (data.allowWhitespaceCollapsing === true) { @@ -479,12 +561,14 @@ export default class FolderNotesPlugin extends Plugin { } if (!data) { return; } - const overview = (data as any).defaultOverview; + const overview = data.defaultOverview; if (!overview) { return; } - this.settings.defaultOverview = Object.assign({}, DEFAULT_SETTINGS.defaultOverview, overview); + this.settings.defaultOverview = Object.assign( + {}, DEFAULT_SETTINGS.defaultOverview, overview, + ); } - async saveSettings(reloadStyles?: boolean) { + async saveSettings(reloadStyles?: boolean): Promise { await this.saveData(this.settings); // cleanup any css if we need too if ((!this.settingsOpened || reloadStyles === true) && reloadStyles !== false) { diff --git a/src/modals/AddSupportedFileType.ts b/src/modals/AddSupportedFileType.ts index c4e802d..787f4e1 100644 --- a/src/modals/AddSupportedFileType.ts +++ b/src/modals/AddSupportedFileType.ts @@ -1,5 +1,4 @@ -import type { App, SettingTab } from 'obsidian'; -import { Modal, Setting, Notice } from 'obsidian'; +import { Modal, Setting, Notice, type App, type SettingTab } from 'obsidian'; import type FolderNotesPlugin from '../main'; import type { ListComponent } from 'src/functions/ListComponent'; @@ -17,7 +16,8 @@ export default class AddSupportedFileModal extends Modal { this.list = list; this.settingsTab = settingsTab; } - onOpen() { + + onOpen(): void { const { contentEl } = this; // close when user presses enter contentEl.addEventListener('keydown', (e) => { @@ -38,7 +38,7 @@ export default class AddSupportedFileModal extends Modal { }), ); } - async onClose() { + async onClose(): Promise { if (this.name.toLocaleLowerCase() === 'markdown') { this.name = 'md'; } @@ -47,9 +47,9 @@ export default class AddSupportedFileModal extends Modal { contentEl.empty(); this.settingsTab.display(); } else if (this.plugin.settings.supportedFileTypes.includes(this.name.toLowerCase())) { - return new Notice('This extension is already supported'); + new Notice('This extension is already supported'); + return; } else { - // @ts-ignore await this.list.addValue(this.name.toLowerCase()); this.settingsTab.display(); this.plugin.saveSettings(); diff --git a/src/modals/AskForExtension.ts b/src/modals/AskForExtension.ts index 50b5ee8..7acd569 100644 --- a/src/modals/AskForExtension.ts +++ b/src/modals/AskForExtension.ts @@ -1,5 +1,4 @@ -import type { TFile } from 'obsidian'; -import { FuzzySuggestModal } from 'obsidian'; +import { FuzzySuggestModal, type TFile } from 'obsidian'; import type FolderNotesPlugin from 'src/main'; import { createFolderNote } from 'src/functions/folderNoteFunctions'; export class AskForExtensionModal extends FuzzySuggestModal { @@ -9,7 +8,14 @@ export class AskForExtensionModal extends FuzzySuggestModal { openFile: boolean; useModal: boolean | undefined; existingNote: TFile | undefined; - constructor(plugin: FolderNotesPlugin, folderPath: string, openFile: boolean, extension: string, useModal?: boolean, existingNote?: TFile) { + constructor( + plugin: FolderNotesPlugin, + folderPath: string, + openFile: boolean, + extension: string, + useModal?: boolean, + existingNote?: TFile, + ) { super(plugin.app); this.plugin = plugin; this.folderPath = folderPath; @@ -21,17 +27,26 @@ export class AskForExtensionModal extends FuzzySuggestModal { } getItems(): string[] { - return this.plugin.settings.supportedFileTypes.filter((item) => item.toLowerCase() !== '.ask'); + return this.plugin.settings.supportedFileTypes.filter( + (item) => item.toLowerCase() !== '.ask', + ); } getItemText(item: string): string { return item; } - onChooseItem(item: string, evt: MouseEvent | KeyboardEvent) { + onChooseItem(item: string, _evt: MouseEvent | KeyboardEvent): void { this.plugin.askModalCurrentlyOpen = false; this.extension = '.' + item; - createFolderNote(this.plugin, this.folderPath, this.openFile, this.extension, this.useModal, this.existingNote); + createFolderNote( + this.plugin, + this.folderPath, + this.openFile, + this.extension, + this.useModal, + this.existingNote, + ); this.close(); } } diff --git a/src/modals/DeleteConfirmation.ts b/src/modals/DeleteConfirmation.ts index c179ec4..ac857b6 100644 --- a/src/modals/DeleteConfirmation.ts +++ b/src/modals/DeleteConfirmation.ts @@ -1,5 +1,4 @@ -import type { App, TFile } from 'obsidian'; -import { Modal, Platform } from 'obsidian'; +import { Modal, Platform, type App, type TFile } from 'obsidian'; import type FolderNotesPlugin from '../main'; import { deleteFolderNote } from 'src/functions/folderNoteFunctions'; export default class DeleteConfirmationModal extends Modal { @@ -12,21 +11,25 @@ export default class DeleteConfirmationModal extends Modal { this.app = app; this.file = file; } - onOpen() { + onOpen(): void { const { contentEl, plugin } = this; const modalTitle = contentEl.createDiv({ cls: 'fn-modal-title' }); const modalContent = contentEl.createDiv({ cls: 'fn-modal-content' }); modalTitle.createEl('h2', { text: 'Delete folder note' }); + // eslint-disable-next-line max-len modalContent.createEl('p', { text: `Are you sure you want to delete the folder note '${this.file.name}' ?` }); switch (plugin.settings.deleteFilesAction) { case 'trash': modalContent.createEl('p', { text: 'It will be moved to your system trash.' }); break; case 'obsidianTrash': + // eslint-disable-next-line max-len modalContent.createEl('p', { text: 'It will be moved to your Obsidian trash, which is located in the ".trash" hidden folder in your vault.' }); break; case 'delete': - modalContent.createEl('p', { text: 'It will be permanently deleted.' }).setCssStyles({ color: 'red' }); + modalContent + .createEl('p', { text: 'It will be permanently deleted.' }) + .setCssStyles({ color: 'red' }); break; } @@ -47,7 +50,10 @@ export default class DeleteConfirmationModal extends Modal { plugin.saveSettings(); }); } else { - const confirmButton = buttonContainer.createEl('button', { text: 'Delete and don\'t ask again', cls: 'mod-destructive' }); + const confirmButton = buttonContainer.createEl('button', { + text: 'Delete and don\'t ask again', + cls: 'mod-destructive', + }); confirmButton.addEventListener('click', async () => { plugin.settings.showDeleteConfirmation = false; plugin.saveSettings(); @@ -56,19 +62,25 @@ export default class DeleteConfirmationModal extends Modal { }); } - const deleteButton = buttonContainer.createEl('button', { text: 'Delete', cls: 'mod-warning' }); + const deleteButton = buttonContainer.createEl('button', { + text: 'Delete', + cls: 'mod-warning', + }); deleteButton.addEventListener('click', async () => { this.close(); deleteFolderNote(plugin, this.file, false); }); deleteButton.focus(); - const cancelButton = buttonContainer.createEl('button', { text: 'Cancel', cls: 'mod-cancel' }); + const cancelButton = buttonContainer.createEl('button', { + text: 'Cancel', + cls: 'mod-cancel', + }); cancelButton.addEventListener('click', async () => { this.close(); }); } - onClose() { + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/ExistingNote.ts b/src/modals/ExistingNote.ts index d3c680c..95ac4da 100644 --- a/src/modals/ExistingNote.ts +++ b/src/modals/ExistingNote.ts @@ -1,5 +1,12 @@ -import type { App, TFile, TFolder, TAbstractFile } from 'obsidian'; -import { Modal, Setting, Platform } from 'obsidian'; +import { + Modal, + Setting, + Platform, + type App, + type TFile, + type TFolder, + type TAbstractFile, +} from 'obsidian'; import type FolderNotesPlugin from '../main'; import { turnIntoFolderNote } from 'src/functions/folderNoteFunctions'; export default class ExistingFolderNoteModal extends Modal { @@ -8,7 +15,13 @@ export default class ExistingFolderNoteModal extends Modal { file: TFile; folder: TFolder; folderNote: TAbstractFile; - constructor(app: App, plugin: FolderNotesPlugin, file: TFile, folder: TFolder, folderNote: TAbstractFile) { + constructor( + app: App, + plugin: FolderNotesPlugin, + file: TFile, + folder: TFolder, + folderNote: TAbstractFile, + ) { super(app); this.plugin = plugin; this.app = app; @@ -16,18 +29,22 @@ export default class ExistingFolderNoteModal extends Modal { this.folder = folder; this.folderNote = folderNote; } - onOpen() { + onOpen(): void { const { contentEl } = this; contentEl.createEl('h2', { text: 'A folder note for this folder already exists' }); const setting = new Setting(contentEl); + // eslint-disable-next-line max-len setting.infoEl.createEl('p', { text: 'Are you sure you want to turn the note into a folder note and rename the existing folder note?' }); setting.infoEl.parentElement?.classList.add('fn-delete-confirmation-modal'); // Create a container for the buttons and the checkbox + // eslint-disable-next-line max-len const buttonContainer = setting.infoEl.createEl('div', { cls: 'fn-delete-confirmation-modal-buttons' }); if (Platform.isMobileApp) { - const confirmButton = buttonContainer.createEl('button', { text: 'Rename and don\'t ask again' }); + const confirmButton = buttonContainer.createEl('button', { + text: 'Rename and don\'t ask again', + }); confirmButton.classList.add('mod-warning', 'fn-confirmation-modal-button'); confirmButton.addEventListener('click', async () => { this.plugin.settings.showRenameConfirmation = false; @@ -64,7 +81,7 @@ export default class ExistingFolderNoteModal extends Modal { }); } - onClose() { + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/FolderName.ts b/src/modals/FolderName.ts index bdef456..b50cf57 100644 --- a/src/modals/FolderName.ts +++ b/src/modals/FolderName.ts @@ -1,5 +1,4 @@ -import type { App, TFolder } from 'obsidian'; -import { Modal, Setting } from 'obsidian'; +import { Modal, Setting, type App, type TFolder } from 'obsidian'; import type FolderNotesPlugin from '../main'; export default class FolderNameModal extends Modal { plugin: FolderNotesPlugin; @@ -11,7 +10,8 @@ export default class FolderNameModal extends Modal { this.app = app; this.folder = folder; } - onOpen() { + + onOpen(): void { const { contentEl } = this; // close when user presses enter contentEl.addEventListener('keydown', (e) => { @@ -27,14 +27,25 @@ export default class FolderNameModal extends Modal { .setValue(this.folder.name.replace(this.plugin.settings.folderNoteType, '')) .onChange(async (value) => { if (value.trim() !== '') { - if (!this.app.vault.getAbstractFileByPath(this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + value.trim())) { - this.plugin.app.fileManager.renameFile(this.folder, this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + value.trim()); + const parentPath = this.folder.path.slice( + 0, + this.folder.path.lastIndexOf('/') + 1, + ); + const newFolderPath = parentPath + value.trim(); + if ( + !this.app.vault.getAbstractFileByPath(newFolderPath) + ) { + this.plugin.app.fileManager.renameFile( + this.folder, + newFolderPath, + ); } } }), ); } - onClose() { + + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/modals/NewFolderName.ts b/src/modals/NewFolderName.ts index 64fa454..8ecb63f 100644 --- a/src/modals/NewFolderName.ts +++ b/src/modals/NewFolderName.ts @@ -1,5 +1,4 @@ -import type { App, TFolder } from 'obsidian'; -import { Modal } from 'obsidian'; +import { Modal, type App, type TFolder } from 'obsidian'; import type FolderNotesPlugin from '../main'; export default class NewFolderNameModal extends Modal { plugin: FolderNotesPlugin; @@ -11,7 +10,8 @@ export default class NewFolderNameModal extends Modal { this.app = app; this.folder = folder; } - onOpen() { + + onOpen(): void { const { contentEl } = this; contentEl.addEventListener('keydown', (e) => { @@ -37,7 +37,7 @@ export default class NewFolderNameModal extends Modal { }, }); - textarea.addEventListener('focus', function() { + textarea.addEventListener('focus', function () { this.select(); }); @@ -50,23 +50,32 @@ export default class NewFolderNameModal extends Modal { this.close(); }); - const cancelButton = buttonContainer.createEl('button', { text: 'Cancel', cls: 'mod-cancel' }); + const cancelButton = buttonContainer.createEl('button', { + text: 'Cancel', + cls: 'mod-cancel', + }); cancelButton.addEventListener('click', () => { this.close(); }); } - onClose() { + + onClose(): void { const { contentEl } = this; contentEl.empty(); } - saveFolderName() { + saveFolderName(): void { const textarea = this.contentEl.querySelector('textarea'); if (textarea) { const newName = textarea.value.trim(); if (newName.trim() !== '') { - if (!this.app.vault.getAbstractFileByPath(this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + newName.trim())) { - this.plugin.app.fileManager.renameFile(this.folder, this.folder.path.slice(0, this.folder.path.lastIndexOf('/') + 1) + newName.trim()); + const folderBasePath = this.folder.path.slice( + 0, + this.folder.path.lastIndexOf('/') + 1, + ); + const newFolderPath = folderBasePath + newName.trim(); + if (!this.app.vault.getAbstractFileByPath(newFolderPath)) { + this.plugin.app.fileManager.renameFile(this.folder, newFolderPath); } } } diff --git a/src/settings/ExcludedFoldersSettings.ts b/src/settings/ExcludedFoldersSettings.ts index e4126fb..b8b9345 100644 --- a/src/settings/ExcludedFoldersSettings.ts +++ b/src/settings/ExcludedFoldersSettings.ts @@ -1,4 +1,7 @@ -import { addExcludeFolderListItem, addExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; +import { + addExcludeFolderListItem, + addExcludedFolder, +} from 'src/ExcludeFolders/functions/folderFunctions'; import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder'; import { addExcludePatternListItem } from 'src/ExcludeFolders/functions/patternFunctions'; import { Setting } from 'obsidian'; @@ -8,7 +11,7 @@ import PatternSettings from 'src/ExcludeFolders/modals/PatternSettings'; import WhitelistedFoldersSettings from 'src/ExcludeFolders/modals/WhitelistedFoldersSettings'; // import ExcludedFoldersWhitelist from 'src/ExcludeFolders/modals/WhitelistModal'; -export async function renderExcludeFolders(settingsTab: SettingsTab) { +export async function renderExcludeFolders(settingsTab: SettingsTab): Promise { const containerEl = settingsTab.settingsPage; const manageExcluded = new Setting(containerEl) .setHeading() @@ -25,9 +28,12 @@ export async function renderExcludeFolders(settingsTab: SettingsTab) { 'Use * after the folder name to exclude folders that start with the folder name.', ); manageExcluded.setDesc(desc3); + // eslint-disable-next-line max-len manageExcluded.infoEl.appendText('The regexes and wildcards are only for the folder name, not the path.'); manageExcluded.infoEl.createEl('br'); + // eslint-disable-next-line max-len manageExcluded.infoEl.appendText('If you want to switch to a folder path delete the pattern first.'); + // eslint-disable-next-line max-len manageExcluded.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; @@ -48,7 +54,11 @@ export async function renderExcludeFolders(settingsTab: SettingsTab) { cb.setButtonText('Manage'); cb.setCta(); cb.onClick(async () => { - new ExcludedFolderSettings(settingsTab.app, settingsTab.plugin, settingsTab.plugin.settings.excludeFolderDefaultSettings).open(); + new ExcludedFolderSettings( + settingsTab.app, + settingsTab.plugin, + settingsTab.plugin.settings.excludeFolderDefaultSettings, + ).open(); }); }); @@ -58,7 +68,11 @@ export async function renderExcludeFolders(settingsTab: SettingsTab) { cb.setButtonText('Manage'); cb.setCta(); cb.onClick(async () => { - new PatternSettings(settingsTab.app, settingsTab.plugin, settingsTab.plugin.settings.excludePatternDefaultSettings).open(); + new PatternSettings( + settingsTab.app, + settingsTab.plugin, + settingsTab.plugin.settings.excludePatternDefaultSettings, + ).open(); }); }); @@ -71,18 +85,29 @@ export async function renderExcludeFolders(settingsTab: SettingsTab) { cb.setClass('add-exclude-folder'); cb.setTooltip('Add excluded folder'); cb.onClick(() => { - const excludedFolder = new ExcludedFolder('', settingsTab.plugin.settings.excludeFolders.length, undefined, settingsTab.plugin); + const excludedFolder = new ExcludedFolder( + '', + settingsTab.plugin.settings.excludeFolders.length, + undefined, + settingsTab.plugin, + ); addExcludeFolderListItem(settingsTab, containerEl, excludedFolder); addExcludedFolder(settingsTab.plugin, excludedFolder); settingsTab.display(); }); }); - settingsTab.plugin.settings.excludeFolders.filter((folder) => !folder.hideInSettings).sort((a, b) => a.position - b.position).forEach((excludedFolder) => { - if (excludedFolder.string?.trim() !== '' && excludedFolder.path?.trim() === '') { - addExcludePatternListItem(settingsTab, containerEl, excludedFolder); - } else { - addExcludeFolderListItem(settingsTab, containerEl, excludedFolder); - } - }); + settingsTab.plugin.settings.excludeFolders + .filter((folder) => !folder.hideInSettings) + .sort((a, b) => a.position - b.position) + .forEach((excludedFolder) => { + if ( + excludedFolder.string?.trim() !== '' && + excludedFolder.path?.trim() === '' + ) { + addExcludePatternListItem(settingsTab, containerEl, excludedFolder); + } else { + addExcludeFolderListItem(settingsTab, containerEl, excludedFolder); + } + }); } diff --git a/src/settings/FileExplorerSettings.ts b/src/settings/FileExplorerSettings.ts index 0d79841..c69e620 100644 --- a/src/settings/FileExplorerSettings.ts +++ b/src/settings/FileExplorerSettings.ts @@ -1,6 +1,7 @@ +/* eslint-disable max-len */ import { Setting } from 'obsidian'; import type { SettingsTab } from './SettingsTab'; -export async function renderFileExplorer(settingsTab: SettingsTab) { +export async function renderFileExplorer(settingsTab: SettingsTab): Promise { const containerEl = settingsTab.settingsPage; new Setting(containerEl) @@ -34,7 +35,8 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { ); setting2.infoEl.appendText('Requires a restart to take effect'); - setting2.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; + const setting2AccentColor = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; + setting2.infoEl.style.color = setting2AccentColor; new Setting(containerEl) .setName('Open folder notes by only clicking directly on the folder name') @@ -65,7 +67,8 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { }), ); disableSetting.infoEl.appendText('Requires a restart to take effect'); - disableSetting.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; + const accentColor = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; + disableSetting.infoEl.style.color = accentColor; new Setting(containerEl) .setName('Use submenus') @@ -91,7 +94,15 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { settingsTab.plugin.settings.frontMatterTitle.explorer = value; await settingsTab.plugin.saveSettings(); settingsTab.plugin.app.vault.getFiles().forEach((file) => { - settingsTab.plugin.fmtpHandler?.fmptUpdateFileName({ id: '', result: false, path: file.path, pathOnly: false }, false); + settingsTab.plugin.fmtpHandler?.fmptUpdateFileName( + { + id: '', + result: false, + path: file.path, + pathOnly: false, + }, + false, + ); }); }), ); diff --git a/src/settings/FolderOverviewSettings.ts b/src/settings/FolderOverviewSettings.ts index 310fb1e..de253e3 100644 --- a/src/settings/FolderOverviewSettings.ts +++ b/src/settings/FolderOverviewSettings.ts @@ -2,7 +2,7 @@ import { Setting } from 'obsidian'; import type { SettingsTab } from './SettingsTab'; import { createOverviewSettings } from 'src/obsidian-folder-overview/src/settings'; -export async function renderFolderOverview(settingsTab: SettingsTab) { +export async function renderFolderOverview(settingsTab: SettingsTab): Promise { const { plugin } = settingsTab; const defaultOverviewSettings = plugin.settings.defaultOverview; const containerEl = settingsTab.settingsPage; @@ -10,6 +10,7 @@ export async function renderFolderOverview(settingsTab: SettingsTab) { containerEl.createEl('h3', { text: 'Global settings' }); new Setting(containerEl) .setName('Auto-update links without opening the overview') + // eslint-disable-next-line max-len .setDesc('If enabled, the links that appear in the graph view will be updated even when you don\'t have the overview open somewhere.') .addToggle((toggle) => toggle @@ -35,5 +36,15 @@ export async function renderFolderOverview(settingsTab: SettingsTab) { span.setAttr('style', `color: ${accentColor};`); pEl.appendChild(span); - createOverviewSettings(containerEl, defaultOverviewSettings, plugin, plugin.settings.defaultOverview, settingsTab.display, undefined, undefined, undefined, settingsTab); + createOverviewSettings( + containerEl, + defaultOverviewSettings, + plugin, + plugin.settings.defaultOverview, + settingsTab.display, + undefined, + undefined, + undefined, + settingsTab, + ); } diff --git a/src/settings/GeneralSettings.ts b/src/settings/GeneralSettings.ts index 3a15b8e..27b026a 100644 --- a/src/settings/GeneralSettings.ts +++ b/src/settings/GeneralSettings.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ import { Setting, Platform } from 'obsidian'; import type { SettingsTab } from './SettingsTab'; import { ListComponent } from '../functions/ListComponent'; @@ -11,7 +12,8 @@ import RenameFolderNotesModal from './modals/RenameFns'; let debounceTimer: NodeJS.Timeout; -export async function renderGeneral(settingsTab: SettingsTab) { +// eslint-disable-next-line complexity +export async function renderGeneral(settingsTab: SettingsTab): Promise { const containerEl = settingsTab.settingsPage; const nameSetting = new Setting(containerEl) .setName('Folder note name template') @@ -25,6 +27,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { await settingsTab.plugin.saveSettings(); clearTimeout(debounceTimer); + const FOLDER_NOTE_NAME_DEBOUNCE_MS = 2000; debounceTimer = setTimeout(() => { if (!value.includes('{{folder_name}}')) { if (!settingsTab.showFolderNameInTabTitleSetting) { @@ -37,7 +40,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { settingsTab.showFolderNameInTabTitleSetting = false; } } - }, 2000); + }, FOLDER_NOTE_NAME_DEBOUNCE_MS); }), ) .addButton((button) => @@ -91,13 +94,24 @@ export async function renderGeneral(settingsTab: SettingsTab) { } }); - if (!settingsTab.plugin.settings.supportedFileTypes.includes(settingsTab.plugin.settings.folderNoteType.replace('.', '')) && settingsTab.plugin.settings.folderNoteType !== '.ask') { + if ( + !settingsTab.plugin.settings.supportedFileTypes.includes( + settingsTab.plugin.settings.folderNoteType.replace('.', ''), + ) && + settingsTab.plugin.settings.folderNoteType !== '.ask' + ) { settingsTab.plugin.settings.folderNoteType = '.md'; settingsTab.plugin.saveSettings(); } - let defaultType = settingsTab.plugin.settings.folderNoteType.startsWith('.') ? settingsTab.plugin.settings.folderNoteType : '.' + settingsTab.plugin.settings.folderNoteType; - if (!settingsTab.plugin.settings.supportedFileTypes.includes(defaultType.replace('.', ''))) { + let defaultType = settingsTab.plugin.settings.folderNoteType.startsWith('.') + ? settingsTab.plugin.settings.folderNoteType + : '.' + settingsTab.plugin.settings.folderNoteType; + if ( + !settingsTab.plugin.settings.supportedFileTypes.includes( + defaultType.replace('.', ''), + ) + ) { defaultType = '.ask'; settingsTab.plugin.settings.folderNoteType = defaultType; } @@ -118,14 +132,22 @@ export async function renderGeneral(settingsTab: SettingsTab) { 'Specify which file types are allowed as folder notes. Applies to both new and existing folders. Adding many types may affect performance.', ); setting0.setDesc(desc0); - const list = new ListComponent(setting0.settingEl, settingsTab.plugin.settings.supportedFileTypes || [], ['md', 'canvas']); + const list = new ListComponent( + setting0.settingEl, + settingsTab.plugin.settings.supportedFileTypes || [], + ['md', 'canvas'], + ); list.on('update', async (values: string[]) => { settingsTab.plugin.settings.supportedFileTypes = values; await settingsTab.plugin.saveSettings(); settingsTab.display(); }); - if (!settingsTab.plugin.settings.supportedFileTypes.includes('md') || !settingsTab.plugin.settings.supportedFileTypes.includes('canvas') || !settingsTab.plugin.settings.supportedFileTypes.includes('excalidraw')) { + if ( + !settingsTab.plugin.settings.supportedFileTypes.includes('md') || + !settingsTab.plugin.settings.supportedFileTypes.includes('canvas') || + !settingsTab.plugin.settings.supportedFileTypes.includes('excalidraw') + ) { setting0.addDropdown((dropdown) => { const options = [ { value: 'md', label: 'Markdown' }, @@ -144,7 +166,12 @@ export async function renderGeneral(settingsTab: SettingsTab) { dropdown.setValue('+'); dropdown.onChange(async (value) => { if (value === 'custom') { - return new AddSupportedFileModal(settingsTab.app, settingsTab.plugin, settingsTab, list as ListComponent).open(); + return new AddSupportedFileModal( + settingsTab.app, + settingsTab.plugin, + settingsTab, + list as ListComponent, + ).open(); } await list.addValue(value.toLowerCase()); settingsTab.display(); @@ -157,7 +184,12 @@ export async function renderGeneral(settingsTab: SettingsTab) { .setButtonText('Add custom file type') .setCta() .onClick(async () => { - new AddSupportedFileModal(settingsTab.app, settingsTab.plugin, settingsTab, list as ListComponent).open(); + new AddSupportedFileModal( + settingsTab.app, + settingsTab.plugin, + settingsTab, + list as ListComponent, + ).open(); }), ); } @@ -169,7 +201,11 @@ export async function renderGeneral(settingsTab: SettingsTab) { .addSearch((cb) => { new TemplateSuggest(cb.inputEl, settingsTab.plugin); cb.setPlaceholder('Template path'); - cb.setValue(settingsTab.plugin.app.vault.getAbstractFileByPath(settingsTab.plugin.settings.templatePath)?.name.replace('.md', '') || ''); + const templateFile = settingsTab.plugin.app.vault.getAbstractFileByPath( + settingsTab.plugin.settings.templatePath, + ); + const templateName = templateFile?.name.replace('.md', '') || ''; + cb.setValue(templateName); cb.onChange(async (value) => { if (value.trim() === '') { settingsTab.plugin.settings.templatePath = ''; @@ -464,16 +500,26 @@ export async function renderGeneral(settingsTab: SettingsTab) { settingsTab.plugin.settings.frontMatterTitle.enabled = value; await settingsTab.plugin.saveSettings(); if (value) { - settingsTab.plugin.fmtpHandler = new FrontMatterTitlePluginHandler(settingsTab.plugin); + settingsTab.plugin.fmtpHandler = + new FrontMatterTitlePluginHandler(settingsTab.plugin); } else { if (settingsTab.plugin.fmtpHandler) { settingsTab.plugin.updateAllBreadcrumbs(true); } settingsTab.plugin.app.vault.getFiles().forEach((file) => { - settingsTab.plugin.fmtpHandler?.fmptUpdateFileName({ id: '', result: false, path: file.path, pathOnly: false }, false); + settingsTab.plugin.fmtpHandler?.fmptUpdateFileName( + { + id: '', + result: false, + path: file.path, + pathOnly: false, + }, + false, + ); }); settingsTab.plugin.fmtpHandler?.deleteEvent(); - settingsTab.plugin.fmtpHandler = new FrontMatterTitlePluginHandler(settingsTab.plugin); + settingsTab.plugin.fmtpHandler = + new FrontMatterTitlePluginHandler(settingsTab.plugin); } settingsTab.display(); }), diff --git a/src/settings/PathSettings.ts b/src/settings/PathSettings.ts index 3ea8be2..034b1ed 100644 --- a/src/settings/PathSettings.ts +++ b/src/settings/PathSettings.ts @@ -1,6 +1,7 @@ +/* eslint-disable max-len */ import { Setting } from 'obsidian'; import type { SettingsTab } from './SettingsTab'; -export async function renderPath(settingsTab: SettingsTab) { +export async function renderPath(settingsTab: SettingsTab): Promise { const containerEl = settingsTab.settingsPage; new Setting(containerEl) .setName('Open folder note through path') diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index e6723c8..2eeb650 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -1,5 +1,7 @@ -import type { App, MarkdownPostProcessorContext } from 'obsidian'; -import { Notice, PluginSettingTab, TFile, TFolder } from 'obsidian'; +import { + Notice, PluginSettingTab, TFile, + TFolder, type App, type MarkdownPostProcessorContext, +} from 'obsidian'; import type FolderNotesPlugin from '../main'; import type { ExcludePattern } from 'src/ExcludeFolders/ExcludePattern'; import type { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder'; @@ -236,7 +238,8 @@ export class SettingsTab extends PluginSettingTab { id: 'path', }, }; - renderSettingsPage(tabId: string) { + + renderSettingsPage(tabId: string): void { this.settingsPage.empty(); switch (tabId.toLocaleLowerCase()) { case this.TABS.GENERAL.id: @@ -257,7 +260,17 @@ export class SettingsTab extends PluginSettingTab { } } - display(contentEl?: HTMLElement, yaml?: defaultOverviewSettings, plugin?: FolderNotesPlugin, defaultSettings?: boolean, display?: CallableFunction, el?: HTMLElement, ctx?: MarkdownPostProcessorContext, file?: TFile | null, settingsTab?: this) { + display( + contentEl?: HTMLElement, + yaml?: defaultOverviewSettings, + plugin?: FolderNotesPlugin, + defaultSettings?: boolean, + display?: CallableFunction, + el?: HTMLElement, + ctx?: MarkdownPostProcessorContext, + file?: TFile | null, + settingsTab?: this, + ): void { plugin = this?.plugin ?? plugin; if (plugin) { plugin.settingsOpened = true; @@ -274,13 +287,17 @@ export class SettingsTab extends PluginSettingTab { for (const [tabId, tabInfo] of Object.entries(settingsTab.TABS)) { const tabEl = tabBar.createEl('div', { cls: 'fn-settings-tab' }); tabEl.createEl('div', { cls: 'fn-settings-tab-name', text: tabInfo.name }); - if (plugin && plugin.settings.settingsTab.toLocaleLowerCase() === tabId.toLocaleLowerCase()) { + if ( + plugin && + plugin.settings.settingsTab.toLocaleLowerCase() === + tabId.toLocaleLowerCase() + ) { tabEl.addClass('fn-settings-tab-active'); } tabEl.addEventListener('click', () => { - // @ts-ignore - for (const tabEl of tabBar.children) { - tabEl.removeClass('fn-settings-tab-active'); + // @ts-expect-error: tabBar.children may not have removeClass method, but we know it works in this context + for (const child of tabBar.children) { + child.removeClass('fn-settings-tab-active'); if (!plugin) { return; } plugin.settings.settingsTab = tabId.toLocaleLowerCase(); plugin.saveSettings(); @@ -300,23 +317,31 @@ export class SettingsTab extends PluginSettingTab { } } - renameFolderNotes() { + renameFolderNotes(): void { new Notice('Starting to update folder notes...'); const oldTemplate = this.plugin.settings.oldFolderNoteName ?? '{{folder_name}}'; for (const folder of this.app.vault.getAllLoadedFiles()) { if (folder instanceof TFolder) { - const folderNote = getFolderNote(this.plugin, folder.path, undefined, undefined, oldTemplate); + const folderNote = getFolderNote( + this.plugin, + folder.path, + undefined, + undefined, + oldTemplate, + ); if (!(folderNote instanceof TFile)) { continue; } const folderName = extractFolderName(oldTemplate, folderNote.basename) ?? ''; - const newFolderNoteName = this.plugin.settings.folderNoteName.replace('{{folder_name}}', folderName); + const newFolderNoteName = this.plugin.settings.folderNoteName + .replace('{{folder_name}}', folderName); let newPath = ''; if (this.plugin.settings.storageLocation === 'parentFolder') { if (getFolderPathFromString(folder.path).trim() === '/') { newPath = `${newFolderNoteName}.${folderNote.extension}`; } else { + // eslint-disable-next-line max-len newPath = `${folderNote.parent?.path}/${newFolderNoteName}.${folderNote.extension}`; } } else if (this.plugin.settings.storageLocation === 'insideFolder') { @@ -332,7 +357,7 @@ export class SettingsTab extends PluginSettingTab { new Notice('Finished updating folder notes'); } - switchStorageLocation(oldMethod: string) { + switchStorageLocation(oldMethod: string): void { new Notice('Starting to switch storage location...'); this.app.vault.getAllLoadedFiles().forEach((file) => { if (file instanceof TFolder) { diff --git a/src/settings/modals/BackupWarning.ts b/src/settings/modals/BackupWarning.ts index a0deb27..f6c8694 100644 --- a/src/settings/modals/BackupWarning.ts +++ b/src/settings/modals/BackupWarning.ts @@ -5,10 +5,16 @@ export default class BackupWarningModal extends Modal { plugin: FolderNotesPlugin; title: string; desc: string; - callback: (...args: any[]) => void; - args: any[]; + callback: (...args: unknown[]) => void; + args: unknown[]; - constructor(plugin: FolderNotesPlugin, title: string, description: string, callback: (...args: any[]) => void, args: any[] = []) { + constructor( + plugin: FolderNotesPlugin, + title: string, + description: string, + callback: (...args: unknown[]) => void, + args: unknown[] = [], + ) { super(plugin.app); this.plugin = plugin; this.title = title; @@ -17,7 +23,7 @@ export default class BackupWarningModal extends Modal { this.desc = description; } - onOpen() { + onOpen(): void { this.modalEl.addClass('fn-backup-warning-modal'); const { contentEl } = this; @@ -25,8 +31,7 @@ export default class BackupWarningModal extends Modal { contentEl.createEl('p', { text: this.desc }); - this.insertCustomHtml(); - + // eslint-disable-next-line max-len contentEl.createEl('p', { text: 'Make sure to backup your vault before using this feature.' }).style.color = '#fb464c'; const buttonContainer = contentEl.createDiv({ cls: 'fn-modal-button-container' }); @@ -45,11 +50,7 @@ export default class BackupWarningModal extends Modal { }); } - insertCustomHtml(): void { - - } - - onClose() { + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/settings/modals/CreateFnForEveryFolder.ts b/src/settings/modals/CreateFnForEveryFolder.ts index 0380886..048664e 100644 --- a/src/settings/modals/CreateFnForEveryFolder.ts +++ b/src/settings/modals/CreateFnForEveryFolder.ts @@ -1,5 +1,4 @@ -import type { App, ButtonComponent } from 'obsidian'; -import { Modal, Setting, TFolder, Notice } from 'obsidian'; +import { Modal, Setting, TFolder, Notice, type App, type ButtonComponent } from 'obsidian'; import type FolderNotesPlugin from '../../main'; import { createFolderNote, getFolderNote } from 'src/functions/folderNoteFunctions'; import { getTemplatePlugins } from 'src/template'; @@ -15,7 +14,8 @@ export default class ConfirmationModal extends Modal { this.app = app; this.extension = plugin.settings.folderNoteType; } - onOpen() { + + onOpen(): void { this.modalEl.addClass('fn-confirmation-modal'); let templateFolderPath: string; const { templateFolder, templaterPlugin } = getTemplatePlugins(this.plugin.app); @@ -23,19 +23,29 @@ export default class ConfirmationModal extends Modal { templateFolderPath = ''; } if (templaterPlugin) { - templateFolderPath = templaterPlugin.plugin?.settings?.templates_folder as string; - } else { + templateFolderPath = ( + templaterPlugin as unknown as { + plugin?: { settings?: { templates_folder?: string } } + } + ).plugin?.settings?.templates_folder as string; + } else if (templateFolder) { templateFolderPath = templateFolder; } const { contentEl } = this; contentEl.createEl('h2', { text: 'Create folder note for every folder' }); const setting = new Setting(contentEl); + // eslint-disable-next-line max-len setting.infoEl.createEl('p', { text: 'Make sure to backup your vault before using this feature.' }).style.color = '#fb464c'; + // eslint-disable-next-line max-len setting.infoEl.createEl('p', { text: 'This feature will create a folder note for every folder in your vault.' }); + // eslint-disable-next-line max-len setting.infoEl.createEl('p', { text: 'Every folder that already has a folder note will be ignored.' }); setting.infoEl.createEl('p', { text: 'Every excluded folder will be ignored.' }); - if (!this.plugin.settings.templatePath || this.plugin.settings.templatePath?.trim() === '') { + if ( + !this.plugin.settings.templatePath || + this.plugin.settings.templatePath?.trim() === '' + ) { new Setting(contentEl) .setName('Folder note file extension') .setDesc('Choose the file extension for the folder notes.') @@ -56,17 +66,24 @@ export default class ConfirmationModal extends Modal { cb.setCta(); cb.buttonEl.focus(); cb.onClick(async () => { - if (this.plugin.settings.templatePath && this.plugin.settings.templatePath.trim() !== '') { + if ( + this.plugin.settings.templatePath && + this.plugin.settings.templatePath.trim() !== '' + ) { this.extension = '.' + this.plugin.settings.templatePath.split('.').pop(); } if (this.extension === '.ask') { return new Notice('Please choose a file extension'); } this.close(); - const folders = this.app.vault.getAllLoadedFiles().filter((file) => file.parent instanceof TFolder); + const folders = this.app.vault + .getAllLoadedFiles() + .filter((file) => file.parent instanceof TFolder); for (const folder of folders) { if (folder instanceof TFolder) { - const excludedFolder = getExcludedFolder(this.plugin, folder.path, true); + const excludedFolder = getExcludedFolder( + this.plugin, folder.path, true, + ); if (excludedFolder) continue; if (folder.path === templateFolderPath) continue; const folderNote = getFolderNote(this.plugin, folder.path); @@ -83,7 +100,8 @@ export default class ConfirmationModal extends Modal { }); }); } - onClose() { + + onClose(): void { const { contentEl } = this; contentEl.empty(); } diff --git a/src/settings/modals/RenameFns.ts b/src/settings/modals/RenameFns.ts index 56cd925..b2183c6 100644 --- a/src/settings/modals/RenameFns.ts +++ b/src/settings/modals/RenameFns.ts @@ -3,7 +3,13 @@ import type FolderNotesPlugin from 'src/main'; import { Setting } from 'obsidian'; export default class RenameFolderNotesModal extends BackupWarningModal { - constructor(plugin: FolderNotesPlugin, title: string, description: string, callback: (...args: any[]) => void, args: any[] = []) { + constructor( + plugin: FolderNotesPlugin, + title: string, + description: string, + callback: (...args: unknown[]) => void, + args: unknown[] = [], + ) { super(plugin, title, description, callback, args); } @@ -11,6 +17,7 @@ export default class RenameFolderNotesModal extends BackupWarningModal { const { contentEl } = this; new Setting(contentEl) .setName('Old Folder Note Name') + // eslint-disable-next-line max-len .setDesc('Every folder note that matches this name will be renamed to the new folder note name.') .addText((text) => text .setPlaceholder('Enter the old folder note name') @@ -22,6 +29,7 @@ export default class RenameFolderNotesModal extends BackupWarningModal { new Setting(contentEl) .setName('New Folder Note Name') + // eslint-disable-next-line max-len .setDesc('Every folder note that matches the old folder note name will be renamed to this name.') .addText((text) => text .setPlaceholder('Enter the new folder note name') diff --git a/src/suggesters/FolderSuggester.ts b/src/suggesters/FolderSuggester.ts index e3d07b6..ce4d34e 100644 --- a/src/suggesters/FolderSuggester.ts +++ b/src/suggesters/FolderSuggester.ts @@ -1,7 +1,6 @@ // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes and https://github.com/SilentVoid13/Templater -import type { TAbstractFile } from 'obsidian'; -import { TFolder } from 'obsidian'; +import { TFolder, type TAbstractFile } from 'obsidian'; import { TextInputSuggest } from './Suggest'; import type FolderNotesPlugin from '../main'; export enum FileSuggestMode { @@ -36,13 +35,18 @@ export class FolderSuggest extends TextInputSuggest { if (this.folder) { files = this.folder.children; } else { - files = this.plugin.app.vault.getAllLoadedFiles().slice(0,100); + const MAX_FILE_SUGGESTIONS = 100; + files = this.plugin.app.vault.getAllLoadedFiles().slice(0, MAX_FILE_SUGGESTIONS); } files.forEach((folder: TAbstractFile) => { if ( folder instanceof TFolder && - folder.path.toLowerCase().contains(lower_input_str) && - (!this.plugin.settings.excludeFolders.find((f) => f.path === folder.path) || this.whitelistSuggester) + folder.path.toLowerCase().contains(lower_input_str) && + ( + !this.plugin.settings.excludeFolders.find( + (f) => f.path === folder.path, + ) || this.whitelistSuggester + ) ) { folders.push(folder); } diff --git a/src/suggesters/Suggest.ts b/src/suggesters/Suggest.ts index 4a3fb02..9f2822b 100644 --- a/src/suggesters/Suggest.ts +++ b/src/suggesters/Suggest.ts @@ -1,10 +1,7 @@ // Credits go to Liam's Periodic Notes Plugin: https://github.com/liamcain/obsidian-periodic-notes and https://github.com/SilentVoid13/Templater - -import type { ISuggestOwner } from 'obsidian'; -import { Scope } from 'obsidian'; -import type { Instance as PopperInstance } from '@popperjs/core'; -import { createPopper } from '@popperjs/core'; +import { Scope, type ISuggestOwner } from 'obsidian'; +import { createPopper, type Instance as PopperInstance } from '@popperjs/core'; import type FolderNotesPlugin from 'src/main'; const wrapAround = (value: number, size: number): number => { @@ -73,7 +70,7 @@ class Suggest { this.setSelectedItem(item, false); } - setSuggestions(values: T[]) { + setSuggestions(values: T[]): void { this.containerEl.empty(); const suggestionEls: HTMLDivElement[] = []; @@ -88,14 +85,14 @@ class Suggest { this.setSelectedItem(0, false); } - useSelectedItem(event: MouseEvent | KeyboardEvent) { + useSelectedItem(event: MouseEvent | KeyboardEvent): void { const currentValue = this.values[this.selectedItem]; if (currentValue) { this.owner.selectSuggestion(currentValue, event); } } - setSelectedItem(selectedIndex: number, scrollIntoView: boolean) { + setSelectedItem(selectedIndex: number, scrollIntoView: boolean): void { const normalizedIndex = wrapAround( selectedIndex, this.suggestions.length, @@ -157,7 +154,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { if (suggestions.length > 0) { this.suggest.setSuggestions(suggestions); - // @ts-ignore + // @ts-expect-error App may not exist this.open(app.dom.appContainerEl, this.inputEl); } else { this.close(); @@ -175,7 +172,7 @@ export abstract class TextInputSuggest implements ISuggestOwner { { name: 'sameWidth', enabled: true, - fn: ({ state, instance }) => { + fn: ({ state, instance }): void => { // 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 diff --git a/src/suggesters/TemplateSuggester.ts b/src/suggesters/TemplateSuggester.ts index 0e26932..60b6f83 100644 --- a/src/suggesters/TemplateSuggester.ts +++ b/src/suggesters/TemplateSuggester.ts @@ -1,5 +1,4 @@ -import type { TAbstractFile } from 'obsidian'; -import { TFile, TFolder, Vault, AbstractInputSuggest } from 'obsidian'; +import { TFile, TFolder, Vault, AbstractInputSuggest, type TAbstractFile } from 'obsidian'; import type FolderNotesPlugin from '../main'; import { getTemplatePlugins } from 'src/template'; export enum FileSuggestMode { @@ -39,12 +38,21 @@ export class TemplateSuggest extends AbstractInputSuggest { let folder: TFolder | TAbstractFile | null = null; if (templaterPlugin) { folder = this.plugin.app.vault.getAbstractFileByPath( - templaterPlugin.plugin?.settings?.templates_folder as string, + (templaterPlugin as unknown as { + plugin?: { settings?: { templates_folder?: string } } + }).plugin?.settings?.templates_folder as string, ); if (!(folder instanceof TFolder)) { - return [{ path: '', name: 'You need to set the Templates folder in the Templater settings first.' } as TFile]; + return [ + { + path: '', + name: + // eslint-disable-next-line max-len + 'You need to set the Templates folder in the Templater settings first.', + } as TFile, + ]; } - } else { + } else if (templateFolder) { folder = this.plugin.app.vault.getAbstractFileByPath(templateFolder) as TFolder; } diff --git a/src/template.ts b/src/template.ts index 09753cb..48240fd 100644 --- a/src/template.ts +++ b/src/template.ts @@ -1,12 +1,41 @@ import { TFile, WorkspaceLeaf, type App } from 'obsidian'; import type FolderNotesPlugin from './main'; +interface TemplatesPlugin { + enabled: boolean; + instance: { + options: { + folder: string; + }; + insertTemplate: (templateFile: TFile) => Promise; + }; +} + +interface TemplaterPlugin { + settings?: { + empty_file_template?: string; + template_folder?: string; + }; + templater?: { + write_template_to_file: (templateFile: TFile, targetFile: TFile) => Promise; + }; +} + +interface TemplatePluginReturn { + templatesPlugin: TemplatesPlugin | null; + templatesEnabled: boolean; + templaterPlugin: TemplaterPlugin['templater'] | null; + templaterEnabled: boolean; + templaterEmptyFileTemplate?: string; + templateFolder?: string; +} + export async function applyTemplate( plugin: FolderNotesPlugin, file: TFile, leaf?: WorkspaceLeaf | null, templatePath?: string, -) { +): Promise { const fileContent = await plugin.app.vault.read(file).catch((err) => { console.error(`Error reading file ${file.path}:`, err); }); @@ -25,14 +54,15 @@ export async function applyTemplate( templaterPlugin, } = getTemplatePlugins(plugin.app); const templateContent = await plugin.app.vault.read(templateFile); + // eslint-disable-next-line max-len if (templateContent.includes('==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠==')) { return; } // Prioritize Templater if both plugins are enabled - if (templaterEnabled) { + if (templaterEnabled && templaterPlugin) { return await templaterPlugin.write_template_to_file(templateFile, file); - } else if (templatesEnabled) { + } else if (templatesEnabled && templatesPlugin) { if (leaf instanceof WorkspaceLeaf) { await leaf.openFile(file); } @@ -47,23 +77,37 @@ export async function applyTemplate( } } -export function getTemplatePlugins(app: App) { - const templatesPlugin = (app as any).internalPlugins.plugins.templates; - const templatesEnabled = templatesPlugin.enabled; - const templaterPlugin = (app as any).plugins.plugins['templater-obsidian']; - const templaterEnabled = (app as any).plugins.enabledPlugins.has('templater-obsidian'); +export function getTemplatePlugins(app: App): TemplatePluginReturn { + const appAsUnknown = app as unknown as { + internalPlugins: { + plugins: { + templates: TemplatesPlugin; + }; + }; + plugins: { + plugins: { + 'templater-obsidian': TemplaterPlugin; + }; + enabledPlugins: Set; + }; + }; + + const templatesPlugin = appAsUnknown.internalPlugins.plugins.templates; + const templatesEnabled = templatesPlugin?.enabled ?? false; + const templaterPlugin = appAsUnknown.plugins.plugins['templater-obsidian']; + const templaterEnabled = appAsUnknown.plugins.enabledPlugins.has('templater-obsidian'); const templaterEmptyFileTemplate = templaterPlugin && templaterPlugin.settings?.empty_file_template; const templateFolder = templatesEnabled ? templatesPlugin.instance.options.folder - : templaterPlugin?.settings.template_folder; + : templaterPlugin?.settings?.template_folder; return { - templatesPlugin, + templatesPlugin: templatesPlugin || null, templatesEnabled, - templaterPlugin: templaterPlugin?.templater, + templaterPlugin: templaterPlugin?.templater || null, templaterEnabled, templaterEmptyFileTemplate, templateFolder,