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] 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');