// modules/api-manager/ObserverManager.js import { Logger } from '../../core/logger.js'; /** * Observer APIs Manager - Intersection, Resize, Mutation, Performance Observers */ export class ObserverManager { constructor(config = {}) { this.config = config; this.activeObservers = new Map(); this.observerInstances = new Map(); Logger.info('[ObserverManager] Initialized with support:', { intersection: 'IntersectionObserver' in window, resize: 'ResizeObserver' in window, mutation: 'MutationObserver' in window, performance: 'PerformanceObserver' in window }); } /** * Intersection Observer - Viewport intersection detection */ intersection(elements, callback, options = {}) { if (!('IntersectionObserver' in window)) { Logger.warn('[ObserverManager] IntersectionObserver not supported'); return this.createFallbackObserver('intersection', elements, callback); } const defaultOptions = { root: null, rootMargin: '50px', threshold: [0, 0.1, 0.5, 1.0], ...options }; const observerId = this.generateId('intersection'); const observer = new IntersectionObserver((entries, obs) => { const processedEntries = entries.map(entry => ({ element: entry.target, isIntersecting: entry.isIntersecting, intersectionRatio: entry.intersectionRatio, boundingClientRect: entry.boundingClientRect, rootBounds: entry.rootBounds, intersectionRect: entry.intersectionRect, time: entry.time, // Enhanced data visibility: this.calculateVisibility(entry), direction: this.getScrollDirection(entry), position: this.getElementPosition(entry) })); callback(processedEntries, obs); }, defaultOptions); // Observe elements const elementList = Array.isArray(elements) ? elements : [elements]; elementList.forEach(el => { if (el instanceof Element) observer.observe(el); }); this.observerInstances.set(observerId, observer); this.activeObservers.set(observerId, { type: 'intersection', elements: elementList, callback, options: defaultOptions }); Logger.info(`[ObserverManager] IntersectionObserver created: ${observerId}`); return { id: observerId, observer, unobserve: (element) => observer.unobserve(element), disconnect: () => this.disconnect(observerId), updateThreshold: (threshold) => this.updateIntersectionThreshold(observerId, threshold) }; } /** * Resize Observer - Element resize detection */ resize(elements, callback, options = {}) { if (!('ResizeObserver' in window)) { Logger.warn('[ObserverManager] ResizeObserver not supported'); return this.createFallbackObserver('resize', elements, callback); } const observerId = this.generateId('resize'); const observer = new ResizeObserver((entries) => { const processedEntries = entries.map(entry => ({ element: entry.target, contentRect: entry.contentRect, borderBoxSize: entry.borderBoxSize, contentBoxSize: entry.contentBoxSize, devicePixelContentBoxSize: entry.devicePixelContentBoxSize, // Enhanced data dimensions: { width: entry.contentRect.width, height: entry.contentRect.height, aspectRatio: entry.contentRect.width / entry.contentRect.height }, deltaSize: this.calculateDeltaSize(entry), breakpoint: this.detectBreakpoint(entry.contentRect.width) })); callback(processedEntries); }); const elementList = Array.isArray(elements) ? elements : [elements]; elementList.forEach(el => { if (el instanceof Element) observer.observe(el); }); this.observerInstances.set(observerId, observer); this.activeObservers.set(observerId, { type: 'resize', elements: elementList, callback, options }); Logger.info(`[ObserverManager] ResizeObserver created: ${observerId}`); return { id: observerId, observer, unobserve: (element) => observer.unobserve(element), disconnect: () => this.disconnect(observerId) }; } /** * Mutation Observer - DOM change detection */ mutation(target, callback, options = {}) { if (!('MutationObserver' in window)) { Logger.warn('[ObserverManager] MutationObserver not supported'); return null; } const defaultOptions = { childList: true, attributes: true, subtree: true, attributeOldValue: true, characterDataOldValue: true, ...options }; const observerId = this.generateId('mutation'); const observer = new MutationObserver((mutations) => { const processedMutations = mutations.map(mutation => ({ type: mutation.type, target: mutation.target, addedNodes: Array.from(mutation.addedNodes), removedNodes: Array.from(mutation.removedNodes), attributeName: mutation.attributeName, attributeNamespace: mutation.attributeNamespace, oldValue: mutation.oldValue, // Enhanced data summary: this.summarizeMutation(mutation), impact: this.assessMutationImpact(mutation) })); callback(processedMutations); }); observer.observe(target, defaultOptions); this.observerInstances.set(observerId, observer); this.activeObservers.set(observerId, { type: 'mutation', target, callback, options: defaultOptions }); Logger.info(`[ObserverManager] MutationObserver created: ${observerId}`); return { id: observerId, observer, disconnect: () => this.disconnect(observerId), takeRecords: () => observer.takeRecords() }; } /** * Performance Observer - Performance metrics monitoring */ performance(callback, options = {}) { if (!('PerformanceObserver' in window)) { Logger.warn('[ObserverManager] PerformanceObserver not supported'); return null; } const defaultOptions = { entryTypes: ['measure', 'navigation', 'paint', 'largest-contentful-paint'], buffered: true, ...options }; const observerId = this.generateId('performance'); const observer = new PerformanceObserver((list) => { const entries = list.getEntries().map(entry => ({ name: entry.name, entryType: entry.entryType, startTime: entry.startTime, duration: entry.duration, // Enhanced data based on entry type details: this.enhancePerformanceEntry(entry), timestamp: Date.now() })); callback(entries); }); observer.observe(defaultOptions); this.observerInstances.set(observerId, observer); this.activeObservers.set(observerId, { type: 'performance', callback, options: defaultOptions }); Logger.info(`[ObserverManager] PerformanceObserver created: ${observerId}`); return { id: observerId, observer, disconnect: () => this.disconnect(observerId), takeRecords: () => observer.takeRecords() }; } /** * Lazy Loading Helper - Uses IntersectionObserver */ lazyLoad(selector = 'img[data-src], iframe[data-src]', options = {}) { const elements = document.querySelectorAll(selector); return this.intersection(elements, (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const element = entry.element; // Load image or iframe if (element.dataset.src) { element.src = element.dataset.src; delete element.dataset.src; } // Load srcset if available if (element.dataset.srcset) { element.srcset = element.dataset.srcset; delete element.dataset.srcset; } // Add loaded class element.classList.add('loaded'); // Stop observing this element entry.observer.unobserve(element); Logger.info('[ObserverManager] Lazy loaded:', element.src); } }); }, { rootMargin: '100px', ...options }); } /** * Scroll Trigger Helper - Uses IntersectionObserver */ scrollTrigger(elements, callback, options = {}) { return this.intersection(elements, (entries) => { entries.forEach(entry => { const triggerData = { element: entry.element, progress: entry.intersectionRatio, isVisible: entry.isIntersecting, direction: entry.direction, position: entry.position }; callback(triggerData); }); }, { threshold: this.createThresholdArray(options.steps || 10), ...options }); } /** * Viewport Detection Helper */ viewport(callback, options = {}) { const viewportElement = document.createElement('div'); viewportElement.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; visibility: hidden; `; document.body.appendChild(viewportElement); return this.resize([viewportElement], (entries) => { const viewport = entries[0]; callback({ width: viewport.dimensions.width, height: viewport.dimensions.height, aspectRatio: viewport.dimensions.aspectRatio, orientation: viewport.dimensions.width > viewport.dimensions.height ? 'landscape' : 'portrait', breakpoint: viewport.breakpoint }); }, options); } // Helper Methods generateId(type) { return `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } disconnect(observerId) { const observer = this.observerInstances.get(observerId); if (observer) { observer.disconnect(); this.observerInstances.delete(observerId); this.activeObservers.delete(observerId); Logger.info(`[ObserverManager] Observer disconnected: ${observerId}`); } } disconnectAll() { this.observerInstances.forEach((observer, id) => { observer.disconnect(); }); this.observerInstances.clear(); this.activeObservers.clear(); Logger.info('[ObserverManager] All observers disconnected'); } calculateVisibility(entry) { if (!entry.isIntersecting) return 0; const visibleArea = entry.intersectionRect.width * entry.intersectionRect.height; const totalArea = entry.boundingClientRect.width * entry.boundingClientRect.height; return totalArea > 0 ? Math.round((visibleArea / totalArea) * 100) : 0; } getScrollDirection(entry) { // This would need to store previous positions to determine direction // For now, return based on intersection ratio change return entry.intersectionRatio > 0.5 ? 'down' : 'up'; } getElementPosition(entry) { const rect = entry.boundingClientRect; const viewportHeight = window.innerHeight; if (rect.top < 0 && rect.bottom > 0) return 'entering-top'; if (rect.top < viewportHeight && rect.bottom > viewportHeight) return 'entering-bottom'; if (rect.top >= 0 && rect.bottom <= viewportHeight) return 'visible'; return 'hidden'; } calculateDeltaSize(entry) { // Would need to store previous sizes to calculate delta return { width: 0, height: 0 }; } detectBreakpoint(width) { if (width < 576) return 'xs'; if (width < 768) return 'sm'; if (width < 992) return 'md'; if (width < 1200) return 'lg'; return 'xl'; } summarizeMutation(mutation) { return `${mutation.type} on ${mutation.target.tagName}`; } assessMutationImpact(mutation) { // Simple impact assessment if (mutation.type === 'childList') { return mutation.addedNodes.length + mutation.removedNodes.length > 5 ? 'high' : 'low'; } return 'medium'; } enhancePerformanceEntry(entry) { const details = { raw: entry }; switch (entry.entryType) { case 'navigation': details.loadTime = entry.loadEventEnd - entry.navigationStart; details.domContentLoaded = entry.domContentLoadedEventEnd - entry.navigationStart; break; case 'paint': details.paintType = entry.name; break; case 'largest-contentful-paint': details.element = entry.element; details.url = entry.url; break; } return details; } createThresholdArray(steps) { const thresholds = []; for (let i = 0; i <= steps; i++) { thresholds.push(i / steps); } return thresholds; } createFallbackObserver(type, elements, callback) { Logger.warn(`[ObserverManager] Creating fallback for ${type}Observer`); // Simple polling fallback const fallbackId = this.generateId(`fallback_${type}`); let intervalId; switch (type) { case 'intersection': intervalId = setInterval(() => { const elementList = Array.isArray(elements) ? elements : [elements]; const entries = elementList.map(el => ({ element: el, isIntersecting: this.isElementInViewport(el), intersectionRatio: this.calculateIntersectionRatio(el) })); callback(entries); }, 100); break; } return { id: fallbackId, disconnect: () => { if (intervalId) clearInterval(intervalId); } }; } isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= window.innerHeight && rect.right <= window.innerWidth ); } calculateIntersectionRatio(el) { const rect = el.getBoundingClientRect(); const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth; const visibleHeight = Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0); const visibleWidth = Math.min(rect.right, viewportWidth) - Math.max(rect.left, 0); if (visibleHeight <= 0 || visibleWidth <= 0) return 0; const visibleArea = visibleHeight * visibleWidth; const totalArea = rect.height * rect.width; return totalArea > 0 ? visibleArea / totalArea : 0; } getActiveObservers() { return Array.from(this.activeObservers.entries()).map(([id, data]) => ({ id, ...data })); } }