devonthesofa_obsidian-note-.../core/lazyElementObserver.ts
2025-07-15 17:30:17 +02:00

156 lines
3.8 KiB
TypeScript

/**
* Interface for element processing callbacks
*/
export interface IElementProcessor {
processElement(element: HTMLElement): void;
}
/**
* Service for observing DOM changes in file explorer
* Single responsibility: DOM observation and element detection
*/
export class LazyElementObserver {
private mutationObserver: MutationObserver | null = null;
private intersectionObserver: IntersectionObserver | null = null;
private processedElements = new WeakSet<HTMLElement>();
private elementProcessor: IElementProcessor;
private readonly INTERSECTION_ROOT_MARGIN = "50px";
private readonly FILE_SELECTOR = "[data-path]";
constructor(elementProcessor: IElementProcessor) {
this.elementProcessor = elementProcessor;
}
/**
* Sets up observers for a container element
*/
setupObservers(container: Element): void {
this.cleanup();
this.initializeIntersectionObserver();
this.initializeMutationObserver(container);
this.observeExistingElements(container);
}
/**
* Marks element as unprocessed for re-processing
*/
markElementForReprocessing(element: HTMLElement): void {
this.processedElements.delete(element);
}
/**
* Cleanup all observers and reset state
*/
cleanup(): void {
this.disconnectObservers();
this.resetState();
}
/**
* Initializes intersection observer for visibility detection
*/
private initializeIntersectionObserver(): void {
this.intersectionObserver = new IntersectionObserver(
(entries) => this.handleIntersectionEntries(entries),
{ rootMargin: this.INTERSECTION_ROOT_MARGIN },
);
}
/**
* Handles intersection observer entries
*/
private handleIntersectionEntries(
entries: IntersectionObserverEntry[],
): void {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const element = entry.target as HTMLElement;
this.processElementIfNeeded(element);
}
});
}
/**
* Processes element if not already processed
*/
private processElementIfNeeded(element: HTMLElement): void {
if (!this.processedElements.has(element)) {
this.elementProcessor.processElement(element);
this.processedElements.add(element);
}
}
/**
* Initializes mutation observer for DOM changes
*/
private initializeMutationObserver(container: Element): void {
this.mutationObserver = new MutationObserver((mutations) =>
this.handleMutations(mutations),
);
this.mutationObserver.observe(container, {
childList: true,
subtree: true,
});
}
/**
* Handles mutation observer entries
*/
private handleMutations(mutations: MutationRecord[]): void {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
this.handleAddedElement(node as HTMLElement);
}
});
});
}
/**
* Handles newly added DOM elements
*/
private handleAddedElement(element: HTMLElement): void {
this.observeElement(element);
const fileElements = element.querySelectorAll(this.FILE_SELECTOR);
fileElements.forEach((el) => this.observeElement(el as HTMLElement));
}
/**
* Observes existing elements in container
*/
private observeExistingElements(container: Element): void {
const fileElements = container.querySelectorAll(this.FILE_SELECTOR);
fileElements.forEach((el) => this.observeElement(el as HTMLElement));
}
/**
* Starts observing a single element
*/
private observeElement(element: HTMLElement): void {
const dataPath = element.getAttribute("data-path");
if (dataPath && this.intersectionObserver) {
this.intersectionObserver.observe(element);
}
}
/**
* Disconnects all observers
*/
private disconnectObservers(): void {
this.mutationObserver?.disconnect();
this.intersectionObserver?.disconnect();
}
/**
* Resets internal state
*/
private resetState(): void {
this.mutationObserver = null;
this.intersectionObserver = null;
this.processedElements = new WeakSet();
}
}