From fe64c66b603a50d4ccde331502feab52b8d090a0 Mon Sep 17 00:00:00 2001 From: Lost Paul <70213368+LostPaul@users.noreply.github.com> Date: Fri, 30 May 2025 16:29:00 +0200 Subject: [PATCH 1/4] Improve performance of mutation observer --- src/events/MutationObserver.ts | 226 +++++++++++++++++++++------------ src/main.ts | 15 +-- 2 files changed, 154 insertions(+), 87 deletions(-) diff --git a/src/events/MutationObserver.ts b/src/events/MutationObserver.ts index 4ab0a5b..9efce2e 100644 --- a/src/events/MutationObserver.ts +++ b/src/events/MutationObserver.ts @@ -1,96 +1,121 @@ +import { TFile, 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 { getExcludedFolder } from 'src/ExcludeFolders/functions/folderFunctions'; import { applyCSSClassesToFolder } 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); + }); - 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(); - } - }); - - 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 }); - }); - } - }); + // Also listen for file-open events (once) + plugin.registerEvent( + plugin.app.workspace.on('file-open', (file) => { + if (file instanceof TFile) { + scheduleIdle(() => updateBreadcrumbs(file, plugin), { timeout: 1000 }); } - }); + }) + ); +} + +export function unregisterFileExplorerObserver() { + if (fileExplorerMutationObserver) { + fileExplorerMutationObserver.disconnect(); + fileExplorerMutationObserver = null; + } +} + +function initializeFolderNoteFeatures(plugin: FolderNotesPlugin) { + const explorer = document.querySelector('.workspace-leaf-content[data-type="file-explorer"] .nav-files-container'); + if (!explorer) return; + + initializeAllFolderTitles(explorer, plugin); + observeFolderTitleMutations(explorer, plugin); + + // Add breadcrumb click listener + explorer.addEventListener('click', (e) => handleBreadcrumbClick((e as MouseEvent), plugin), true); +} + +/** + * 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(container: Element, 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; + processAddedFolderNodes(node, plugin); + } + } + }); + + fileExplorerMutationObserver.observe(container, { childList: true, subtree: true }); +} + +function initializeAllFolderTitles(container: Element, plugin: FolderNotesPlugin) { + const allTitles = container.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 processAddedFolderNodes(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 initializeFolderTitle(folderTitle: HTMLElement, plugin: any) { - if (folderTitle.onclick) return; +async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlugin, folderPath: string) { + if (folderTitle.dataset.initialized === 'true') return; + if (!folderPath) return; if (Platform.isMobile && plugin.settings.disableOpenFolderNoteOnClick) return; - const folderPath = folderTitle.parentElement?.getAttribute('data-path') || ''; - + folderTitle.dataset.initialized = 'true'; await applyCSSClassesToFolder(folderPath, plugin); - // Handle middle click (auxclick) folderTitle.addEventListener('auxclick', (event: MouseEvent) => { - if (event.button === 1) { - handleFolderClick(event, plugin); - } + if (event.button === 1) handleFolderClick(event, plugin); }, { capture: true }); folderTitle.onclick = (event: MouseEvent) => handleFolderClick(event, plugin); @@ -102,17 +127,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; }); @@ -123,3 +147,47 @@ async function initializeFolderTitle(folderTitle: HTMLElement, plugin: any) { plugin.hoverLinkTriggered = false; }); } + +async function updateBreadcrumbs(file: TFile, plugin: FolderNotesPlugin) { + const headers = document.querySelectorAll('span.view-header-breadcrumb'); + headers.forEach(async (breadcrumb: HTMLElement) => { + let path = ''; + const allCrumbs = breadcrumb.parentElement?.querySelectorAll('span.view-header-breadcrumb'); + if (!allCrumbs) return; + + for (const crumb of Array.from(allCrumbs)) { + path += crumb.getAttribute('old-name') ?? (crumb as HTMLElement).innerText.trim(); + path += '/'; + const folderPath = path.slice(0, -1); + const folder = plugin.fmtpHandler?.modifiedFolders.get(folderPath); + if (folder && plugin.settings.frontMatterTitle.path && plugin.settings.frontMatterTitle.enabled) { + crumb.setAttribute('old-name', folder.name || ''); + (crumb as HTMLElement).innerText = folder.newName || ''; + } + const excludedFolder = await getExcludedFolder(plugin, folderPath, true); + if (excludedFolder?.disableFolderNote) return; + const folderNote = getFolderNote(plugin, folderPath); + if (folderNote) crumb.classList.add('has-folder-note'); + } + + breadcrumb.parentElement?.setAttribute('data-path', path.slice(0, -1)); + }); +} + +function handleBreadcrumbClick(event: MouseEvent, plugin: FolderNotesPlugin) { + const target = event.target as HTMLElement; + const breadcrumb = target.closest('span.view-header-breadcrumb') as HTMLElement; + if (!breadcrumb || breadcrumb.onclick) return; + + breadcrumb.addEventListener('click', (e) => { + handleViewHeaderClick(e, plugin); + }, { capture: true }); +} + +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/main.ts b/src/main.ts index c538e18..39c604b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { DEFAULT_SETTINGS, FolderNotesSettings, SettingsTab } from './settings/S 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 { handleCreate } from './events/handleCreate'; @@ -67,12 +67,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'; @@ -144,6 +142,7 @@ export default class FolderNotesPlugin extends Plugin { } onLayoutReady() { + registerFileExplorerObserver(this); this.registerView(FOLDER_OVERVIEW_VIEW, (leaf: WorkspaceLeaf) => { return new FolderOverviewView(leaf, this); }); @@ -332,7 +331,7 @@ export default class FolderNotesPlugin extends Plugin { 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'); From 84d8c608fd5d3d2d994c46dc4a88be00ebe4ba87 Mon Sep 17 00:00:00 2001 From: Lost Paul <70213368+LostPaul@users.noreply.github.com> Date: Sun, 1 Jun 2025 13:11:36 +0200 Subject: [PATCH 2/4] Remove unnecessary awaits --- src/Commands.ts | 2 +- src/events/MutationObserver.ts | 10 ++++++---- src/events/handleClick.ts | 4 ++-- src/events/handleCreate.ts | 4 ++-- src/events/handleRename.ts | 6 +++--- src/functions/folderNoteFunctions.ts | 2 +- src/functions/styleFunctions.ts | 4 ++-- src/main.ts | 2 +- src/settings/modals/CreateFnForEveryFolder.ts | 2 +- 9 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/Commands.ts b/src/Commands.ts index b0f9e2a..f11101a 100644 --- a/src/Commands.ts +++ b/src/Commands.ts @@ -328,7 +328,7 @@ 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) { // I'm not sure if I'm ever going to add this because of the possibility that a folder got more than one excluded diff --git a/src/events/MutationObserver.ts b/src/events/MutationObserver.ts index 9efce2e..2af215e 100644 --- a/src/events/MutationObserver.ts +++ b/src/events/MutationObserver.ts @@ -24,7 +24,7 @@ export function registerFileExplorerObserver(plugin: FolderNotesPlugin) { plugin.registerEvent( plugin.app.workspace.on('file-open', (file) => { if (file instanceof TFile) { - scheduleIdle(() => updateBreadcrumbs(file, plugin), { timeout: 1000 }); + scheduleIdle(() => updateBreadcrumbs(plugin), { timeout: 1000 }); } }) ); @@ -44,7 +44,6 @@ function initializeFolderNoteFeatures(plugin: FolderNotesPlugin) { initializeAllFolderTitles(explorer, plugin); observeFolderTitleMutations(explorer, plugin); - // Add breadcrumb click listener explorer.addEventListener('click', (e) => handleBreadcrumbClick((e as MouseEvent), plugin), true); } @@ -148,7 +147,7 @@ async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlu }); } -async function updateBreadcrumbs(file: TFile, plugin: FolderNotesPlugin) { +async function updateBreadcrumbs(plugin: FolderNotesPlugin) { const headers = document.querySelectorAll('span.view-header-breadcrumb'); headers.forEach(async (breadcrumb: HTMLElement) => { let path = ''; @@ -164,7 +163,7 @@ async function updateBreadcrumbs(file: TFile, plugin: FolderNotesPlugin) { crumb.setAttribute('old-name', folder.name || ''); (crumb as HTMLElement).innerText = folder.newName || ''; } - const excludedFolder = await getExcludedFolder(plugin, folderPath, true); + const excludedFolder = getExcludedFolder(plugin, folderPath, true); if (excludedFolder?.disableFolderNote) return; const folderNote = getFolderNote(plugin, folderPath); if (folderNote) crumb.classList.add('has-folder-note'); @@ -184,6 +183,9 @@ function handleBreadcrumbClick(event: MouseEvent, plugin: FolderNotesPlugin) { }, { capture: 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); diff --git a/src/events/handleClick.ts b/src/events/handleClick.ts index 5022812..3cb9d1f 100644 --- a/src/events/handleClick.ts +++ b/src/events/handleClick.ts @@ -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(); @@ -61,7 +61,7 @@ export async function handleFolderClick(event: MouseEvent, plugin: FolderNotesPl const folderPath = event.target.parentElement?.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(); diff --git a/src/events/handleCreate.ts b/src/events/handleCreate.ts index 072ff85..72dbb09 100644 --- a/src/events/handleCreate.ts +++ b/src/events/handleCreate.ts @@ -31,7 +31,7 @@ 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); + const detachedFolder = getExcludedFolder(plugin, folder.path, true); if (detachedFolder) { return; } const folderNote = getFolderNote(plugin, folder.path); @@ -59,7 +59,7 @@ 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); diff --git a/src/events/handleRename.ts b/src/events/handleRename.ts index 5e4c19c..ba9e1b1 100644 --- a/src/events/handleRename.ts +++ b/src/events/handleRename.ts @@ -55,7 +55,7 @@ export async function handleFileMove(file: TFile, oldPath: string, plugin: Folde 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); // file has been moved into position where it can be a folder note! @@ -101,7 +101,7 @@ export async function handleFolderRename(file: TFolder, oldPath: string, plugin: 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); } @@ -138,7 +138,7 @@ 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); diff --git a/src/functions/folderNoteFunctions.ts b/src/functions/folderNoteFunctions.ts index a507577..85fe9eb 100644 --- a/src/functions/folderNoteFunctions.ts +++ b/src/functions/folderNoteFunctions.ts @@ -203,7 +203,7 @@ export async function turnIntoFolderNote(plugin: FolderNotesPlugin, file: TFile, } 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; diff --git a/src/functions/styleFunctions.ts b/src/functions/styleFunctions.ts index a9a8cb7..d5a27f2 100644 --- a/src/functions/styleFunctions.ts +++ b/src/functions/styleFunctions.ts @@ -18,7 +18,7 @@ export function loadFileClasses(forceReload = false, plugin: FolderNotesPlugin) return; } - const excludedFolder = await getExcludedFolder(plugin, file.path, true); + const excludedFolder = getExcludedFolder(plugin, file.path, true); // cleanup after ourselves // Incase settings have changed if (excludedFolder?.disableFolderNote) { @@ -48,7 +48,7 @@ export async function applyCSSClassesToFolder(folderPath: string, plugin: Folder 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); diff --git a/src/main.ts b/src/main.ts index 39c604b..f01c02a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -119,7 +119,7 @@ export default class FolderNotesPlugin extends Plugin { 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; } 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); From 095e275ef8b78f06ea2b50d38a84b7c3c6e85d6b Mon Sep 17 00:00:00 2001 From: Lost Paul <70213368+LostPaul@users.noreply.github.com> Date: Tue, 3 Jun 2025 21:38:57 +0200 Subject: [PATCH 3/4] Fix fmtp changing names in the explorer --- src/events/FrontMatterTitle.ts | 30 ++++++++++-- src/events/MutationObserver.ts | 85 +++++++++++++++++---------------- src/settings/GeneralSettings.ts | 4 +- 3 files changed, 74 insertions(+), 45 deletions(-) diff --git a/src/events/FrontMatterTitle.ts b/src/events/FrontMatterTitle.ts index 8a66190..a8ae594 100644 --- a/src/events/FrontMatterTitle.ts +++ b/src/events/FrontMatterTitle.ts @@ -41,9 +41,9 @@ 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.app.vault.getFiles().forEach((file) => { + // this.handleRename({ id: '', result: false, path: file.path }, false); + // }); this.plugin.updateBreadcrumbs(); })(); } @@ -84,4 +84,28 @@ export class FrontMatterTitlePluginHandler { } } + + async handleRenameFolder(data: { + id: string; + result: boolean; + path: string; + }, isEvent: 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 (isEvent) { + this.plugin.changeName(folder, newName, true); + } else { + this.plugin.changeName(folder, newName, false); + } + folder.newName = newName; + this.modifiedFolders.set(folder.path, folder); + } } diff --git a/src/events/MutationObserver.ts b/src/events/MutationObserver.ts index 2af215e..f01589c 100644 --- a/src/events/MutationObserver.ts +++ b/src/events/MutationObserver.ts @@ -1,4 +1,4 @@ -import { TFile, Keymap, Platform } from 'obsidian'; +import { Keymap, Platform } from 'obsidian'; import FolderNotesPlugin from 'src/main'; import { getFolderNote } from 'src/functions/folderNoteFunctions'; import { handleFolderClick, handleViewHeaderClick } from './handleClick'; @@ -11,21 +11,21 @@ export function registerFileExplorerObserver(plugin: FolderNotesPlugin) { // Run once on initial layout plugin.app.workspace.onLayoutReady(() => { initializeFolderNoteFeatures(plugin); + initializeBreadcrumbs(plugin); }); // Re-run when layout changes (e.g. File Explorer is reopened) plugin.registerEvent( plugin.app.workspace.on('layout-change', () => { initializeFolderNoteFeatures(plugin); - }) - ); - // Also listen for file-open events (once) - plugin.registerEvent( - plugin.app.workspace.on('file-open', (file) => { - if (file instanceof TFile) { - scheduleIdle(() => updateBreadcrumbs(plugin), { timeout: 1000 }); - } + const activeLeaf = plugin.app.workspace.getActiveFileView()?.containerEl; + if (!activeLeaf) return; + + const titleContainer = activeLeaf.querySelector('.view-header-title-container'); + if (!(titleContainer instanceof HTMLElement)) return; + + updateBreadcrumbs(plugin, titleContainer); }) ); } @@ -43,8 +43,15 @@ function initializeFolderNoteFeatures(plugin: FolderNotesPlugin) { initializeAllFolderTitles(explorer, plugin); observeFolderTitleMutations(explorer, plugin); +} - explorer.addEventListener('click', (e) => handleBreadcrumbClick((e as MouseEvent), plugin), true); +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(() => updateBreadcrumbs(plugin, container), { timeout: 1000 }); + }); } /** @@ -75,7 +82,9 @@ function initializeAllFolderTitles(container: Element, plugin: FolderNotesPlugin if (!folderEl) continue; const folderPath = folderEl.getAttribute('data-path') || ''; - setupFolderTitle(folderTitle, plugin, folderPath); + setTimeout(() => { + setupFolderTitle(folderTitle, plugin, folderPath); + }, 1000); } } @@ -113,6 +122,10 @@ async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlu folderTitle.dataset.initialized = 'true'; await applyCSSClassesToFolder(folderPath, plugin); + if (plugin.settings.frontMatterTitle.enabled) { + plugin.fmtpHandler?.handleRenameFolder({ id: '', result: false, path: folderPath }, false); + } + folderTitle.addEventListener('auxclick', (event: MouseEvent) => { if (event.button === 1) handleFolderClick(event, plugin); }, { capture: true }); @@ -147,42 +160,32 @@ async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlu }); } -async function updateBreadcrumbs(plugin: FolderNotesPlugin) { - const headers = document.querySelectorAll('span.view-header-breadcrumb'); +async function updateBreadcrumbs(plugin: FolderNotesPlugin, titleContainer: HTMLElement) { + const headers = titleContainer.querySelectorAll('span.view-header-breadcrumb'); + let path = ''; headers.forEach(async (breadcrumb: HTMLElement) => { - let path = ''; - const allCrumbs = breadcrumb.parentElement?.querySelectorAll('span.view-header-breadcrumb'); - if (!allCrumbs) return; - - for (const crumb of Array.from(allCrumbs)) { - path += crumb.getAttribute('old-name') ?? (crumb as HTMLElement).innerText.trim(); - path += '/'; - const folderPath = path.slice(0, -1); - const folder = plugin.fmtpHandler?.modifiedFolders.get(folderPath); - if (folder && plugin.settings.frontMatterTitle.path && plugin.settings.frontMatterTitle.enabled) { - crumb.setAttribute('old-name', folder.name || ''); - (crumb as HTMLElement).innerText = folder.newName || ''; - } - const excludedFolder = getExcludedFolder(plugin, folderPath, true); - if (excludedFolder?.disableFolderNote) return; - const folderNote = getFolderNote(plugin, folderPath); - if (folderNote) crumb.classList.add('has-folder-note'); + path += breadcrumb.getAttribute('old-name') ?? (breadcrumb as HTMLElement).innerText.trim(); + path += '/'; + const folderPath = path.slice(0, -1); + if (plugin.settings.frontMatterTitle.enabled) { + plugin.fmtpHandler?.handleRenameFolder({ id: '', result: false, path: folderPath }, false); } - breadcrumb.parentElement?.setAttribute('data-path', 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 }); + } }); } -function handleBreadcrumbClick(event: MouseEvent, plugin: FolderNotesPlugin) { - const target = event.target as HTMLElement; - const breadcrumb = target.closest('span.view-header-breadcrumb') as HTMLElement; - if (!breadcrumb || breadcrumb.onclick) return; - - breadcrumb.addEventListener('click', (e) => { - handleViewHeaderClick(e, plugin); - }, { capture: 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). diff --git a/src/settings/GeneralSettings.ts b/src/settings/GeneralSettings.ts index 63406a1..347b64b 100644 --- a/src/settings/GeneralSettings.ts +++ b/src/settings/GeneralSettings.ts @@ -429,7 +429,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { ' with folder notes. It allows you to set the folder name to some name you set in the front matter.', ); - new Setting(containerEl) + const fmtpSetting = new Setting(containerEl) .setName('Enable front matter title plugin integration') .setDesc(desc1) .addToggle((toggle) => @@ -453,6 +453,8 @@ export async function renderGeneral(settingsTab: SettingsTab) { 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' }); From d6cf91b76712edd2217b3c8bc2a72a476ad7d2f7 Mon Sep 17 00:00:00 2001 From: Lost Paul <70213368+LostPaul@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:54:11 +0200 Subject: [PATCH 4/4] Fix changing folder names in the path --- src/events/FrontMatterTitle.ts | 39 +++++++++++++++++++--------- src/events/MutationObserver.ts | 20 +++++++------- src/events/handleRename.ts | 4 +-- src/functions/styleFunctions.ts | 2 +- src/main.ts | 29 +++++++++++++-------- src/settings/FileExplorerSettings.ts | 2 +- src/settings/GeneralSettings.ts | 8 +++--- src/settings/PathSettings.ts | 4 +-- 8 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/events/FrontMatterTitle.ts b/src/events/FrontMatterTitle.ts index a8ae594..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 @@ -44,7 +44,7 @@ export class FrontMatterTitlePluginHandler { // this.plugin.app.vault.getFiles().forEach((file) => { // this.handleRename({ id: '', result: false, path: file.path }, false); // }); - this.plugin.updateBreadcrumbs(); + 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); @@ -85,11 +94,13 @@ export class FrontMatterTitlePluginHandler { } - async handleRenameFolder(data: { + async fmptUpdateFolderName(data: { id: string; result: boolean; path: string; - }, isEvent: boolean) { + 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; } @@ -100,11 +111,15 @@ export class FrontMatterTitlePluginHandler { const newName = resolver?.resolve(folderNote?.path ?? ''); if (!newName) return; - if (isEvent) { - this.plugin.changeName(folder, newName, true); - } else { - this.plugin.changeName(folder, newName, false); + 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 f01589c..5b9b66e 100644 --- a/src/events/MutationObserver.ts +++ b/src/events/MutationObserver.ts @@ -25,7 +25,7 @@ export function registerFileExplorerObserver(plugin: FolderNotesPlugin) { const titleContainer = activeLeaf.querySelector('.view-header-title-container'); if (!(titleContainer instanceof HTMLElement)) return; - updateBreadcrumbs(plugin, titleContainer); + updateFolderNamesInPath(plugin, titleContainer); }) ); } @@ -50,7 +50,7 @@ function initializeBreadcrumbs(plugin: FolderNotesPlugin) { if (!titleContainers.length) return; titleContainers.forEach((container) => { if (!(container instanceof HTMLElement)) return; - scheduleIdle(() => updateBreadcrumbs(plugin, container), { timeout: 1000 }); + scheduleIdle(() => updateFolderNamesInPath(plugin, container), { timeout: 1000 }); }); } @@ -66,7 +66,7 @@ function observeFolderTitleMutations(container: Element, plugin: FolderNotesPlug for (const mutation of mutations) { for (const node of Array.from(mutation.addedNodes)) { if (!(node instanceof HTMLElement)) continue; - processAddedFolderNodes(node, plugin); + processAddedFolders(node, plugin); } } }); @@ -88,7 +88,7 @@ function initializeAllFolderTitles(container: Element, plugin: FolderNotesPlugin } } -function processAddedFolderNodes(node: HTMLElement, plugin: FolderNotesPlugin) { +function processAddedFolders(node: HTMLElement, plugin: FolderNotesPlugin) { const titles: HTMLElement[] = []; if (node.matches('.nav-folder-title-content')) { titles.push(node); @@ -123,7 +123,7 @@ async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlu await applyCSSClassesToFolder(folderPath, plugin); if (plugin.settings.frontMatterTitle.enabled) { - plugin.fmtpHandler?.handleRenameFolder({ id: '', result: false, path: folderPath }, false); + plugin.fmtpHandler?.fmptUpdateFolderName({ id: '', result: false, path: folderPath, pathOnly: false }, false); } folderTitle.addEventListener('auxclick', (event: MouseEvent) => { @@ -160,29 +160,29 @@ async function setupFolderTitle(folderTitle: HTMLElement, plugin: FolderNotesPlu }); } -async function updateBreadcrumbs(plugin: FolderNotesPlugin, titleContainer: HTMLElement) { +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); - if (plugin.settings.frontMatterTitle.enabled) { - plugin.fmtpHandler?.handleRenameFolder({ id: '', result: false, path: folderPath }, false); - } 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); + } }); } diff --git a/src/events/handleRename.ts b/src/events/handleRename.ts index ba9e1b1..e0753df 100644 --- a/src/events/handleRename.ts +++ b/src/events/handleRename.ts @@ -36,7 +36,7 @@ export function handleRename(file: TAbstractFile, oldPath: string, plugin: Folde } } else if (file instanceof TFile) { if (isRename) { - return handleFileRename(file, oldPath, plugin); + return fmptUpdateFileName(file, oldPath, plugin); } else { return handleFileMove(file, oldPath, plugin); } @@ -129,7 +129,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; } diff --git a/src/functions/styleFunctions.ts b/src/functions/styleFunctions.ts index d5a27f2..f05bf03 100644 --- a/src/functions/styleFunctions.ts +++ b/src/functions/styleFunctions.ts @@ -5,7 +5,7 @@ import { getFolder, getFolderNote } from 'src/functions/folderNoteFunctions'; import { getFileExplorer } from './utils'; import FolderOverviewPlugin from 'src/obsidian-folder-overview/src/main'; -export function loadFileClasses(forceReload = false, plugin: FolderNotesPlugin) { +export function updateAllFileStyles(forceReload = false, plugin: FolderNotesPlugin) { if (plugin.activeFileExplorer === getFileExplorer(plugin) && !forceReload) { return; } plugin.activeFileExplorer = getFileExplorer(plugin); plugin.app.vault.getAllLoadedFiles().forEach(async (file) => { diff --git a/src/main.ts b/src/main.ts index f01c02a..b2292a7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,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 { addCSSClassToTitleEL, getEl, updateAllFileStyles } from './functions/styleFunctions'; import { getExcludedFolder } from './ExcludeFolders/functions/folderFunctions'; import { FileExplorerView, InternalPlugin } from 'obsidian-typings'; import { getFocusedItem } from './functions/utils'; @@ -275,13 +275,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; + async changeFolderNameInExplorer(folder: TFolder, newName: string | null | undefined, waitForCreate = false, count = 0) { + if (!newName) newName = folder.name; let fileExplorerItem = getEl(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; @@ -290,18 +290,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); @@ -330,7 +338,6 @@ export default class FolderNotesPlugin extends Plugin { } onunload() { - console.log('unloading folder notes plugin'); unregisterFileExplorerObserver(); document.body.classList.remove('folder-notes-plugin'); document.body.classList.remove('folder-note-underline'); @@ -369,7 +376,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); + updateAllFileStyles(true, this); } } diff --git a/src/settings/FileExplorerSettings.ts b/src/settings/FileExplorerSettings.ts index b32475a..3cf99ca 100644 --- a/src/settings/FileExplorerSettings.ts +++ b/src/settings/FileExplorerSettings.ts @@ -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); }); }) ); diff --git a/src/settings/GeneralSettings.ts b/src/settings/GeneralSettings.ts index 347b64b..5371df2 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 { updateAllFileStyles } from '../functions/styleFunctions'; import BackupWarningModal from './modals/BackupWarning'; import RenameFolderNotesModal from './modals/RenameFns'; @@ -186,7 +186,7 @@ export async function renderGeneral(settingsTab: SettingsTab) { settingsTab.plugin.settings.storageLocation = value; await settingsTab.plugin.saveSettings(); settingsTab.display(); - loadFileClasses(undefined, settingsTab.plugin); + updateAllFileStyles(undefined, settingsTab.plugin); }) ) .addButton((button) => @@ -442,10 +442,10 @@ export async function renderGeneral(settingsTab: SettingsTab) { settingsTab.plugin.fmtpHandler = new FrontMatterTitlePluginHandler(settingsTab.plugin); } else { if (settingsTab.plugin.fmtpHandler) { - settingsTab.plugin.updateBreadcrumbs(true); + settingsTab.plugin.updateAllBreadcrumbs(true); } 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); }); settingsTab.plugin.fmtpHandler?.deleteEvent(); settingsTab.plugin.fmtpHandler = null; diff --git a/src/settings/PathSettings.ts b/src/settings/PathSettings.ts index 9916ff5..4915082 100644 --- a/src/settings/PathSettings.ts +++ b/src/settings/PathSettings.ts @@ -49,9 +49,9 @@ export async function renderPath(settingsTab: SettingsTab) { settingsTab.plugin.settings.frontMatterTitle.path = value; await settingsTab.plugin.saveSettings(); if (value) { - settingsTab.plugin.updateBreadcrumbs(); + settingsTab.plugin.updateAllBreadcrumbs(); } else { - settingsTab.plugin.updateBreadcrumbs(true); + settingsTab.plugin.updateAllBreadcrumbs(true); } }) );