mirror of
https://github.com/lostpaul/obsidian-folder-notes.git
synced 2026-07-22 07:40:24 +00:00
Improve performance of mutation observer
This commit is contained in:
parent
f53e2b6648
commit
fe64c66b60
2 changed files with 154 additions and 87 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/main.ts
15
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');
|
||||
|
|
|
|||
Loading…
Reference in a new issue