diff --git a/docs/docs/Folder overview.md b/docs/docs/Folder overview.md index ef38beb..18d5911 100644 --- a/docs/docs/Folder overview.md +++ b/docs/docs/Folder overview.md @@ -29,4 +29,3 @@ Just create the folder overview as normal in a template and then change the sett ## What's the id in the code block for? They're there to differentiate the overviews from each other so that you can use "Edit folder overview" command to edit the overviews from one file (there can be multiple overviews in one file). It's especially useful on mobile because the button to edit the code block on mobile is quite small. - diff --git a/src/Commands.ts b/src/Commands.ts index b0f9e2a..2155075 100644 --- a/src/Commands.ts +++ b/src/Commands.ts @@ -1,10 +1,10 @@ -import { App, TFolder, Menu, TAbstractFile, Notice, TFile, Editor, MarkdownView, Platform, stringifyYaml } from 'obsidian'; +import { App, TFolder, Menu, TAbstractFile, Notice, TFile, Editor, MarkdownView, Platform } from 'obsidian'; import FolderNotesPlugin from './main'; import { getFolderNote, createFolderNote, deleteFolderNote, turnIntoFolderNote, openFolderNote, extractFolderName, detachFolderNote } from './functions/folderNoteFunctions'; import { ExcludedFolder } from './ExcludeFolders/ExcludeFolder'; import { getFolderPathFromString, getFileExplorerActiveFolder } from './functions/utils'; -import { addExcludedFolder, deleteExcludedFolder, getDetachedFolder, getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; -import { applyCSSClassesToFolder } from './functions/styleFunctions'; +import { deleteExcludedFolder, getDetachedFolder, getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; +import { hideFolderNoteInFileExplorer, showFolderNoteInFileExplorer } from './functions/styleFunctions'; @@ -23,20 +23,24 @@ export class Commands { regularCommands() { this.plugin.addCommand({ id: 'turn-into-folder-note', - name: 'Make current active note a folder note for the folder of the active note', - callback: () => { + name: 'Use this file as the folder note for its parent folder', + checkCallback: (checking: boolean) => { const file = this.app.workspace.getActiveFile(); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; const folder = file.parent; - if (!(folder instanceof TFolder)) return; + if (!folder || !(folder instanceof TFolder)) return false; + // Only show if file is NOT in the root folder + if (folder.path === '' || folder.path === '/') return false; const folderNote = getFolderNote(this.plugin, folder.path); + if (folderNote instanceof TFile && folderNote === file) return false; + if (checking) return true; turnIntoFolderNote(this.plugin, file, folder, folderNote); }, }); this.plugin.addCommand({ id: 'create-folder-note', - name: 'Create folder note with a new folder for the active note in the current folder', + name: 'Make a folder with this file as its folder note', callback: async () => { const file = this.app.workspace.getActiveFile(); if (!(file instanceof TFile)) return; @@ -61,12 +65,14 @@ export class Commands { this.plugin.addCommand({ id: 'create-folder-note-for-current-folder', - name: 'Create markdown folder note for current folder of active note', - callback: () => { + name: 'Create markdown folder note for this folder', + checkCallback: (checking) => { const file = this.app.workspace.getActiveFile(); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; const folder = file.parent; - if (!(folder instanceof TFolder)) return; + if (!(folder instanceof TFolder)) return false; + if (folder.path === '' || folder.path === '/') return false; + if (checking) return true; createFolderNote(this.plugin, folder.path, true, '.md', false); }, }); @@ -75,12 +81,14 @@ export class Commands { if (fileType === 'md') return; this.plugin.addCommand({ id: `create-${fileType}-folder-note-for-current-folder`, - name: `Create ${fileType} folder note for current folder of active note`, - callback: () => { + name: `Create ${fileType} folder note for this folder`, + checkCallback: (checking) => { const file = this.app.workspace.getActiveFile(); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; const folder = file.parent; - if (!(folder instanceof TFolder)) return; + if (!(folder instanceof TFolder)) return false; + if (folder.path === '' || folder.path === '/') return false; + if (checking) return true; createFolderNote(this.plugin, folder.path, true, '.' + fileType, false); }, }); @@ -108,14 +116,15 @@ export class Commands { this.plugin.addCommand({ id: 'delete-folder-note-for-current-folder', - name: 'Delete folder note of current folder of active note', - callback: () => { + name: 'Delete this folder\'s linked note', + checkCallback: (checking) => { const file = this.app.workspace.getActiveFile(); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; const folder = file.parent; - if (!(folder instanceof TFolder)) return; + if (!(folder instanceof TFolder)) return false; const folderNote = getFolderNote(this.plugin, folder.path); - if (!(folderNote instanceof TFile)) return; + if (!(folderNote instanceof TFile)) return false; + if (checking) return true; deleteFolderNote(this.plugin, folderNote, true); }, }); @@ -137,14 +146,15 @@ export class Commands { }); this.plugin.addCommand({ id: 'open-folder-note-for-current-folder', - name: 'Open folder note of current folder of active note', - callback: () => { + name: 'Open this folder\'s linked note', + checkCallback: (checking) => { const file = this.app.workspace.getActiveFile(); - if (!(file instanceof TFile)) return; + if (!(file instanceof TFile)) return false; const folder = file.parent; - if (!(folder instanceof TFolder)) return; + if (!(folder instanceof TFolder)) return false; const folderNote = getFolderNote(this.plugin, folder.path); - if (!(folderNote instanceof TFile)) return; + if (!(folderNote instanceof TFile)) return false; + if (checking) return true; openFolderNote(this.plugin, folderNote); }, }); @@ -163,37 +173,10 @@ export class Commands { openFolderNote(this.plugin, folderNote); }, }); - this.plugin.addCommand({ - id: 'insert-folder-overview-fn', - name: 'Insert folder overview', - editorCheckCallback: (checking: boolean, editor: Editor) => { - const line = editor.getCursor().line; - const lineText = editor.getLine(line); - if (lineText.trim() === '' || lineText.trim() === '>') { - if (!checking) { - const json = Object.assign({}, this.plugin.settings.defaultOverview); - json.id = crypto.randomUUID(); - const yaml = stringifyYaml(json); - if (lineText.trim() === '') { - editor.replaceSelection(`\`\`\`folder-overview\n${yaml}\`\`\`\n`); - } else if (lineText.trim() === '>') { - // add > to the beginning of each line - const lines = yaml.split('\n'); - const newLines = lines.map((line) => { - return `> ${line}`; - }); - editor.replaceSelection(`\`\`\`folder-overview\n${newLines.join('\n')}\`\`\`\n`); - } - } - return true; - } - return false; - }, - }); this.plugin.addCommand({ id: 'create-folder-note-from-selected-text', - name: 'Create folder note from selected text', + name: 'Create folder note from selection', editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => { const text = editor.getSelection().trim(); const file = view.file; @@ -317,6 +300,7 @@ export class Commands { }); 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') @@ -328,9 +312,9 @@ export class Commands { }); } if (!(file instanceof TFolder)) return; - const excludedFolder = await getExcludedFolder(this.plugin, file.path, false); + const excludedFolder = getExcludedFolder(this.plugin, file.path, false); const detachedExcludedFolder = getDetachedFolder(this.plugin, file.path); - if (excludedFolder && !excludedFolder.hideNote) { + 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') @@ -365,8 +349,8 @@ export class Commands { deleteExcludedFolder(this.plugin, detachedExcludedFolder); }); }); - // return; } + if (detachedExcludedFolder) { return; } subMenu.addItem((item) => { item.setTitle('Exclude folder from folder notes') .setIcon('x-circle') @@ -414,15 +398,12 @@ export class Commands { }); if (this.plugin.settings.hideFolderNote) { - if (excludedFolder?.hideNote) { + if (excludedFolder?.showFolderNote) { subMenu.addItem((item) => { item.setTitle('Hide folder note in explorer') .setIcon('eye-off') .onClick(() => { - this.plugin.settings.excludeFolders = this.plugin.settings.excludeFolders.filter( - (folder) => (folder.path !== file.path) && folder.hideNote); - this.plugin.saveSettings(false); - applyCSSClassesToFolder(file.path, this.plugin); + hideFolderNoteInFileExplorer(file.path, this.plugin); }); }); } else { @@ -430,17 +411,7 @@ export class Commands { item.setTitle('Show folder note in explorer') .setIcon('eye') .onClick(() => { - const excludedFolder = new ExcludedFolder(file.path, this.plugin.settings.excludeFolders.length, undefined, this.plugin); - excludedFolder.hideNote = true; - excludedFolder.subFolders = false; - excludedFolder.disableSync = false; - excludedFolder.disableAutoCreate = false; - excludedFolder.disableFolderNote = false; - excludedFolder.enableCollapsing = false; - excludedFolder.excludeFromFolderOverview = false; - excludedFolder.hideInSettings = true; - addExcludedFolder(this.plugin, excludedFolder, false); - applyCSSClassesToFolder(file.path, this.plugin); + showFolderNoteInFileExplorer(file.path, this.plugin); }); }); } diff --git a/src/ExcludeFolders/ExcludeFolder.ts b/src/ExcludeFolders/ExcludeFolder.ts index 41382f7..8eda370 100644 --- a/src/ExcludeFolders/ExcludeFolder.ts +++ b/src/ExcludeFolders/ExcludeFolder.ts @@ -14,7 +14,7 @@ export class ExcludedFolder { hideInSettings: boolean; detached: boolean; detachedFilePath?: string; - hideNote: boolean; + showFolderNote: boolean; constructor(path: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) { this.type = 'folder'; this.id = id || crypto.randomUUID(); @@ -28,6 +28,6 @@ export class ExcludedFolder { this.excludeFromFolderOverview = plugin.settings.excludeFolderDefaultSettings.excludeFromFolderOverview; this.string = ''; this.hideInSettings = false; - this.hideNote = false; + this.showFolderNote = plugin.settings.excludeFolderDefaultSettings.showFolderNote; } } diff --git a/src/ExcludeFolders/ExcludePattern.ts b/src/ExcludeFolders/ExcludePattern.ts index fe2cc60..6839069 100644 --- a/src/ExcludeFolders/ExcludePattern.ts +++ b/src/ExcludeFolders/ExcludePattern.ts @@ -14,7 +14,7 @@ export class ExcludePattern { hideInSettings: boolean; detached: boolean; detachedFilePath?: string; - hideNote: boolean; + showFolderNote: boolean; constructor(pattern: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) { this.type = 'pattern'; this.id = id || crypto.randomUUID(); @@ -28,5 +28,6 @@ export class ExcludePattern { this.excludeFromFolderOverview = plugin.settings.excludePatternDefaultSettings.excludeFromFolderOverview; this.path = ''; this.hideInSettings = false; + this.showFolderNote = plugin.settings.excludePatternDefaultSettings.showFolderNote; } } diff --git a/src/ExcludeFolders/WhitelistFolder.ts b/src/ExcludeFolders/WhitelistFolder.ts index c354b77..f0b40db 100644 --- a/src/ExcludeFolders/WhitelistFolder.ts +++ b/src/ExcludeFolders/WhitelistFolder.ts @@ -8,8 +8,9 @@ export class WhitelistedFolder { enableSync: boolean; enableAutoCreate: boolean; enableFolderNote: boolean; - enableCollapsing: boolean; + disableCollapsing: boolean; showInFolderOverview: boolean; + hideInFileExplorer: boolean; position: number; hideInSettings: boolean; constructor(path: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) { diff --git a/src/ExcludeFolders/WhitelistPattern.ts b/src/ExcludeFolders/WhitelistPattern.ts index 3aeea07..43ecd45 100644 --- a/src/ExcludeFolders/WhitelistPattern.ts +++ b/src/ExcludeFolders/WhitelistPattern.ts @@ -9,8 +9,9 @@ export class WhitelistedPattern { enableSync: boolean; enableAutoCreate: boolean; enableFolderNote: boolean; - enableCollapsing: boolean; + disableCollapsing: boolean; showInFolderOverview: boolean; + hideInFileExplorer: boolean; hideInSettings: boolean; constructor(pattern: string, position: number, id: string | undefined, plugin: FolderNotesPlugin) { this.type = 'pattern'; diff --git a/src/ExcludeFolders/functions/folderFunctions.ts b/src/ExcludeFolders/functions/folderFunctions.ts index cc9e586..1c48af0 100644 --- a/src/ExcludeFolders/functions/folderFunctions.ts +++ b/src/ExcludeFolders/functions/folderFunctions.ts @@ -32,8 +32,8 @@ export function getExcludedFolder(plugin: FolderNotesPlugin, path: string, inclu 'excludeFromFolderOverview', 'detached', 'hideInSettings', - 'hideNote', 'id', + 'showFolderNote', ]; if (combinedExcludedFolders.length > 0) { @@ -56,8 +56,9 @@ export function getExcludedFolder(plugin: FolderNotesPlugin, path: string, inclu excludedFolder.disableAutoCreate ? excludedFolder.disableAutoCreate = !whitelistedFolder.enableAutoCreate : ''; excludedFolder.disableFolderNote ? excludedFolder.disableFolderNote = !whitelistedFolder.enableFolderNote : ''; excludedFolder.disableSync ? excludedFolder.disableSync = !whitelistedFolder.enableSync : ''; - excludedFolder.enableCollapsing = whitelistedFolder.enableCollapsing; + excludedFolder.enableCollapsing = !whitelistedFolder.disableCollapsing; excludedFolder.excludeFromFolderOverview ? excludedFolder.excludeFromFolderOverview = !whitelistedFolder.showInFolderOverview : ''; + excludedFolder.showFolderNote = !whitelistedFolder.hideInFileExplorer; } else if (excludedFolder && Object.keys(excludedFolder).length === 0) { excludedFolder = { type: 'folder', @@ -73,7 +74,7 @@ export function getExcludedFolder(plugin: FolderNotesPlugin, path: string, inclu excludeFromFolderOverview: false, hideInSettings: false, detached: false, - hideNote: false, + showFolderNote: false, }; } diff --git a/src/ExcludeFolders/modals/ExcludeFolderSettings.ts b/src/ExcludeFolders/modals/ExcludeFolderSettings.ts index 54020ad..7326bc6 100644 --- a/src/ExcludeFolders/modals/ExcludeFolderSettings.ts +++ b/src/ExcludeFolders/modals/ExcludeFolderSettings.ts @@ -1,6 +1,7 @@ import { App, Modal, Setting } from 'obsidian'; import FolderNotesPlugin from '../../main'; import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder'; +import { updateCSSClassesForFolder } from 'src/functions/styleFunctions'; export default class ExcludedFolderSettings extends Modal { plugin: FolderNotesPlugin; app: App; @@ -54,6 +55,20 @@ export default class ExcludedFolderSettings extends Modal { }) ); + new Setting(contentEl) + .setName('Show folder note in the file explorer') + .setDesc('Choose if the folder note should be shown in the file explorer') + .addToggle((toggle) => + toggle + .setValue(this.excludedFolder.showFolderNote) + .onChange(async (value) => { + this.excludedFolder.showFolderNote = value; + updateCSSClassesForFolder(this.excludedFolder.path, this.plugin); + await this.plugin.saveSettings(); + this.display(); + }) + ); + new Setting(contentEl) .setName('Disable auto creation of folder notes in this folder') .setDesc('Choose if a folder note should be created when a new folder is created') diff --git a/src/ExcludeFolders/modals/PatternSettings.ts b/src/ExcludeFolders/modals/PatternSettings.ts index 69fd791..a353c26 100644 --- a/src/ExcludeFolders/modals/PatternSettings.ts +++ b/src/ExcludeFolders/modals/PatternSettings.ts @@ -1,6 +1,7 @@ import { App, Modal, Setting } from 'obsidian'; import FolderNotesPlugin from '../../main'; import { ExcludePattern } from 'src/ExcludeFolders/ExcludePattern'; +import { refreshAllFolderStyles } from 'src/functions/styleFunctions'; export default class PatternSettings extends Modal { plugin: FolderNotesPlugin; @@ -56,6 +57,20 @@ export default class PatternSettings extends Modal { }) ); + new Setting(contentEl) + .setName('Show folder note in the file explorer') + .setDesc('Choose if the folder note should be shown in the file explorer') + .addToggle((toggle) => + toggle + .setValue(this.pattern.showFolderNote) + .onChange(async (value) => { + this.pattern.showFolderNote = value; + await this.plugin.saveSettings(); + refreshAllFolderStyles(true, this.plugin); + this.display(); + }) + ); + new Setting(contentEl) .setName('Disable open folder note') .setDesc('Choose if the folder note should be opened when the folder is opened') diff --git a/src/ExcludeFolders/modals/WhitelistFolderSettings.ts b/src/ExcludeFolders/modals/WhitelistFolderSettings.ts index 77f6360..9673758 100644 --- a/src/ExcludeFolders/modals/WhitelistFolderSettings.ts +++ b/src/ExcludeFolders/modals/WhitelistFolderSettings.ts @@ -54,6 +54,18 @@ export default class WhitelistFolderSettings extends Modal { }) ); + new Setting(contentEl) + .setName('Hide folder note in file explorer') + .setDesc('Choose if the folder note should be hidden in the file explorer') + .addToggle((toggle) => + toggle + .setValue(this.whitelistedFolder.hideInFileExplorer) + .onChange(async (value) => { + this.whitelistedFolder.hideInFileExplorer = value; + await this.plugin.saveSettings(); + }) + ); + new Setting(contentEl) .setName('Allow auto creation of folder notes in this folder') .addToggle((toggle) => @@ -81,13 +93,13 @@ export default class WhitelistFolderSettings extends Modal { if (this.whitelistedFolder.enableFolderNote) { new Setting(contentEl) - .setName('Collapse folder when opening folder note') + .setName('Don\'t collapse folder when opening folder note') .setDesc('Choose if the folder should be collapsed when the folder note is opened') .addToggle((toggle) => toggle - .setValue(this.whitelistedFolder.enableCollapsing) + .setValue(this.whitelistedFolder.disableCollapsing) .onChange(async (value) => { - this.whitelistedFolder.enableCollapsing = value; + this.whitelistedFolder.disableCollapsing = value; await this.plugin.saveSettings(); }) ); diff --git a/src/ExcludeFolders/modals/WhitelistPatternSettings.ts b/src/ExcludeFolders/modals/WhitelistPatternSettings.ts index 9d6d1c8..b328f34 100644 --- a/src/ExcludeFolders/modals/WhitelistPatternSettings.ts +++ b/src/ExcludeFolders/modals/WhitelistPatternSettings.ts @@ -70,13 +70,13 @@ export default class WhitelistPatternSettings extends Modal { if (this.pattern.enableFolderNote) { new Setting(contentEl) - .setName('Collapse folder when opening folder note') + .setName('Don\'t collapse folder when opening folder note') .setDesc('Choose if the folder should be collapsed when the folder note is opened') .addToggle((toggle) => toggle - .setValue(this.pattern.enableCollapsing) + .setValue(this.pattern.disableCollapsing) .onChange(async (value) => { - this.pattern.enableCollapsing = value; + this.pattern.disableCollapsing = value; await this.plugin.saveSettings(); }) ); diff --git a/src/events/FrontMatterTitle.ts b/src/events/FrontMatterTitle.ts index 8a66190..966837f 100644 --- a/src/events/FrontMatterTitle.ts +++ b/src/events/FrontMatterTitle.ts @@ -33,7 +33,7 @@ export class FrontMatterTitlePluginHandler { const event: Listener = { name: 'manager:update', cb: (data) => { - this.handleRename(data as any, true); + this.fmptUpdateFileName(data as any, true); }, }; // Keep ref to remove listener @@ -41,10 +41,10 @@ export class FrontMatterTitlePluginHandler { if (ref) { this.eventRef = ref; } - this.plugin.app.vault.getFiles().forEach((file) => { - this.handleRename({ id: '', result: false, path: file.path }, false); - }); - this.plugin.updateBreadcrumbs(); + // this.plugin.app.vault.getFiles().forEach((file) => { + // this.handleRename({ id: '', result: false, path: file.path }, false); + // }); + this.plugin.updateAllBreadcrumbs(); })(); } deleteEvent() { @@ -52,10 +52,12 @@ export class FrontMatterTitlePluginHandler { this.dispatcher.removeListener(this.eventRef); } } - async handleRename(data: { + async fmptUpdateFileName(data: { id: string; result: boolean; path: string; + pathOnly: boolean; + breadcrumb?: HTMLElement; }, isEvent: boolean) { if ((data as any).data) data = (data as any).data; const file = this.app.vault.getAbstractFileByPath(data.path); @@ -69,12 +71,19 @@ export class FrontMatterTitlePluginHandler { const folderNote = getFolderNote(this.plugin, folder.path); if (!folderNote) { return; } if (folderNote !== file) { return; } + if (!data.pathOnly) { + this.plugin.changeFolderNameInExplorer(folder, newName); + } + + const breadcrumb = data.breadcrumb; + if (breadcrumb) { + this.plugin.changeFolderNameInPath(folder, newName, breadcrumb); + } if (isEvent) { - this.plugin.changeName(folder, newName, true); - } else { - this.plugin.changeName(folder, newName, false); + this.plugin.updateAllBreadcrumbs(); } + if (newName) { folder.newName = newName; this.modifiedFolders.set(folder.path, folder); @@ -84,4 +93,34 @@ export class FrontMatterTitlePluginHandler { } } + + async fmptUpdateFolderName(data: { + id: string; + result: boolean; + path: string; + pathOnly: boolean; + breadcrumb?: HTMLElement; + }, replacePath: boolean) { + if ((data as any).data) data = (data as any).data; + const folder = this.app.vault.getAbstractFileByPath(data.path); + if (!(folder instanceof TFolder)) { return; } + const folderNote = getFolderNote(this.plugin, folder.path); + if (!folderNote) { return; } + + const resolver = this.api?.getResolverFactory()?.createResolver('#feature-id#'); + const newName = resolver?.resolve(folderNote?.path ?? ''); + if (!newName) return; + + if (!data.pathOnly) { + this.plugin.changeFolderNameInExplorer(folder, newName); + } + + const breadcrumb = data.breadcrumb; + if (breadcrumb) { + this.plugin.changeFolderNameInPath(folder, newName, breadcrumb); + } + + folder.newName = newName; + this.modifiedFolders.set(folder.path, folder); + } } diff --git a/src/events/MutationObserver.ts b/src/events/MutationObserver.ts index 3d9990c..5b7aea8 100644 --- a/src/events/MutationObserver.ts +++ b/src/events/MutationObserver.ts @@ -1,98 +1,126 @@ +import { Keymap, Platform } from 'obsidian'; import FolderNotesPlugin from 'src/main'; -import { Platform, Keymap } from 'obsidian'; import { getFolderNote } from 'src/functions/folderNoteFunctions'; -import { handleFolderClick, handleViewHeaderClick } from './handleClick'; +import { handleViewHeaderClick } from './handleClick'; import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; -import { applyCSSClassesToFolder } from 'src/functions/styleFunctions'; +import { updateCSSClassesForFolder } from 'src/functions/styleFunctions'; -export async function addObserver(plugin: FolderNotesPlugin) { - plugin.observer = new MutationObserver((mutations: MutationRecord[]) => { - mutations.forEach((rec) => { - if (rec.type === 'childList') { - (rec.target).querySelectorAll('div.nav-folder') - .forEach(async (element: HTMLElement) => { - let folderTitle = element.querySelector('div.nav-folder-title-content') as HTMLElement; - const filePath = folderTitle.parentElement?.getAttribute('data-path') || ''; +let fileExplorerMutationObserver: MutationObserver | null = null; - applyCSSClassesToFolder(filePath, plugin); +export function registerFileExplorerObserver(plugin: FolderNotesPlugin) { + // Run once on initial layout + plugin.app.workspace.onLayoutReady(() => { + initializeFolderNoteFeatures(plugin); + initializeBreadcrumbs(plugin); + }); - if (folderTitle) { - await initializeFolderTitle(folderTitle, plugin); - } else { + // Re-run when layout changes (e.g. File Explorer is reopened) + plugin.registerEvent( + plugin.app.workspace.on('layout-change', () => { + initializeFolderNoteFeatures(plugin); - const observer = new MutationObserver(async (mutations, obs) => { - folderTitle = element.querySelector('div.nav-folder-title-content') as HTMLElement; - if (folderTitle) { - await initializeFolderTitle(folderTitle, plugin); - obs.disconnect(); - } - }); + const activeLeaf = plugin.app.workspace.getActiveFileView()?.containerEl; + if (!activeLeaf) return; - observer.observe(element, { childList: true, subtree: true }); - } - }); - if (!plugin.settings.openFolderNoteOnClickInPath) { return; } - // (rec.target).querySelectorAll('div.nav-file-title-content') - // .forEach(async (element: HTMLElement) => { - // const filePath = element.parentElement?.getAttribute('data-path') || ''; - // applyCSSClassesToFolderNote(filePath, plugin); - // }); - (rec.target).querySelectorAll('span.view-header-breadcrumb') - .forEach((element: HTMLElement) => { - const breadcrumbs = element.parentElement?.querySelectorAll('span.view-header-breadcrumb'); - if (!breadcrumbs) return; - let path = ''; - breadcrumbs.forEach(async (breadcrumb: HTMLElement) => { - if (breadcrumb.hasAttribute('old-name')) { - path += breadcrumb.getAttribute('old-name') + '/'; - } else { - path += breadcrumb.innerText.trim() + '/'; - } - const folderPath = path.slice(0, -1); - breadcrumb.setAttribute('data-path', folderPath); - const folder = plugin.fmtpHandler?.modifiedFolders.get(folderPath); - if (folder && plugin.settings.frontMatterTitle.path && plugin.settings.frontMatterTitle.enabled) { - breadcrumb.setAttribute('old-name', folder.name || ''); - breadcrumb.innerText = folder.newName || ''; - } - const excludedFolder = await getExcludedFolder(plugin, folderPath, true); - if (excludedFolder?.disableFolderNote) return; - const folderNote = getFolderNote(plugin, folderPath); - if (folderNote) { - breadcrumb.classList.add('has-folder-note'); - } - }); - element.parentElement?.setAttribute('data-path', path.slice(0, -1)); - if (breadcrumbs.length > 0) { - breadcrumbs.forEach((breadcrumb: HTMLElement) => { - if (breadcrumb.onclick) return; - breadcrumb.addEventListener('click', (e) => { - handleViewHeaderClick(e, plugin); - }, { capture: true }); - }); - } - }); - } - }); + const titleContainer = activeLeaf.querySelector('.view-header-title-container'); + if (!(titleContainer instanceof HTMLElement)) return; + + updateFolderNamesInPath(plugin, titleContainer); + }) + ); +} + +export function unregisterFileExplorerObserver() { + if (fileExplorerMutationObserver) { + fileExplorerMutationObserver.disconnect(); + fileExplorerMutationObserver = null; + } +} + +function initializeFolderNoteFeatures(plugin: FolderNotesPlugin) { + initializeAllFolderTitles(plugin); + observeFolderTitleMutations(plugin); +} + +function initializeBreadcrumbs(plugin: FolderNotesPlugin) { + const titleContainers = document.querySelectorAll('.view-header-title-container'); + if (!titleContainers.length) return; + titleContainers.forEach((container) => { + if (!(container instanceof HTMLElement)) return; + scheduleIdle(() => updateFolderNamesInPath(plugin, container), { timeout: 1000 }); }); } -async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) { - if (folderTitle.onclick) return; - if (Platform.isMobile && plugin.settings.disableOpenFolderNoteOnClick) return; - - const folderPath = folderTitle.parentElement?.getAttribute('data-path') || ''; - - await applyCSSClassesToFolder(folderPath, plugin); - - // Handle middle click (auxclick) - folderTitle.addEventListener('auxclick', (event: MouseEvent) => { - if (event.button === 1) { - handleFolderClick(event, plugin); +/** + * Observes the File Explorer for newly added folder elements and applies plugin logic (e.g., styles, event listeners) + * automatically when folders are created, expanded, or when the File Explorer view is reopened. + */ +function observeFolderTitleMutations(plugin: FolderNotesPlugin) { + if (fileExplorerMutationObserver) { + fileExplorerMutationObserver.disconnect(); + } + fileExplorerMutationObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of Array.from(mutation.addedNodes)) { + if (!(node instanceof HTMLElement)) continue; + processAddedFolders(node, plugin); + } } - }, { capture: true }); + }); - folderTitle.onclick = (event: MouseEvent) => handleFolderClick(event, plugin); + fileExplorerMutationObserver.observe(document, { childList: true, subtree: true }); +} + +function initializeAllFolderTitles(plugin: FolderNotesPlugin) { + const allTitles = document.querySelectorAll('.nav-folder-title-content'); + for (const title of Array.from(allTitles)) { + const folderTitle = title as HTMLElement; + const folderEl = folderTitle.closest('.nav-folder-title'); + if (!folderEl) continue; + + const folderPath = folderEl.getAttribute('data-path') || ''; + setupFolderTitle(folderTitle, plugin, folderPath); + } +} + +function processAddedFolders(node: HTMLElement, plugin: FolderNotesPlugin) { + const titles: HTMLElement[] = []; + if (node.matches('.nav-folder-title-content')) { + titles.push(node); + } + node.querySelectorAll('.nav-folder-title-content').forEach((el) => { + titles.push(el as HTMLElement); + }); + + titles.forEach((folderTitle) => { + const folderEl = folderTitle.closest('.nav-folder-title'); + const folderPath = folderEl?.getAttribute('data-path') || ''; + if (!folderEl || !folderPath) { + setTimeout(() => { + const retryFolderEl = folderTitle.closest('.nav-folder-title'); + const retryFolderPath = retryFolderEl?.getAttribute('data-path') || ''; + if (retryFolderEl && retryFolderPath) { + setupFolderTitle(folderTitle, plugin, retryFolderPath); + } + }, 50); + return; + } + setupFolderTitle(folderTitle, plugin, folderPath); + }); +} + +async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlugin, folderPath: string) { + if (folderTitle.dataset.initialized === 'true') return; + if (!folderPath) return; + + folderTitle.dataset.initialized = 'true'; + await updateCSSClassesForFolder(folderPath, plugin); + + if (plugin.settings.frontMatterTitle.enabled) { + plugin.fmtpHandler?.fmptUpdateFolderName({ id: '', result: false, path: folderPath, pathOnly: false }, false); + } + + if (Platform.isMobile && plugin.settings.disableOpenFolderNoteOnClick) return; plugin.registerDomEvent(folderTitle, 'pointerover', (event: MouseEvent) => { plugin.hoveredElement = folderTitle; @@ -101,17 +129,16 @@ async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) { if (!Keymap.isModEvent(event)) return; if (!(event.target instanceof HTMLElement)) return; - const folderPath = event?.target.parentElement?.getAttribute('data-path') || ''; const folderNote = getFolderNote(plugin, folderPath); if (!folderNote) return; plugin.app.workspace.trigger('hover-link', { - event: event, + event, source: 'preview', hoverParent: { file: folderNote }, targetEl: event.target, - linktext: folderNote?.basename, - sourcePath: folderNote?.path, + linktext: folderNote.basename, + sourcePath: folderNote.path, }); plugin.hoverLinkTriggered = true; }); @@ -122,3 +149,40 @@ async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) { plugin.hoverLinkTriggered = false; }); } + +async function updateFolderNamesInPath(plugin: FolderNotesPlugin, titleContainer: HTMLElement) { + const headers = titleContainer.querySelectorAll('span.view-header-breadcrumb'); + let path = ''; + headers.forEach(async (breadcrumb: HTMLElement) => { + path += breadcrumb.getAttribute('old-name') ?? (breadcrumb as HTMLElement).innerText.trim(); + path += '/'; + const folderPath = path.slice(0, -1); + + const excludedFolder = getExcludedFolder(plugin, folderPath, true); + if (excludedFolder?.disableFolderNote) return; + const folderNote = getFolderNote(plugin, folderPath); + if (folderNote) breadcrumb.classList.add('has-folder-note'); + + breadcrumb?.setAttribute('data-path', path.slice(0, -1)); + if (!breadcrumb.onclick) { + breadcrumb.addEventListener('click', (e) => { + handleViewHeaderClick(e as MouseEvent, plugin); + }, { capture: true }); + } + + if (plugin.settings.frontMatterTitle.enabled) { + plugin.fmtpHandler?.fmptUpdateFolderName({ id: '', result: false, path: folderPath, pathOnly: true, breadcrumb: breadcrumb }, true); + } + }); +} + +// Schedules a callback to run when the browser is idle, or after a timeout as a fallback. +// - callback: The function to execute when idle or after the timeout. +// - options: Optional object with a 'timeout' property (in milliseconds). +function scheduleIdle(callback: () => void, options?: { timeout: number }) { + if ('requestIdleCallback' in window) { + (window as any).requestIdleCallback(callback, options); + } else { + setTimeout(callback, options?.timeout || 200); + } +} diff --git a/src/events/handleClick.ts b/src/events/handleClick.ts index 5022812..d5106e7 100644 --- a/src/events/handleClick.ts +++ b/src/events/handleClick.ts @@ -2,7 +2,7 @@ import { Keymap, Platform } from 'obsidian'; import FolderNotesPlugin from 'src/main'; import { openFolderNote, createFolderNote, getFolderNote } from 'src/functions/folderNoteFunctions'; import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; -import { addCSSClassToTitleEL, removeCSSClassFromEL } from 'src/functions/styleFunctions'; +import { addCSSClassToFileExplorerEl, removeCSSClassFromFileExplorerEL } from 'src/functions/styleFunctions'; export async function handleViewHeaderClick(event: MouseEvent, plugin: FolderNotesPlugin) { event.stopImmediatePropagation(); @@ -13,7 +13,7 @@ export async function handleViewHeaderClick(event: MouseEvent, plugin: FolderNot const folderPath = event.target.getAttribute('data-path'); if (!folderPath) { return; } - const excludedFolder = await getExcludedFolder(plugin, folderPath, true); + const excludedFolder = getExcludedFolder(plugin, folderPath, true); if (excludedFolder?.disableFolderNote) { event.target.onclick = null; event.target.click(); @@ -38,65 +38,11 @@ export async function handleViewHeaderClick(event: MouseEvent, plugin: FolderNot } else if (event.altKey || Keymap.isModEvent(event) === 'tab') { if ((plugin.settings.altKey && event.altKey) || (plugin.settings.ctrlKey && Keymap.isModEvent(event) === 'tab')) { await createFolderNote(plugin, folderPath, true, undefined, true); - addCSSClassToTitleEL(folderPath, 'has-folder-note', plugin); - removeCSSClassFromEL(folderPath, 'has-not-folder-note', plugin); + addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, plugin); return; } } event.target.onclick = null; event.target.click(); } - -export async function handleFolderClick(event: MouseEvent, plugin: FolderNotesPlugin) { - - if (!(event.target instanceof HTMLElement)) return; - if (!event || !event.target) return; - - if (!event.shiftKey) { - event.stopImmediatePropagation(); - } else { - return; - } - - const folderPath = event.target.parentElement?.getAttribute('data-path'); - if (!folderPath) { return; } - - const excludedFolder = await getExcludedFolder(plugin, folderPath, true); - if (excludedFolder?.disableFolderNote) { - event.target.onclick = null; - event.target.click(); - return; - } else if (excludedFolder?.enableCollapsing || plugin.settings.enableCollapsing) { - event.target.onclick = null; - event.target.click(); - } - - const folderNote = getFolderNote(plugin, folderPath); - - if (folderNote) { - if (plugin.settings.openByClick) { - return openFolderNote(plugin, folderNote, event); - } else if (plugin.settings.openWithCtrl && Keymap.isModEvent(event) === 'tab') { - return openFolderNote(plugin, folderNote, event); - } else if (plugin.settings.openWithAlt && event.altKey) { - return openFolderNote(plugin, folderNote, event); - } else { - if (plugin.settings.enableCollapsing) return; - event.target.parentElement?.click(); - return; - } - } else if (event.altKey || Keymap.isModEvent(event) === 'tab') { - if ((plugin.settings.altKey && event.altKey) || (plugin.settings.ctrlKey && Keymap.isModEvent(event) === 'tab')) { - await createFolderNote(plugin, folderPath, true, undefined, true); - addCSSClassToTitleEL(folderPath, 'has-folder-note', plugin); - removeCSSClassFromEL(folderPath, 'has-not-folder-note', plugin); - return; - } - } else if (!folderNote) { - if (plugin.settings.enableCollapsing) return; - return event.target.parentElement?.click(); - } - - event.target.onclick = null; - event.target.click(); -} diff --git a/src/events/handleCreate.ts b/src/events/handleCreate.ts index 072ff85..0cbcaee 100644 --- a/src/events/handleCreate.ts +++ b/src/events/handleCreate.ts @@ -2,17 +2,17 @@ import { TAbstractFile, TFolder, TFile } from 'obsidian'; import FolderNotesPlugin from 'src/main'; import { createFolderNote, getFolder, getFolderNote, turnIntoFolderNote } from 'src/functions/folderNoteFunctions'; import { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; -import { removeCSSClassFromEL, addCSSClassToTitleEL } from 'src/functions/styleFunctions'; +import { removeCSSClassFromFileExplorerEL, addCSSClassToFileExplorerEl } from 'src/functions/styleFunctions'; export async function handleCreate(file: TAbstractFile, plugin: FolderNotesPlugin) { if (!plugin.app.workspace.layoutReady) return; const folder = file.parent; if (folder instanceof TFolder) { - if (plugin.isEmptyFolderNoteFolder(folder)) { - addCSSClassToTitleEL(folder.path, 'only-has-folder-note', plugin); + if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) { + addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin); } else { - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } } @@ -31,13 +31,17 @@ async function handleFileCreation(file: TFile, plugin: FolderNotesPlugin) { const newFolder = await plugin.app.fileManager.createNewFolder(file.parent); turnIntoFolderNote(plugin, file, newFolder); } else if (folder instanceof TFolder) { - const detachedFolder = await getExcludedFolder(plugin, folder.path, true); + if (folder.children.length >= 1) { + removeCSSClassFromFileExplorerEL(folder.path, 'fn-empty-folder', false, plugin); + } + + const detachedFolder = getExcludedFolder(plugin, folder.path, true); if (detachedFolder) { return; } const folderNote = getFolderNote(plugin, folder.path); if (folderNote && folderNote.path === file.path) { - addCSSClassToTitleEL(folder.path, 'has-folder-note', plugin); - addCSSClassToTitleEL(file.path, 'is-folder-note', plugin); + addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); + addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); } else if (plugin.settings.autoCreateForFiles) { if (!file.parent) { return; } const newFolder = await plugin.app.fileManager.createNewFolder(file.parent); @@ -52,6 +56,7 @@ async function handleFolderCreation(folder: TFolder, plugin: FolderNotesPlugin) const attachmentFolderPath = plugin.app.vault.getConfig('attachmentFolderPath') as string; const cleanAttachmentFolderPath = attachmentFolderPath?.replace('./', '') || ''; const attachmentsAreInRootFolder = attachmentFolderPath === './' || attachmentFolderPath === ''; + addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, plugin); if (!plugin.settings.autoCreateForAttachmentFolder) { if (!attachmentsAreInRootFolder && cleanAttachmentFolderPath === folder.name) return; @@ -59,12 +64,12 @@ async function handleFolderCreation(folder: TFolder, plugin: FolderNotesPlugin) openFile = false; } - const excludedFolder = await getExcludedFolder(plugin, folder.path, true); + const excludedFolder = getExcludedFolder(plugin, folder.path, true); if (excludedFolder?.disableAutoCreate) return; const folderNote = getFolderNote(plugin, folder.path); if (folderNote) return; createFolderNote(plugin, folder.path, openFile, undefined, true); - addCSSClassToTitleEL(folder.path, 'has-folder-note', plugin); + addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); } diff --git a/src/events/handleDelete.ts b/src/events/handleDelete.ts index 324c9d5..b2b644c 100644 --- a/src/events/handleDelete.ts +++ b/src/events/handleDelete.ts @@ -1,16 +1,16 @@ import { TAbstractFile, TFolder, TFile } from 'obsidian'; import FolderNotesPlugin from 'src/main'; import { getFolderNote, getFolder, deleteFolderNote } from 'src/functions/folderNoteFunctions'; -import { removeCSSClassFromEL, addCSSClassToTitleEL } from 'src/functions/styleFunctions'; +import { removeCSSClassFromFileExplorerEL, addCSSClassToFileExplorerEl, hideFolderNoteInFileExplorer } from 'src/functions/styleFunctions'; import { getFolderPathFromString } from 'src/functions/utils'; export function handleDelete(file: TAbstractFile, plugin: FolderNotesPlugin) { const folder = plugin.app.vault.getAbstractFileByPath(getFolderPathFromString(file.path)); if (folder instanceof TFolder) { - if (plugin.isEmptyFolderNoteFolder(folder)) { - addCSSClassToTitleEL(folder.path, 'only-has-folder-note', plugin); + if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) { + addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin); } else { - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } } @@ -19,14 +19,15 @@ export function handleDelete(file: TAbstractFile, plugin: FolderNotesPlugin) { if (!folder) { return; } const folderNote = getFolderNote(plugin, folder.path); if (folderNote) { return; } - removeCSSClassFromEL(folder.path, 'has-folder-note', plugin); - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); + hideFolderNoteInFileExplorer(folder.path, plugin); } if (!(file instanceof TFolder)) { return; } const folderNote = getFolderNote(plugin, file.path); if (!folderNote) { return; } - removeCSSClassFromEL(folderNote.path, 'is-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin); if (!plugin.settings.syncDelete) { return; } deleteFolderNote(plugin, folderNote, false); } diff --git a/src/events/handleRename.ts b/src/events/handleRename.ts index 5e4c19c..abdb5d3 100644 --- a/src/events/handleRename.ts +++ b/src/events/handleRename.ts @@ -3,46 +3,67 @@ import FolderNotesPlugin from 'src/main'; import { extractFolderName, getFolderNote, getFolderNoteFolder } from '../functions/folderNoteFunctions'; import { getExcludedFolder, addExcludedFolder, updateExcludedFolder, deleteExcludedFolder, getDetachedFolder } from '../ExcludeFolders/functions/folderFunctions'; import { ExcludedFolder } from 'src/ExcludeFolders/ExcludeFolder'; -import { removeCSSClassFromEL, addCSSClassToTitleEL } from 'src/functions/styleFunctions'; +import { removeCSSClassFromFileExplorerEL, addCSSClassToFileExplorerEl, markFileAsFolderNote, unmarkFileAsFolderNote, unmarkFolderAsFolderNote, markFolderWithFolderNoteClasses, hideFolderNoteInFileExplorer, removeActiveFolder, setActiveFolder } from 'src/functions/styleFunctions'; import { getFolderPathFromString, removeExtension, getFileNameFromPathString } from 'src/functions/utils'; export function handleRename(file: TAbstractFile, oldPath: string, plugin: FolderNotesPlugin) { const folder = file.parent; const oldFolder = plugin.app.vault.getAbstractFileByPath(getFolderPathFromString(oldPath)); - const isRename = (file.parent?.path === getFolderPathFromString(oldPath)); + if (folder instanceof TFolder) { - if (plugin.isEmptyFolderNoteFolder(folder)) { - addCSSClassToTitleEL(folder.path, 'only-has-folder-note', plugin); + if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) { + addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin); } else { - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } } if (oldFolder instanceof TFolder) { - if (plugin.isEmptyFolderNoteFolder(oldFolder)) { - addCSSClassToTitleEL(oldFolder.path, 'only-has-folder-note', plugin); + if (plugin.isEmptyFolderNoteFolder(oldFolder) && getFolderNote(plugin, oldFolder.path)) { + addCSSClassToFileExplorerEl(oldFolder.path, 'only-has-folder-note', true, plugin); } else { - removeCSSClassFromEL(oldFolder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(oldFolder.path, 'only-has-folder-note', true, plugin); } } if (file instanceof TFolder) { - plugin.tabManager.updateTab(file.path); - updateExcludedFolderPath(file, oldPath, plugin); - if (isRename) { - return handleFolderRename(file, oldPath, plugin); + const folder = file; + plugin.tabManager.updateTab(folder.path); + updateExcludedFolderPath(folder, oldPath, plugin); + if (isFolderRename(folder, oldPath)) { + return handleFolderRename(folder, oldPath, plugin); } else { - return handleFolderMove(file, oldPath, plugin); + return handleFolderMove(folder, oldPath, plugin); } } else if (file instanceof TFile) { - if (isRename) { - return handleFileRename(file, oldPath, plugin); + if (isFileRename(file, oldPath)) { + return fmptUpdateFileName(file, oldPath, plugin); } else { return handleFileMove(file, oldPath, plugin); } } } +function isFileRename(file: TFile, oldPath: string): boolean { + const oldFolderPath = getFolderPathFromString(oldPath); + const newFolderPath = file.parent?.path; + const oldName = getFileNameFromPathString(oldPath); + const newName = file.name; + + return oldFolderPath === newFolderPath && oldName !== newName; +} + + +function isFolderRename(folder: TFolder, oldPath: string): boolean { + const oldName = getFileNameFromPathString(oldPath); + const newName = folder.name; + const oldParent = getFolderPathFromString(oldPath); + const newParent = folder.parent?.path; + + return oldParent === newParent && oldName !== newName; +} + + export function handleFolderMove(file: TFolder, oldPath: string, plugin: FolderNotesPlugin) { // Soon @@ -54,12 +75,19 @@ export async function handleFileMove(file: TFile, oldPath: string, plugin: Folde const folderName = extractFolderName(plugin.settings.folderNoteName, file.basename) || file.basename; const oldFileName = removeExtension(getFileNameFromPathString(oldPath)); const newFolder = getFolderNoteFolder(plugin, file, file.basename); - const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation, file); - let excludedFolder = await getExcludedFolder(plugin, newFolder?.path || '', true); + let excludedFolder = getExcludedFolder(plugin, newFolder?.path || '', true); const oldFolder = getFolderNoteFolder(plugin, oldPath, oldFileName); + const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation, file); - // file has been moved into position where it can be a folder note! - if (folderName === newFolder?.name && folderNote) { + + const isFileNowFolderNoteInNewFolder = folderName === newFolder?.name; + const isFileMovedFromOldFolderNote = oldFolder && oldFolder.name === oldFileName && newFolder?.path !== oldFolder.path; + + // this is for turning files into folder notes for folders that already have a folder note + // e.g. Turn into folder note for "Folder name" + const isFileNowFolderNoteWithExistingNote = folderName === newFolder?.name && folderNote; + + if (isFileNowFolderNoteWithExistingNote) { let excludedFolderExisted = true; let disabledSync = false; @@ -81,12 +109,28 @@ export async function handleFileMove(file: TFile, oldPath: string, plugin: Folde updateExcludedFolder(plugin, excludedFolder, excludedFolder); } }); - } else if (oldFolder && oldFolder.name === oldFileName && newFolder?.path !== oldFolder.path) { - // the note has been moved somewhere and is no longer a folder note - // cleanup css on the folder and note - removeCSSClassFromEL(oldFolder.path, 'has-folder-note', plugin); - removeCSSClassFromEL(file.path, 'is-folder-note', plugin); - removeCSSClassFromEL(oldPath, 'is-folder-note', plugin); + } else if (isFileNowFolderNoteInNewFolder) { + if (!excludedFolder?.disableFolderNote) { + markFileAsFolderNote(file, plugin); + if (newFolder instanceof TFolder) { + markFolderWithFolderNoteClasses(newFolder, plugin); + if (plugin.app.workspace.getActiveFile()?.path === file.path) { + removeActiveFolder(plugin); + setActiveFolder(newFolder.path, plugin); + } + } + if (oldFolder instanceof TFolder) { + hideFolderNoteInFileExplorer(oldFolder.path, plugin); + unmarkFolderAsFolderNote(oldFolder, plugin); + } + } + } else if (isFileMovedFromOldFolderNote) { + unmarkFileAsFolderNote(file, plugin); + if (oldFolder instanceof TFolder) { + removeActiveFolder(plugin); + hideFolderNoteInFileExplorer(oldFolder.path, plugin); + unmarkFolderAsFolderNote(oldFolder, plugin); + } } } @@ -96,14 +140,13 @@ export async function handleFolderRename(file: TFolder, oldPath: string, plugin: if (fileName === oldFileName) { return; } - const folderNote = getFolderNote(plugin, oldPath); if (!(folderNote instanceof TFile)) return; - const excludedFolder = await getExcludedFolder(plugin, file.path, true); + const excludedFolder = getExcludedFolder(plugin, file.path, true); if (excludedFolder?.disableSync && !folderNote) { - return removeCSSClassFromEL(file.path, 'has-folder-note', plugin); + return removeCSSClassFromFileExplorerEL(file.path, 'has-folder-note', false, plugin); } if (!plugin.settings.syncFolderName) { return; } @@ -129,7 +172,7 @@ export async function handleFolderRename(file: TFolder, oldPath: string, plugin: plugin.app.fileManager.renameFile(folderNote, newPath); } -export async function handleFileRename(file: TFile, oldPath: string, plugin: FolderNotesPlugin) { +export async function fmptUpdateFileName(file: TFile, oldPath: string, plugin: FolderNotesPlugin) { const oldFileName = removeExtension(getFileNameFromPathString(oldPath)); const newFileName = file.basename; if (oldFileName === newFileName) { return; } @@ -138,26 +181,26 @@ export async function handleFileRename(file: TFile, oldPath: string, plugin: Fol const folderName = extractFolderName(plugin.settings.folderNoteName, file.basename) || file.basename; const oldFolderName = extractFolderName(plugin.settings.folderNoteName, oldFileName) || oldFileName; const newFolder = getFolderNoteFolder(plugin, file, file.basename); - const excludedFolder = await getExcludedFolder(plugin, newFolder?.path || '', true); + const excludedFolder = getExcludedFolder(plugin, newFolder?.path || '', true); const detachedExcludedFolder = getDetachedFolder(plugin, newFolder?.path || ''); const folderNote = getFolderNote(plugin, oldPath, plugin.settings.storageLocation, file); if (!excludedFolder?.disableFolderNote && folderName === newFolder?.name && !detachedExcludedFolder) { - addCSSClassToTitleEL(file.path, 'is-folder-note', plugin); - addCSSClassToTitleEL(newFolder.path, 'has-folder-note', plugin); + addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); + addCSSClassToFileExplorerEl(newFolder.path, 'has-folder-note', false, plugin); return; } else if (excludedFolder?.disableFolderNote || (folderName !== newFolder?.name)) { - removeCSSClassFromEL(file.path, 'is-folder-note', plugin); - removeCSSClassFromEL(newFolder?.path || '', 'has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(newFolder?.path || '', 'has-folder-note', false, plugin); } if (excludedFolder?.disableSync || !plugin.settings.syncFolderName) { return; } if (folderName === newFolder?.name) { - addCSSClassToTitleEL(file.path, 'is-folder-note', plugin); - removeCSSClassFromEL(oldFolder?.path, 'has-folder-note', plugin); - addCSSClassToTitleEL(newFolder.path, 'has-folder-note', plugin); + addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(oldFolder?.path, 'has-folder-note', false, plugin); + addCSSClassToFileExplorerEl(newFolder.path, 'has-folder-note', false, plugin); return; } @@ -175,12 +218,12 @@ export async function handleFileRename(file: TFile, oldPath: string, plugin: Fol async function renameFolderOnFileRename(file: TFile, oldPath: string, oldFolder: TAbstractFile, plugin: FolderNotesPlugin) { const newFolderName = extractFolderName(plugin.settings.folderNoteName, file.basename); if (!newFolderName) { - removeCSSClassFromEL(oldFolder.path, 'has-folder-note', plugin); - removeCSSClassFromEL(file.path, 'is-folder-note', plugin); + removeCSSClassFromFileExplorerEL(oldFolder.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin); return; } else if (newFolderName === oldFolder.name) { - addCSSClassToTitleEL(oldFolder.path, 'has-folder-note', plugin); - addCSSClassToTitleEL(file.path, 'is-folder-note', plugin); + addCSSClassToFileExplorerEl(oldFolder.path, 'has-folder-note', false, plugin); + addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); return; } diff --git a/src/functions/folderNoteFunctions.ts b/src/functions/folderNoteFunctions.ts index a507577..24b68c1 100644 --- a/src/functions/folderNoteFunctions.ts +++ b/src/functions/folderNoteFunctions.ts @@ -7,7 +7,7 @@ import { addExcludedFolder, deleteExcludedFolder, getDetachedFolder, getExcluded import { ExcludedFolder } from '../ExcludeFolders/ExcludeFolder'; import { openExcalidrawView } from './excalidraw'; import { AskForExtensionModal } from 'src/modals/AskForExtension'; -import { getEl, addCSSClassToTitleEL, removeCSSClassFromEL } from 'src/functions/styleFunctions'; +import { addCSSClassToFileExplorerEl, removeCSSClassFromFileExplorerEL, removeActiveFolder, setActiveFolder } from 'src/functions/styleFunctions'; import { getFolderNameFromPathString, getFolderPathFromString, removeExtension } from 'src/functions/utils'; const defaultExcalidrawTemplate = `--- @@ -60,7 +60,7 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st if (detachedFolder && folderNote?.extension !== extension && folderNote) { deleteExcludedFolder(plugin, detachedFolder); - removeCSSClassFromEL(folderNote?.path, 'is-folder-note', plugin); + 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; @@ -120,16 +120,12 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st if (openFile) { if (plugin.app.workspace.getActiveFile()?.path === path) { - if (plugin.activeFolderDom) { - plugin.activeFolderDom.removeClass('fn-is-active'); - plugin.activeFolderDom = null; - } + removeActiveFolder(plugin); const folder = getFolder(plugin, folderNote); if (!folder) { return; } - plugin.activeFolderDom = getEl(folder.path, plugin); - if (plugin.activeFolderDom) plugin.activeFolderDom.addClass('fn-is-active'); + setActiveFolder(folder.path, plugin); } await leaf.openFile(folderNote); if (plugin.settings.folderNoteType === '.excalidraw' || extension === '.excalidraw') { @@ -144,8 +140,8 @@ export async function createFolderNote(plugin: FolderNotesPlugin, folderPath: st const folder = plugin.app.vault.getAbstractFileByPath(folderPath); if (!(folder instanceof TFolder)) return; - addCSSClassToTitleEL(path, 'is-folder-note', plugin, true); - addCSSClassToTitleEL(folder.path, 'has-folder-note', plugin); + addCSSClassToFileExplorerEl(path, 'is-folder-note', false, plugin, true); + addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); } export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, folder: TFolder, folderNote?: TFile | null | TAbstractFile, skipConfirmation?: boolean) { @@ -156,7 +152,7 @@ export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, if (plugin.settings.showRenameConfirmation && !skipConfirmation && !detachedExcludedFolder) { return new ExistingFolderNoteModal(plugin.app, plugin, file, folder, folderNote).open(); } - removeCSSClassFromEL(folderNote.path, 'is-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin); const [excludedFolder, excludedFolderExisted, disabledSync] = await tempDisableSync(plugin, folder); @@ -190,20 +186,15 @@ export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, } await plugin.app.fileManager.renameFile(file, path); - addCSSClassToTitleEL(path, 'is-folder-note', plugin, true); - addCSSClassToTitleEL(folder.path, 'has-folder-note', plugin); + addCSSClassToFileExplorerEl(path, 'is-folder-note', false, plugin, true); + addCSSClassToFileExplorerEl(folder.path, 'has-folder-note', false, plugin); - if (plugin.activeFolderDom) { - plugin.activeFolderDom.removeClass('fn-is-active'); - plugin.activeFolderDom = null; - } - - plugin.activeFolderDom = getEl(folder.path, plugin); - if (plugin.activeFolderDom) plugin.activeFolderDom.addClass('fn-is-active'); + removeActiveFolder(plugin); + setActiveFolder(folder.path, plugin); } export async function tempDisableSync(plugin: FolderNotesPlugin, folder: TFolder): Promise<[excludedFolder: ExcludedFolder | undefined, excludedFolderExisted: boolean, disabledSync: boolean]> { - let excludedFolder = await getExcludedFolder(plugin, folder.path, false); + let excludedFolder = getExcludedFolder(plugin, folder.path, false); let excludedFolderExisted = true; let disabledSync = false; @@ -223,10 +214,34 @@ export async function tempDisableSync(plugin: FolderNotesPlugin, folder: TFolder export async function openFolderNote(plugin: FolderNotesPlugin, file: TAbstractFile, evt?: MouseEvent) { const path = file.path; - if (plugin.app.workspace.getActiveFile()?.path === path && !(Keymap.isModEvent(evt) === 'tab')) { return; } - const leaf = plugin.app.workspace.getLeaf(Keymap.isModEvent(evt) || plugin.settings.openInNewTab); - if (file instanceof TFile) { - await leaf.openFile(file); + const focusExistingTab = plugin.settings.focusExistingTab && plugin.settings.openInNewTab; + const activeFilePath = plugin.app.workspace.getActiveFile()?.path; + + // If already active and not opening in new tab, do nothing + if (activeFilePath === path && !(Keymap.isModEvent(evt) === 'tab')) { + return; + } + + // Try to find an existing tab with this file open + let foundLeaf = null; + if (focusExistingTab && file instanceof TFile) { + plugin.app.workspace.iterateAllLeaves((leaf) => { + if ( + leaf.getViewState().type === 'markdown' && + (leaf.view as import('obsidian').MarkdownView).file?.path === path + ) { + foundLeaf = leaf; + } + }); + } + + if (foundLeaf) { + plugin.app.workspace.setActiveLeaf(foundLeaf, { focus: true }); + } else { + const leaf = plugin.app.workspace.getLeaf(Keymap.isModEvent(evt) || plugin.settings.openInNewTab); + if (file instanceof TFile) { + await leaf.openFile(file); + } } } @@ -236,7 +251,12 @@ export async function deleteFolderNote(plugin: FolderNotesPlugin, file: TFile, d } const folder = getFolder(plugin, file); if (!folder) return; - removeCSSClassFromEL(folder.path, 'has-folder-note', plugin); + + plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter( + (excludedFolder) => (excludedFolder.path !== folder.path) && excludedFolder.showFolderNote); + plugin.saveSettings(false); + + removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); switch (plugin.settings.deleteFilesAction) { case 'trash': await plugin.app.vault.trash(file, true); @@ -295,7 +315,7 @@ export function getFolderNote(plugin: FolderNotesPlugin, folderPath: string, sto } let folderNote = plugin.app.vault.getAbstractFileByPath(path + folderNoteType); - if (folderNote instanceof TFile) { + if (folderNote instanceof TFile && plugin.settings.supportedFileTypes.includes(plugin.settings.folderNoteType.replace('.', ''))) { return folderNote; } else { const supportedFileTypes = plugin.settings.supportedFileTypes.filter((type) => type !== plugin.settings.folderNoteType.replace('.', '')); diff --git a/src/functions/styleFunctions.ts b/src/functions/styleFunctions.ts index a9a8cb7..9965d35 100644 --- a/src/functions/styleFunctions.ts +++ b/src/functions/styleFunctions.ts @@ -1,156 +1,208 @@ import { TFile, TFolder } from 'obsidian'; import FolderNotesPlugin from '../main'; -import { getDetachedFolder, getExcludedFolder } 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'; import FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main'; -export function loadFileClasses(forceReload = false, plugin: FolderNotesPlugin) { +/** + * @description Refreshes the CSS classes for all folder notes in the file explorer. + */ +export function refreshAllFolderStyles(forceReload = false, plugin: FolderNotesPlugin) { if (plugin.activeFileExplorer === getFileExplorer(plugin) && !forceReload) { return; } plugin.activeFileExplorer = getFileExplorer(plugin); plugin.app.vault.getAllLoadedFiles().forEach(async (file) => { - if (!(file instanceof TFolder)) { return; } - const folderNote = getFolderNote(plugin, file.path); - if (!folderNote) { - removeCSSClassFromEL(file?.path, 'has-folder-note', plugin); - removeCSSClassFromEL(file?.path, 'only-has-folder-note', plugin); - plugin.isEmptyFolderNoteFolder(file); - return; - } - - const excludedFolder = await getExcludedFolder(plugin, file.path, true); - // cleanup after ourselves - // Incase settings have changed - if (excludedFolder?.disableFolderNote) { - removeCSSClassFromEL(folderNote.path, 'is-folder-note', plugin); - removeCSSClassFromEL(file.path, 'has-folder-note', plugin); - removeCSSClassFromEL(file?.path, 'only-has-folder-note', plugin); - } else { - if (!excludedFolder?.hideNote) { - addCSSClassToTitleEL(folderNote.path, 'is-folder-note', plugin); - } - addCSSClassesToFolder(file, plugin); + if (file instanceof TFolder) { + await updateCSSClassesForFolder(file.path, plugin); } }); } - -export async function applyCSSClassesToFolder(folderPath: string, plugin: FolderNotesPlugin) { +/** + * @description Updates the CSS classes for a specific folder in the file explorer. + */ +export async function updateCSSClassesForFolder(folderPath: string, plugin: FolderNotesPlugin) { const folder = plugin.app.vault.getAbstractFileByPath(folderPath); if (!folder || !(folder instanceof TFolder)) { return; } const folderNote = getFolderNote(plugin, folder.path); const detachedFolderNote = getDetachedFolder(plugin, folder.path); + if (folder.children.length === 0) { + addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, plugin); + } + if (!folderNote || detachedFolderNote) { - removeCSSClassFromEL(folder?.path, 'has-folder-note', plugin); - removeCSSClassFromEL(folder?.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder?.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder?.path, 'only-has-folder-note', true, plugin); return; } - const excludedFolder = await getExcludedFolder(plugin, folder.path, true); - + const excludedFolder = getExcludedFolder(plugin, folder.path, true); if (excludedFolder?.disableFolderNote) { - removeCSSClassFromEL(folderNote.path, 'is-folder-note', plugin); - removeCSSClassFromEL(folder.path, 'has-folder-note', plugin); - removeCSSClassFromEL(folder?.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folderNote.path, 'is-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder?.path, 'only-has-folder-note', true, plugin); } else { - if (!excludedFolder?.hideNote) { - addCSSClassToFolderNote(folderNote, plugin); + markFolderWithFolderNoteClasses(folder, plugin); + if (excludedFolder?.showFolderNote) { + addCSSClassToFileExplorerEl(folder.path, 'show-folder-note-in-explorer', true, plugin); + unmarkFileAsFolderNote(folderNote, plugin); + return; } - addCSSClassesToFolder(folder, plugin); - if (plugin.isEmptyFolderNoteFolder(folder)) { - addCSSClassToTitleEL(folder.path, 'only-has-folder-note', plugin); + if (plugin.isEmptyFolderNoteFolder(folder) && getFolderNote(plugin, folder.path)) { + addCSSClassToFileExplorerEl(folder.path, 'only-has-folder-note', true, plugin); } else { - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } } - addCSSClassesToBothFolderAndNote(folderNote, folder, plugin); + markFolderAndNoteWithClasses(folderNote, folder, plugin); } -export async function applyCSSClassesToFolderNote(filePath: string, plugin: FolderNotesPlugin) { +/** + * @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) { const file = plugin.app.vault.getAbstractFileByPath(filePath); if (!file || !(file instanceof TFile)) { return; } const folder = getFolder(plugin, file); if (!folder || !(folder instanceof TFolder)) { return; } - applyCSSClassesToFolder(folder.path, plugin); - + updateCSSClassesForFolder(folder.path, plugin); } -export function addCSSClassesToBothFolderAndNote(file: TFile, folder: TFolder, plugin: FolderNotesPlugin) { - addCSSClassToFolderNote(file, plugin); - addCSSClassesToFolder(folder, plugin); +export function markFolderAndNoteWithClasses(file: TFile, folder: TFolder, plugin: FolderNotesPlugin) { + markFileAsFolderNote(file, plugin); + markFolderWithFolderNoteClasses(folder, plugin); } -export function removeCSSClassesFromBothFolderAndNote(folder: TFolder, file: TFile, plugin: FolderNotesPlugin) { - removeCSSClassFromFolderNote(file, plugin); - removeCSSClassesFromFolder(folder, plugin); +export function clearFolderAndNoteClasses(folder: TFolder, file: TFile, plugin: FolderNotesPlugin) { + unmarkFileAsFolderNote(file, plugin); + clearFolderNoteClassesFromFolder(folder, plugin); } -export function addCSSClassesToFolder(folder: TFolder, plugin: FolderNotesPlugin) { - addCSSClassToTitleEL(folder.path, 'has-folder-note', plugin); - if (plugin.isEmptyFolderNoteFolder(folder)) { - addCSSClassToTitleEL(folder.path, 'only-has-folder-note', plugin); +export function markFolderWithFolderNoteClasses(folder: TFolder, plugin: FolderNotesPlugin) { + 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); } else { - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } } -export function addCSSClassToFolderNote(file: TFile, plugin: FolderNotesPlugin) { - addCSSClassToTitleEL(file.path, 'is-folder-note', plugin); +export function markFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin) { + addCSSClassToFileExplorerEl(file.path, 'is-folder-note', false, plugin); } -export function removeCSSClassFromFolderNote(file: TFile, plugin: FolderNotesPlugin) { - removeCSSClassFromEL(file.path, 'is-folder-note', plugin); +export function unmarkFileAsFolderNote(file: TFile, plugin: FolderNotesPlugin) { + removeCSSClassFromFileExplorerEL(file.path, 'is-folder-note', false, plugin); } -export function removeCSSClassesFromFolder(folder: TFolder, plugin: FolderNotesPlugin) { - removeCSSClassFromEL(folder.path, 'has-folder-note', plugin); - removeCSSClassFromEL(folder.path, 'only-has-folder-note', plugin); +export function unmarkFolderAsFolderNote(folder: TFolder, plugin: FolderNotesPlugin) { + removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); } -export async function addCSSClassToTitleEL(path: string, cssClass: string, plugin: FolderNotesPlugin, waitForCreate = false, count = 0) { - const fileExplorerItem = getEl(path, plugin); +export function clearFolderNoteClassesFromFolder(folder: TFolder, plugin: FolderNotesPlugin) { + removeCSSClassFromFileExplorerEL(folder.path, 'has-folder-note', false, plugin); + removeCSSClassFromFileExplorerEL(folder.path, 'only-has-folder-note', true, plugin); +} + +/** + * @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) { + const fileExplorerItem = getFileExplorerElement(path, plugin); if (!fileExplorerItem) { if (waitForCreate && count < 5) { - // sleep for a second for the file-explorer event to catch up - // this is annoying as in most scanarios our plugin recieves the event before file explorer - // If we could guarrantee load order it wouldn't be an issue but we can't - // realise this is racey and needs to be fixed. await new Promise((r) => setTimeout(r, 500)); - addCSSClassToTitleEL(path, cssClass, plugin, waitForCreate, count + 1); + addCSSClassToFileExplorerEl(path, cssClass, parent, plugin, waitForCreate, count + 1); return; } return; } - fileExplorerItem.addClass(cssClass); - const viewHeaderItems = document.querySelectorAll(`[data-path='${CSS.escape(path)}']`); - viewHeaderItems.forEach((item) => { - item.addClass(cssClass); - }); + if (parent) { + const parentElement = fileExplorerItem?.parentElement; + if (parentElement) { + parentElement.addClass(cssClass); + } + } else { + fileExplorerItem.addClass(cssClass); + document.querySelectorAll(`[data-path='${CSS.escape(path)}']`).forEach((item) => { + item.addClass(cssClass); + }); + } } -export function removeCSSClassFromEL(path: string | undefined, cssClass: string, plugin: FolderNotesPlugin) { +/** + * @param path Can be a folder or file path + * @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) { if (!path) return; - const fileExplorerItem = getEl(path, plugin); - const viewHeaderItems = document.querySelectorAll(`[data-path='${CSS.escape(path)}']`); - viewHeaderItems.forEach((item) => { + const fileExplorerItem = getFileExplorerElement(path, plugin); + document.querySelectorAll(`[data-path='${CSS.escape(path)}']`).forEach((item) => { item.removeClass(cssClass); }); if (!fileExplorerItem) { return; } - fileExplorerItem.removeClass(cssClass); + if (parent) { + const parentElement = fileExplorerItem?.parentElement; + if (parentElement) { + parentElement.removeClass(cssClass); + } + return; + } else { + fileExplorerItem.removeClass(cssClass); + } } -export function getEl(path: string, plugin: FolderNotesPlugin | FolderOverviewPlugin): HTMLElement | null { +export function getFileExplorerElement(path: string, plugin: FolderNotesPlugin | FolderOverviewPlugin): HTMLElement | null { const fileExplorer = getFileExplorer(plugin); - if (!fileExplorer) { return null; } - if (!fileExplorer.view) { return null; } - if (!fileExplorer.view.fileItems) { return null; } + if (!fileExplorer?.view?.fileItems) { return null; } const fileExplorerItem = fileExplorer.view.fileItems?.[path]; - if (!fileExplorerItem) { return null; } - if (fileExplorerItem.selfEl) return fileExplorerItem.selfEl; - return fileExplorerItem.titleEl; + return fileExplorerItem?.selfEl ?? fileExplorerItem?.titleEl ?? null; +} + +export function showFolderNoteInFileExplorer(path: string, plugin: FolderNotesPlugin) { + const excludedFolder = new ExcludedFolder(path, plugin.settings.excludeFolders.length, undefined, plugin); + excludedFolder.subFolders = false; + excludedFolder.disableSync = false; + excludedFolder.disableAutoCreate = false; + excludedFolder.disableFolderNote = false; + excludedFolder.enableCollapsing = false; + excludedFolder.excludeFromFolderOverview = false; + excludedFolder.hideInSettings = true; + excludedFolder.showFolderNote = true; + addExcludedFolder(plugin, excludedFolder, false); + addCSSClassToFileExplorerEl(path, 'show-folder-note-in-explorer', true, plugin); + updateCSSClassesForFolder(path, plugin); +} + +export function hideFolderNoteInFileExplorer(folderPath: string, plugin: FolderNotesPlugin) { + plugin.settings.excludeFolders = plugin.settings.excludeFolders.filter( + (folder) => (folder.path !== folderPath) && folder.showFolderNote); + plugin.saveSettings(false); + removeCSSClassFromFileExplorerEL(folderPath, 'show-folder-note-in-explorer', true, plugin); + updateCSSClassesForFolder(folderPath, plugin); +} + +export function setActiveFolder(folderPath: string, plugin: FolderNotesPlugin) { + const fileExplorerItem = getFileExplorerElement(folderPath, plugin); + if (fileExplorerItem) { + fileExplorerItem.addClass('fn-is-active'); + plugin.activeFolderDom = fileExplorerItem; + } +} + +export function removeActiveFolder(plugin: FolderNotesPlugin) { + if (plugin.activeFolderDom) { + plugin.activeFolderDom.removeClass('fn-is-active'); + plugin.activeFolderDom?.removeClass('has-focus'); + plugin.activeFolderDom = null; + } } diff --git a/src/main.ts b/src/main.ts index c538e18..7c0bb5a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,11 @@ -import { Plugin, TFile, TFolder, TAbstractFile, MarkdownPostProcessorContext, parseYaml, Notice, Keymap, WorkspaceLeaf, requireApiVersion } from 'obsidian'; +import { Plugin, TFile, TFolder, TAbstractFile, MarkdownPostProcessorContext, parseYaml, Notice, Keymap, WorkspaceLeaf, requireApiVersion, Platform } from 'obsidian'; import { DEFAULT_SETTINGS, FolderNotesSettings, SettingsTab } from './settings/SettingsTab'; import { Commands } from './Commands'; import { FileExplorerWorkspaceLeaf } from './globals'; -import { handleFolderClick } from './events/handleClick'; -import { addObserver } from './events/MutationObserver'; +import { registerFileExplorerObserver, unregisterFileExplorerObserver } from './events/MutationObserver'; import { handleRename } from './events/handleRename'; -import { getFolderNote, getFolder, openFolderNote } 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'; @@ -14,7 +13,7 @@ import { FolderOverview } from './obsidian-folder-overview/src/FolderOverview'; import { TabManager } from './events/TabManager'; import './functions/ListComponent'; import { handleDelete } from './events/handleDelete'; -import { addCSSClassToTitleEL, getEl, loadFileClasses } from './functions/styleFunctions'; +import { addCSSClassToFileExplorerEl, getFileExplorerElement, removeCSSClassFromFileExplorerEL, refreshAllFolderStyles, setActiveFolder, removeActiveFolder } from './functions/styleFunctions'; import { getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; import { FileExplorerView, InternalPlugin } from 'obsidian-typings'; import { getFocusedItem } from './functions/utils'; @@ -67,12 +66,10 @@ export default class FolderNotesPlugin extends Plugin { this.app.workspace.onLayoutReady(this.onLayoutReady.bind(this)); - await addObserver(this); - - this.observer.observe(document.body, { - childList: true, - subtree: true, - }); + // this.observer.observe(document.body, { + // childList: true, + // subtree: true, + // }); if (!this.settings.persistentSettingsTab.afterRestart) { this.settings.settingsTab = 'general'; @@ -112,22 +109,18 @@ export default class FolderNotesPlugin extends Plugin { })); this.registerEvent(this.app.workspace.on('file-open', async (openFile: TFile | null) => { - if (this.activeFolderDom) { - this.activeFolderDom.removeClass('fn-is-active'); - this.activeFolderDom = null; - } + removeActiveFolder(this); if (!openFile || !openFile.basename) { return; } const folder = getFolder(this, openFile); if (!folder) { return; } - const excludedFolder = await getExcludedFolder(this, folder.path, true); + const excludedFolder = getExcludedFolder(this, folder.path, true); if (excludedFolder?.disableFolderNote) return; const folderNote = getFolderNote(this, folder.path); if (!folderNote) { return; } if (folderNote.path !== openFile.path) { return; } - this.activeFolderDom = getEl(folder.path, this); - if (this.activeFolderDom) this.activeFolderDom.addClass('fn-is-active'); + setActiveFolder(folder.path, this); })); this.registerEvent(this.app.vault.on('rename', (file: TAbstractFile, oldPath: string) => { @@ -141,9 +134,23 @@ export default class FolderNotesPlugin extends Plugin { this.registerMarkdownCodeBlockProcessor('folder-overview', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { this.handleOverviewBlock(source, el, ctx); }); + + this.registerEvent( + this.app.workspace.on('layout-change', () => { + const fileExplorerLeaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (!fileExplorerLeaf) return; + const container = fileExplorerLeaf.view.containerEl; + if (container) { + this.registerDomEvent(container, 'click', (evt: MouseEvent) => { + this.handleFileExplorerClick(evt); + }, true); + } + }) + ); } onLayoutReady() { + registerFileExplorerObserver(this); this.registerView(FOLDER_OVERVIEW_VIEW, (leaf: WorkspaceLeaf) => { return new FolderOverviewView(leaf, this); }); @@ -153,6 +160,35 @@ export default class FolderNotesPlugin extends Plugin { this.tabManager = new TabManager(this); this.tabManager.updateTabs(); + const fileExplorerLeaf = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (fileExplorerLeaf) { + const container = fileExplorerLeaf.view.containerEl; + if (container) { + this.registerDomEvent(container, 'click', (evt: MouseEvent) => { + this.handleFileExplorerClick(evt); + }, true); + } + } + + const fileExplorerPlugin = this.app.internalPlugins.getEnabledPluginById('file-explorer'); + if (fileExplorerPlugin) { + const originalRevealInFolder = fileExplorerPlugin.revealInFolder.bind(fileExplorerPlugin); + fileExplorerPlugin.revealInFolder = (file: TAbstractFile) => { + if (file instanceof TFile) { + const folder = getFolder(this, file); + if (folder instanceof TFolder) { + document.body.classList.remove('hide-folder-note'); + originalRevealInFolder.call(fileExplorerPlugin, folder); + setTimeout(() => { + document.body.classList.add('hide-folder-note'); + }, 100); + return; + } + } + return originalRevealInFolder.call(fileExplorerPlugin, file); + }; + } + const leaf = this.app.workspace.getLeavesOfType('markdown').first(); const view = leaf?.view; @@ -160,37 +196,90 @@ export default class FolderNotesPlugin extends Plugin { // @ts-ignore const editMode = view.editMode ?? view.sourceMode ?? this.app.workspace.activeEditor?.editMode; + // eslint-disable-next-line + const plugin = this; if (!editMode) { return; } // @ts-ignore - const originalHandleDragOver = editMode.clipboardManager.constructor.prototype.handleDragOver; + const clipboardProto = editMode.clipboardManager.constructor.prototype; - // @ts-ignore - editMode.clipboardManager.constructor.prototype.handleDragOver = function (evt, ...args) { - const { draggable } = this.app.dragManager; - if (draggable && draggable.file instanceof TFolder && getFolderNote(this, draggable.file.path)) { - this.app.dragManager.setAction(window.i18next.t('interface.drag-and-drop.insert-link-here')); - } else { - originalHandleDragOver.call(this, evt, ...args); + const originalHandleDragOver = clipboardProto.handleDragOver; + const originalHandleDrop = clipboardProto.handleDrop; + + clipboardProto.handleDragOver = function (evt: DragEvent, ...args: any[]) { + const dragManager = this.app.dragManager; + 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')); + return; + } } + + return originalHandleDragOver.call(this, evt, ...args); }; - // @ts-ignore - const originalHandleDrop = editMode.clipboardManager.constructor.prototype.handleDrop; - // @ts-ignore - editMode.clipboardManager.constructor.prototype.handleDrop = function (evt, ...args) { - const { draggable } = this.app.dragManager; - if (draggable && draggable.file instanceof TFolder && getFolderNote(this, draggable.file.path)) { - const folderNote = getFolderNote(this, draggable.file.path); - if (draggable?.type === 'folder' && draggable.file instanceof TFolder && folderNote) { + clipboardProto.handleDrop = function (evt: DragEvent, ...args: any[]) { + const dragManager = this.app.dragManager; + const draggable = dragManager?.draggable; + + if (draggable?.file instanceof TFolder) { + const folderNote = getFolderNote(plugin, draggable.file.path); + if (folderNote) { draggable.file = folderNote; draggable.type = 'file'; } } + return originalHandleDrop.call(this, evt, ...args); }; } + handleFileExplorerClick(evt: MouseEvent) { + const target = evt.target as HTMLElement; + if (evt.shiftKey) 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; + if (!folderTitleEl) return; + + const onlyClickedOnFolderTitle = !!target.closest('.nav-folder-title-content'); + if (!this.settings.stopWhitespaceCollapsing && !onlyClickedOnFolderTitle) return; + + // Ignore clicks on the collapse icon + if (target.closest('.collapse-icon')) return; + + const folderPath = folderTitleEl.getAttribute('data-path'); + if (!folderPath) return; + + const excludedFolder = getExcludedFolder(this, folderPath, true); + if (excludedFolder?.disableFolderNote) return; + + const folderNote = getFolderNote(this, folderPath); + if (!folderNote && (evt.altKey || Keymap.isModEvent(evt) === 'tab')) { + if ((this.settings.altKey && evt.altKey) || (this.settings.ctrlKey && Keymap.isModEvent(evt) === 'tab')) { + createFolderNote(this, folderPath, true, undefined, true); + addCSSClassToFileExplorerEl(folderPath, 'has-folder-note', false, this); + removeCSSClassFromFileExplorerEL(folderPath, 'has-not-folder-note', false, this); + return; + } + } + if (!(folderNote instanceof TFile)) return; + + if (this.settings.openWithCtrl && !evt.ctrlKey) return; + if (this.settings.openWithAlt && !evt.altKey) return; + + if (!this.settings.enableCollapsing) { + evt.preventDefault(); + evt.stopImmediatePropagation(); + } + + openFolderNote(this, folderNote, evt); + } handleOverviewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { const observer = new MutationObserver(() => { @@ -252,7 +341,7 @@ export default class FolderNotesPlugin extends Plugin { const attachmentsAreInRootFolder = attachmentFolderPath === './' || attachmentFolderPath === ''; const threshold = this.settings.storageLocation === 'insideFolder' ? 1 : 0; if (folder.children.length === 0) { - addCSSClassToTitleEL(folder.path, 'fn-empty-folder', this); + addCSSClassToFileExplorerEl(folder.path, 'fn-empty-folder', false, this); } if (folder.children.length === threshold) { @@ -265,7 +354,7 @@ export default class FolderNotesPlugin extends Plugin { const attachmentFolder = this.app.vault.getAbstractFileByPath(folderPath); if (attachmentFolder instanceof TFolder && folder.children.length <= threshold + 1) { if (!folder.collapsed) { - getEl(folder.path, this)?.click(); + getFileExplorerElement(folder.path, this)?.click(); } } return folder.children.length <= threshold + 1; @@ -276,13 +365,13 @@ export default class FolderNotesPlugin extends Plugin { return true; } - async changeName(folder: TFolder, name: string | null | undefined, replacePath: boolean, waitForCreate = false, count = 0) { - if (!name) name = folder.name; - let fileExplorerItem = getEl(folder.path, this); + async changeFolderNameInExplorer(folder: TFolder, newName: string | null | undefined, waitForCreate = false, count = 0) { + 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.changeName(folder, name, replacePath, waitForCreate, count + 1); + this.changeFolderNameInExplorer(folder, newName, waitForCreate, count + 1); return; } return; @@ -291,18 +380,26 @@ 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 = name; + fileExplorerItem.innerText = newName; fileExplorerItem.setAttribute('old-name', folder.name); } else { fileExplorerItem.innerText = folder.name; fileExplorerItem.removeAttribute('old-name'); } - if (replacePath) { - this.updateBreadcrumbs(); - } } - updateBreadcrumbs(remove?: boolean) { + async changeFolderNameInPath(folder: TFolder, newName: string | null | undefined, breadcrumb: HTMLElement) { + if (!newName) newName = folder.name; + + breadcrumb.textContent = folder.newName || folder.name; + breadcrumb.setAttribute('old-name', folder.name); + breadcrumb.setAttribute('data-path', folder.path); + } + + /** + * Updates all folder names in the path above the note editor + */ + updateAllBreadcrumbs(remove?: boolean) { 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); @@ -322,22 +419,13 @@ export default class FolderNotesPlugin extends Plugin { }); } - reloadHandlers() { - document.querySelectorAll('div.nav-folder-title-content') - .forEach((element: HTMLElement) => { - if (element.onclick) return; - element.onclick = (event: MouseEvent) => handleFolderClick(event, this); - }); - } - onunload() { - console.log('unloading folder notes plugin'); - this.observer.disconnect(); + unregisterFileExplorerObserver(); document.body.classList.remove('folder-notes-plugin'); document.body.classList.remove('folder-note-underline'); document.body.classList.remove('hide-folder-note'); document.body.classList.remove('fn-whitespace-stop-collapsing'); - if (this.activeFolderDom) { this.activeFolderDom.removeClass('is-active'); } + removeActiveFolder(this); if (this.fmtpHandler) { this.fmtpHandler.deleteEvent(); } @@ -370,7 +458,7 @@ export default class FolderNotesPlugin extends Plugin { await this.saveData(this.settings); // cleanup any css if we need too if ((!this.settingsOpened || reloadStyles === true) && reloadStyles !== false) { - loadFileClasses(true, this); + refreshAllFolderStyles(true, this); } } diff --git a/src/obsidian-folder-overview b/src/obsidian-folder-overview index e512c58..192ecd3 160000 --- a/src/obsidian-folder-overview +++ b/src/obsidian-folder-overview @@ -1 +1 @@ -Subproject commit e512c581db4234fc643f9400f39eec3f51a627c2 +Subproject commit 192ecd3cd7ca983db1a9d4a97ec90333bdbc8fd8 diff --git a/src/settings/FileExplorerSettings.ts b/src/settings/FileExplorerSettings.ts index b32475a..e408014 100644 --- a/src/settings/FileExplorerSettings.ts +++ b/src/settings/FileExplorerSettings.ts @@ -5,7 +5,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Hide folder note') - .setDesc('Hide the folder note in the file explorer') + .setDesc('Hide the folder note file from appearing in the file explorer') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.hideFolderNote) @@ -22,8 +22,8 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { ); const setting2 = new Setting(containerEl) - .setName('Don\'t open folder notes by clicking on the name (on mobile)') - .setDesc('Folder notes don\'t open when clicking on the name of the folder (on mobile)') + .setName('Disable click-to-open folder note on mobile') + .setDesc('Prevents folder notes from opening when tapping the folder name or surrounding area on mobile devices. They can now only be opened via the context menu or a command.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.disableOpenFolderNoteOnClick) @@ -37,8 +37,8 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { setting2.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; new Setting(containerEl) - .setName('Only open folder notes through the name') - .setDesc('Only open folder notes in the file explorer by clicking on the folder name') + .setName('Open folder notes by only clicking directly on the folder name') + .setDesc('Only allow folder notes to open when clicking directly on the folder name in the file explorer') .addToggle((toggle) => toggle .setValue(!settingsTab.plugin.settings.stopWhitespaceCollapsing) @@ -55,7 +55,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { const disableSetting = new Setting(containerEl); disableSetting.setName('Disable folder collapsing'); - disableSetting.setDesc('Disable the ability to collapse folders by clicking exactly on the folder name'); + disableSetting.setDesc('When enabled, folders in the file explorer will only collapse when clicking the collapse icon next to the folder name, not when clicking near a folder name when it has a folder note.'); disableSetting.addToggle((toggle) => toggle .setValue(!settingsTab.plugin.settings.enableCollapsing) @@ -82,8 +82,8 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { if (settingsTab.plugin.settings.frontMatterTitle.enabled) { new Setting(containerEl) - .setName('Change folder name in the file explorer') - .setDesc('Automatically rename a folder name in the file explorer when the folder note is renamed') + .setName('Auto update folder name in the file explorer (front matter title plugin only)') + .setDesc('Automatically update the folder name in the file explorer when the front matter title plugin is enabled and the title for a folder note is changed in the front matter. This will not change the file name, only the displayed name in the file explorer.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.frontMatterTitle.explorer) @@ -91,7 +91,7 @@ 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?.handleRename({ id: '', result: false, path: file.path }, false); + settingsTab.plugin.fmtpHandler?.fmptUpdateFileName({ id: '', result: false, path: file.path, pathOnly: false }, false); }); }) ); @@ -101,7 +101,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Highlight folder in the file explorer') - .setDesc('Highlight the folder name in the file explorer when you click on a folder that has a folder note') + .setDesc('Highlight the folder in the file explorer when it has a folder note and the folder note is open in the editor') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.highlightFolder) @@ -154,7 +154,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { if (settingsTab.plugin.settings.hideCollapsingIcon) { new Setting(containerEl) - .setName('Hide collapse icon also when the attachment folder is in the same folder') + .setName('Hide collapse icon also when only the attachment folder is in the same folder') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.ignoreAttachmentFolder) @@ -184,7 +184,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Bold the name of folder notes') - .setDesc('Make the folder name bold in the file explorer') + .setDesc('Make the folder name bold in the file explorer when it has a folder note') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.boldName) @@ -201,7 +201,7 @@ export async function renderFileExplorer(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Cursive the name of folder notes') - .setDesc('Make the folder name cursive in the file explorer') + .setDesc('Make the folder name cursive in the file explorer when it has a folder note') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.cursiveName) diff --git a/src/settings/GeneralSettings.ts b/src/settings/GeneralSettings.ts index 2e76bde..50b1ac7 100644 --- a/src/settings/GeneralSettings.ts +++ b/src/settings/GeneralSettings.ts @@ -5,7 +5,7 @@ import AddSupportedFileModal from '../modals/AddSupportedFileType'; import { FrontMatterTitlePluginHandler } from '../events/FrontMatterTitle'; import ConfirmationModal from './modals/CreateFnForEveryFolder'; import { TemplateSuggest } from '../suggesters/TemplateSuggester'; -import { loadFileClasses } from '../functions/styleFunctions'; +import { refreshAllFolderStyles } from '../functions/styleFunctions'; import BackupWarningModal from './modals/BackupWarning'; import RenameFolderNotesModal from './modals/RenameFns'; @@ -90,11 +90,17 @@ export async function renderGeneral(settingsTab: SettingsTab) { dropdown.addOption('.' + type, type); } }); + if (!settingsTab.plugin.settings.supportedFileTypes.includes(settingsTab.plugin.settings.folderNoteType.replace('.', '')) && settingsTab.plugin.settings.folderNoteType !== '.ask') { settingsTab.plugin.settings.folderNoteType = '.md'; settingsTab.plugin.saveSettings(); } - const defaultType = settingsTab.plugin.settings.folderNoteType.startsWith('.') ? settingsTab.plugin.settings.folderNoteType : '.' + settingsTab.plugin.settings.folderNoteType; + + 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; + } dropdown .setValue(defaultType) @@ -116,6 +122,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { 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')) { @@ -186,7 +193,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { settingsTab.plugin.settings.storageLocation = value; await settingsTab.plugin.saveSettings(); settingsTab.display(); - loadFileClasses(undefined, settingsTab.plugin); + refreshAllFolderStyles(undefined, settingsTab.plugin); }) ) .addButton((button) => @@ -238,6 +245,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { ); } if (Platform.isDesktopApp) { + settingsTab.settingsPage.createEl('h3', { text: 'Keyboard Shortcuts' }); + new Setting(containerEl) .setName('Key for creating folder note') .setDesc('The key combination to create a folder note') @@ -285,6 +294,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { }); } + settingsTab.settingsPage.createEl('h3', { text: 'Folder note behavior' }); + new Setting(containerEl) .setName('Confirm folder note deletion') .setDesc('Ask for confirmation before deleting a folder note') @@ -330,56 +341,20 @@ export async function renderGeneral(settingsTab: SettingsTab) { setting3.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; } - const desc1 = document.createDocumentFragment(); - - const link = document.createElement('a'); - link.href = 'https://github.com/snezhig/obsidian-front-matter-title'; - link.textContent = 'front matter title plugin'; - link.target = '_blank'; - - desc1.append( - 'Allows you to use the ', - link, - ' with folder notes. It allows you to set the folder name to some name you set in the front matter.', - ); - - new Setting(containerEl) - .setName('Enable front matter title plugin integration') - .setDesc(desc1) - .addToggle((toggle) => - toggle - .setValue(settingsTab.plugin.settings.frontMatterTitle.enabled) - .onChange(async (value) => { - settingsTab.plugin.settings.frontMatterTitle.enabled = value; - await settingsTab.plugin.saveSettings(); - if (value) { - settingsTab.plugin.fmtpHandler = new FrontMatterTitlePluginHandler(settingsTab.plugin); - } else { - if (settingsTab.plugin.fmtpHandler) { - settingsTab.plugin.updateBreadcrumbs(true); - } - settingsTab.plugin.app.vault.getFiles().forEach((file) => { - settingsTab.plugin.fmtpHandler?.handleRename({ id: '', result: false, path: file.path }, false); - }); - settingsTab.plugin.fmtpHandler?.deleteEvent(); - settingsTab.plugin.fmtpHandler = null; - } - settingsTab.display(); - }) - ); - - new Setting(containerEl) - .setName('Create folder note for every folder') - .setDesc('Create a folder note for every folder in the vault') - .addButton((cb) => { - cb.setIcon('plus'); - cb.setTooltip('Create folder notes'); - cb.onClick(async () => { - new ConfirmationModal(settingsTab.app, settingsTab.plugin).open(); - }); - }); - - settingsTab.settingsPage.createEl('h3', { text: 'Automation settings' }); + if (settingsTab.plugin.settings.openInNewTab) { + new Setting(containerEl) + .setName('Focus existing tab instead of creating a new one') + .setDesc('If a folder note is already open in a tab, focus that tab instead of creating a new one.') + .addToggle((toggle) => + toggle + .setValue(settingsTab.plugin.settings.focusExistingTab) + .onChange(async (value) => { + settingsTab.plugin.settings.focusExistingTab = value; + await settingsTab.plugin.saveSettings(); + settingsTab.display(); + }) + ); + } new Setting(containerEl) .setName('Sync folder name') @@ -394,9 +369,22 @@ export async function renderGeneral(settingsTab: SettingsTab) { }) ); + settingsTab.settingsPage.createEl('h4', { text: 'Automation settings' }); + new Setting(containerEl) - .setName('Automatically create folder notes') - .setDesc('Automatically create a folder note when a new folder is created') + .setName('Create folder notes for all folders') + .setDesc('Generate folder notes for every folder in the vault.') + .addButton((cb) => { + cb.setIcon('plus'); + cb.setTooltip('Create folder notes'); + cb.onClick(async () => { + new ConfirmationModal(settingsTab.app, settingsTab.plugin).open(); + }); + }); + + new Setting(containerEl) + .setName('Auto-create on folder creation') + .setDesc('Automatically create a folder note whenever a new folder is added.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.autoCreate) @@ -409,8 +397,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { if (settingsTab.plugin.settings.autoCreate) { new Setting(containerEl) - .setName('Open folder note after creating') - .setDesc('Automatically open the folder note after automatically creating it') + .setName('Auto-open after creation') + .setDesc('Open the folder note immediately after it’s created automatically.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.autoCreateFocusFiles) @@ -422,8 +410,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { ); new Setting(containerEl) - .setName('Auto create folder note for attachment folder') - .setDesc('Automatically create a folder note for the attachment folder when you attach a file to a note and the attachment folder gets created') + .setName('Auto-create for attachment folders') + .setDesc('Also automatically create folder notes for attachment folders (e.g., "Attachments", "Media", etc.).') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.autoCreateForAttachmentFolder) @@ -436,8 +424,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { } new Setting(containerEl) - .setName('Automatically create folder note when you create a note') - .setDesc('Automatically create a folder note when a note is created. It has to be a file type that you selected in the supported file types') + .setName('Auto-create when creating notes') + .setDesc('Automatically create a folder note when a regular note is created inside a folder. Works for supported file types only.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.autoCreateForFiles) @@ -448,11 +436,53 @@ export async function renderGeneral(settingsTab: SettingsTab) { }) ); - settingsTab.settingsPage.createEl('h3', { text: 'Settings tab' }); + settingsTab.settingsPage.createEl('h3', { text: 'Integration & Compatibility' }); + + const desc1 = document.createDocumentFragment(); + + const link = document.createElement('a'); + link.href = 'https://github.com/snezhig/obsidian-front-matter-title'; + link.textContent = 'front matter title plugin'; + link.target = '_blank'; + + desc1.append( + 'Allows you to use the ', + link, + ' with folder notes. It allows you to set the folder name to some name you set in the front matter.', + ); + + const fmtpSetting = new Setting(containerEl) + .setName('Enable front matter title plugin integration') + .setDesc(desc1) + .addToggle((toggle) => + toggle + .setValue(settingsTab.plugin.settings.frontMatterTitle.enabled) + .onChange(async (value) => { + settingsTab.plugin.settings.frontMatterTitle.enabled = value; + await settingsTab.plugin.saveSettings(); + if (value) { + 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?.deleteEvent(); + settingsTab.plugin.fmtpHandler = null; + } + settingsTab.display(); + }) + ); + fmtpSetting.infoEl.appendText('Requires a restart to take effect'); + fmtpSetting.infoEl.style.color = settingsTab.app.vault.getConfig('accentColor') as string || '#7d5bed'; + + settingsTab.settingsPage.createEl('h3', { text: 'Session & Persistence' }); new Setting(containerEl) .setName('Persist tab after restart') - .setDesc('Reopen the same settings tab after restarting Obsidian') + .setDesc('Restore the same settings tab after restarting Obsidian.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.persistentSettingsTab.afterRestart) @@ -465,7 +495,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Persist tab during session only') - .setDesc('Keep the same settings tab while Obsidian is open, but reset after restart or tab reload') + .setDesc('Keep the current settings tab open during the session, but reset it after a restart or reload.') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.persistentSettingsTab.afterChangingTab) diff --git a/src/settings/PathSettings.ts b/src/settings/PathSettings.ts index 9916ff5..7bf3e14 100644 --- a/src/settings/PathSettings.ts +++ b/src/settings/PathSettings.ts @@ -39,22 +39,24 @@ export async function renderPath(settingsTab: SettingsTab) { }) ); - new Setting(containerEl) - .setName('Change folder name in the path') - .setDesc('Automatically rename a folder name in the path above a note when the folder note is renamed') - .addToggle((toggle) => - toggle - .setValue(settingsTab.plugin.settings.frontMatterTitle.path) - .onChange(async (value) => { - settingsTab.plugin.settings.frontMatterTitle.path = value; - await settingsTab.plugin.saveSettings(); - if (value) { - settingsTab.plugin.updateBreadcrumbs(); - } else { - settingsTab.plugin.updateBreadcrumbs(true); - } - }) - ); + if (settingsTab.plugin.settings.frontMatterTitle.enabled) { + new Setting(containerEl) + .setName('Auto update folder name in the path (front matter title plugin only)') + .setDesc('Automatically update the folder name in the path when the front matter title plugin is enabled and the title for a folder note is changed in the front matter. This will not change the file name, only the displayed name in the path.') + .addToggle((toggle) => + toggle + .setValue(settingsTab.plugin.settings.frontMatterTitle.path) + .onChange(async (value) => { + settingsTab.plugin.settings.frontMatterTitle.path = value; + await settingsTab.plugin.saveSettings(); + if (value) { + settingsTab.plugin.updateAllBreadcrumbs(); + } else { + settingsTab.plugin.updateAllBreadcrumbs(true); + } + }) + ); + } settingsTab.settingsPage.createEl('h3', { text: 'Style settings' }); @@ -77,7 +79,7 @@ export async function renderPath(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Bold folders in the path') - .setDesc('Make the folder name bold in the path above a note') + .setDesc('Make the folder name bold in the path above a note when it has a folder note') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.boldNameInPath) @@ -94,7 +96,7 @@ export async function renderPath(settingsTab: SettingsTab) { new Setting(containerEl) .setName('Cursive the name of folder notes in the path') - .setDesc('Make the folder name cursive in the path above a note') + .setDesc('Make the folder name cursive in the path above a note when it has a folder note') .addToggle((toggle) => toggle .setValue(settingsTab.plugin.settings.cursiveNameInPath) diff --git a/src/settings/SettingsTab.ts b/src/settings/SettingsTab.ts index 3f27d1e..4cea682 100644 --- a/src/settings/SettingsTab.ts +++ b/src/settings/SettingsTab.ts @@ -33,6 +33,7 @@ export interface FolderNotesSettings { underlineFolderInPath: boolean; openFolderNoteOnClickInPath: boolean; openInNewTab: boolean; + focusExistingTab: boolean; oldFolderNoteName: string | undefined; folderNoteName: string; newFolderNoteName: string; @@ -95,6 +96,7 @@ export const DEFAULT_SETTINGS: FolderNotesSettings = { underlineFolderInPath: true, openFolderNoteOnClickInPath: true, openInNewTab: false, + focusExistingTab: false, oldFolderNoteName: undefined, folderNoteName: '{{folder_name}}', folderNoteType: '.md', @@ -154,7 +156,7 @@ export const DEFAULT_SETTINGS: FolderNotesSettings = { string: '', hideInSettings: false, detached: false, - hideNote: false, + showFolderNote: false, }, excludePatternDefaultSettings: { type: 'pattern', @@ -170,7 +172,7 @@ export const DEFAULT_SETTINGS: FolderNotesSettings = { string: '', hideInSettings: false, detached: false, - hideNote: false, + showFolderNote: false, }, hideCollapsingIcon: false, hideCollapsingIconForEmptyFolders: false, diff --git a/src/settings/modals/CreateFnForEveryFolder.ts b/src/settings/modals/CreateFnForEveryFolder.ts index 86583c8..c3e719a 100644 --- a/src/settings/modals/CreateFnForEveryFolder.ts +++ b/src/settings/modals/CreateFnForEveryFolder.ts @@ -65,7 +65,7 @@ export default class ConfirmationModal extends Modal { const folders = this.app.vault.getAllLoadedFiles().filter((file) => file.parent instanceof TFolder); for (const folder of folders) { if (folder instanceof TFolder) { - const excludedFolder = await 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); diff --git a/styles.css b/styles.css index a7a0f85..86b8b4f 100644 --- a/styles.css +++ b/styles.css @@ -1,36 +1,26 @@ +/* ========================================================================== + General States & Utilities + ========================================================================== */ - -.fn-whitespace-stop-collapsing .nav-folder-title-content { - flex-grow: 1 !important; - display: flex; - padding-bottom: 4px !important; - padding-top: 4px !important; +.hide, +.hide-folder .folder-name, +.hide-folder-note .is-folder-note { + display: none; } -.version-1-7-2.fn-whitespace-stop-collapsing .nav-files-container .collapse-icon { - padding-top: 4px !important; +.pointer-cursor, +.has-folder-note .nav-folder-title-content:hover, +.has-folder-note.view-header-breadcrumb:hover, +.nav-folder-collapse-indicator:hover, +.fn-delete-confirmation-modal-buttons span:hover, +.fn-delete-confirmation-modal-buttons input:hover { + cursor: pointer !important; } -.mod-rtl .fn-whitespace-stop-collapsing .nav-folder-title-content { - padding-left: 8px !important; -} -.fn-whitespace-stop-collapsing .nav-folder-title { - padding-bottom: 0 !important; - padding-top: 0 !important; -} - -body:not(.mod-rtl).fn-whitespace-stop-collapsing .nav-folder-title { - padding-right: 0 !important; -} - -.mod-rtl.fn-whitespace-stop-collapsing .nav-folder-title { - padding-left: 0 !important; -} - -.fn-whitespace-stop-collapsing .nav-folder-collapse-indicator { - margin-top: 4px !important; -} +/* ========================================================================== + Tree Items + ========================================================================== */ body:not(.is-grabbing) .tree-item-self.fn-is-active:hover, body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { @@ -39,36 +29,17 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { font-weight: var(--nav-item-weight-active); } -.has-folder-note .nav-folder-title-content:hover, -.has-folder-note.view-header-breadcrumb:hover { - cursor: pointer; -} -.hide { - display: none; -} - -.hide-folder-note .is-folder-note { - display: none; -} - -.hide-folder .folder-name { - display: none; -} - -.nav-folder-collapse-indicator:hover { - cursor: pointer; -} +/* ========================================================================== + Exclude Folder Settings + ========================================================================== */ .fn-excluded-folder-heading { margin-top: 0 !important; border-top: 1px solid var(--background-modifier-border); } -.add-exclude-folder-item { - padding-bottom: 0 !important; -} - +.add-exclude-folder-item, .fn-exclude-folder-list { padding-bottom: 0 !important; } @@ -91,7 +62,10 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { width: 100%; } -/* Modal styles */ + +/* ========================================================================== + Modal Styles + ========================================================================== */ .fn-backup-warning-modal .fn-modal-button-container { display: flex; @@ -103,9 +77,13 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { padding-bottom: 0; } +.fn-confirmation-modal .setting-item { + border-top: 0 !important; + padding-top: 0 !important; +} + :not(.is-phone) .fn-confirmation-modal-button { margin-right: 0.7rem; - } :not(.is-phone) .fn-delete-confirmation-modal-buttons { @@ -114,11 +92,6 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { margin-top: 10px; } -.fn-delete-confirmation-modal-buttons span:hover, -.fn-delete-confirmation-modal-buttons input:hover { - cursor: pointer; -} - :not(.is-phone) .fn-delete-confirmation-modal-buttons .fn-confirmation-modal-button { margin-left: auto; } @@ -137,22 +110,17 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { margin-top: 10px; } -/* Folder overview */ + +/* ========================================================================== + Folder Overview + ========================================================================== */ .folder-overview-container.fv-remove-edit-button .folder-overview-edit-button { display: none; } -.fn-folder-overview-collapse-icon { - display: block !important; -} - -.fn-has-no-files .collapse-icon { - display: none !important; -} - .folder-overview-list { - margin-top: 0 !important; + margin-top: 0 !important; margin-bottom: 0 !important; padding-bottom: 1.200 !important; padding-top: 1.200 !important; @@ -162,7 +130,6 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { display: flex; } - .folder-overview-list::marker { color: var(--text-faint); } @@ -177,12 +144,17 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { grid-template-columns: repeat(3, 1fr); } +.folder-overview-grid-item { + flex: 1 1 auto; + margin: 0 1.2rem 1.2rem 0; +} + .folder-overview-grid-item-article article { - padding: 15px; - flex: 1; display: flex; flex-direction: column; justify-content: space-between; + padding: 15px; + flex: 1; } .folder-overview-grid-item-article a { @@ -204,52 +176,9 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { } -.folder-overview-grid-item { - flex: 1 1 auto; - margin-right: 1.200rem; - margin-bottom: 1.200rem; -} - -.fn-confirmation-modal .setting-item { - border-top: 0 !important; - padding-top: 0 !important; -} - -.pointer-cursor { - cursor: pointer !important; -} - - -/* Setting tab style */ -.fn-settings-tab-bar { - display: flex; - flex-direction: row; - padding-bottom: 1rem; -} - -.fn-settings-tab { - display: flex; - flex-direction: row; - align-items: center; - gap: var(--size-4-2); - padding: 10px; - border: 1px solid var(--background-modifier-border) -} - -.fn-settings-tab-active { - background-color: var(--color-accent); - color: var(--text-on-accent); -} - -.fn-settings-tab-name { - font-weight: bold; -} - -.fn-settings-tab-icon { - display: flex; -} - -/* File explorer & path styles */ +/* ========================================================================== + File Explorer & Path Styling + ========================================================================== */ .folder-note-underline .has-folder-note .nav-folder-title-content { text-decoration-line: underline; @@ -275,14 +204,57 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { font-style: italic; } -.fn-hide-collapse-icon .has-folder-note.only-has-folder-note .tree-item-icon { + +/* Collapse Icon Handling */ + +.fn-folder-overview-collapse-icon { + display: block !important; +} + +.fn-has-no-files .collapse-icon, +.fn-hide-collapse-icon .has-folder-note.only-has-folder-note .tree-item-icon, +.fn-hide-empty-collapse-icon .fn-empty-folder .tree-item-icon, +.only-has-folder-note:not(.is-collapsed):not(.show-folder-note-in-explorer) > .nav-folder-children { display: none; } -.fn-hide-empty-collapse-icon .fn-empty-folder .tree-item-icon { - display: none; + +/* ========================================================================== + Settings Tabs + ========================================================================== */ + +.fn-settings-tab-bar { + display: flex; + flex-direction: row; + padding-bottom: 1rem; } +.fn-settings-tab { + display: flex; + flex-direction: row; + align-items: center; + gap: var(--size-4-2); + padding: 10px; + border: 1px solid var(--background-modifier-border); +} + +.fn-settings-tab-active { + background-color: var(--color-accent); + color: var(--text-on-accent); +} + +.fn-settings-tab-name { + font-weight: bold; +} + +.fn-settings-tab-icon { + display: flex; +} + + +/* ========================================================================== + Suggestion Container + ========================================================================== */ .fn-suggestion-container { position: absolute; @@ -299,12 +271,11 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { } -/* Mobile style overrides */ -.is-phone .fn-overview-folder-path .setting-item-control{ - display: block; -} +/* ========================================================================== + Whitelist Folder Input (Desktop & Mobile) + ========================================================================== */ -/* Default desktop layout */ +/* Default Desktop Layout */ .fn-whitelist-folder-input-container { display: flex; justify-content: space-between; @@ -316,9 +287,9 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { .fn-whitelist-folder-input-container input { flex-grow: 1; width: auto; - box-sizing: border-box; margin-right: 8px; - height: 40px; /* Ensure consistent height */ + height: 40px; + box-sizing: border-box; } .fn-whitelist-folder-buttons { @@ -330,12 +301,12 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { flex-shrink: 0; } -/* Mobile-specific layout */ +/* Mobile Overrides */ @media (max-width: 768px) { .fn-whitelist-folder-input-container { display: block; width: 100%; - text-align: center; /* Center elements vertically when stacked */ + text-align: center; } .fn-whitelist-folder-input-container input { @@ -350,4 +321,8 @@ body:not(.disable-folder-highlight) .tree-item-self.fn-is-active { align-items: center; width: 100%; } -} + + .is-phone .fn-overview-folder-path .setting-item-control { + display: block; + } +} \ No newline at end of file