Improve performance of mutation observer

This commit is contained in:
Lost Paul 2025-05-30 16:29:00 +02:00
parent f53e2b6648
commit fe64c66b60
2 changed files with 154 additions and 87 deletions

View file

@ -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') {
(<Element>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; }
// (<Element>rec.target).querySelectorAll('div.nav-file-title-content')
// .forEach(async (element: HTMLElement) => {
// const filePath = element.parentElement?.getAttribute('data-path') || '';
// applyCSSClassesToFolderNote(filePath, plugin);
// });
(<Element>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);
}
}

View file

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