UNPKG

juris

Version:

JavaScript Unified Reactive Interface Solution - Browser-optimized version for script tags and CDN usage

669 lines (619 loc) 30.2 kB
// DOM Enhancer // Complete Rewritten DOMEnhancer - Unified Observer System // Maintains 100% backward compatibility while reducing complexity and improving performance // juris-enhance.js if (typeof DOMEnhancer === 'undefined') { class DOMEnhancer { constructor(juris) { console.info(log.i('DOMEnhancer initialized', {}, 'framework')); this.juris = juris; this.enhancedElements = new WeakSet(); this.containerEnhancements = new WeakMap(); this.options = { debounceMs: 5, batchUpdates: true, observeSubtree: true, observeChildList: true, observeNewElements: true, viewportAware: false, viewportMargin: '50px' }; this.enhancementRegistry = new Map(); this.unifiedObserver = null; this.observerRefCount = 0; this.pendingEnhancements = new Set(); this.enhancementTimer = null; this.intersectionObserver = null; this.viewportElements = new WeakMap(); } enhance(selectorOrElement, definition, options = {}) { if (typeof MutationObserver === 'undefined') { return () => {}; } const config = { ...this.options, ...options }; if (config.viewportAware) { return this._enhanceViewportAware(selectorOrElement, definition, config); } if (selectorOrElement instanceof Element) { if (this.enhancedElements.has(selectorOrElement)) return () => { }; const enhancementType = this._determineEnhancementType('', definition); if (enhancementType === 'selectors') { this._enhanceContainer(selectorOrElement, definition, config); } else { this._enhanceElement(selectorOrElement, definition, config); } return () => this._cleanupElement(selectorOrElement); } const selector = selectorOrElement; const enhancementType = this._determineEnhancementType(selector, definition); console.info(log.i('Enhancement registered', { selector, type: enhancementType, viewportAware: config.viewportAware, optionKeys: Object.keys(options) }, 'framework')); this.enhancementRegistry.set(selector, { definition, config, type: enhancementType }); this._enhanceExistingElements(selector, definition, config, enhancementType); if (config.observeNewElements !== false) { this._ensureUnifiedObserver(config); this.observerRefCount++; } return () => this._unenhance(selector); } _enhanceViewportAware(selectorOrElement, definition, config) { console.debug(log.d('Viewport-aware enhancement starting', { selector: typeof selectorOrElement === 'string' ? selectorOrElement : 'element', margin: config.viewportMargin }, 'framework')); // Following existing element vs selector handling pattern if (selectorOrElement instanceof Element) { this._setupViewportObserver(config); this._observeElementForViewport(selectorOrElement, definition, config); return () => this._cleanupViewportElement(selectorOrElement); } const selector = selectorOrElement; const elements = document.querySelectorAll(selector); this._setupViewportObserver(config); elements.forEach(element => { this._observeElementForViewport(element, definition, config); }); return () => { elements.forEach(element => this._cleanupViewportElement(element)); this._cleanupViewportObserver(); }; } _determineEnhancementType(selector, definition) { if (this._hasSelectorsCategory(definition)) return 'selectors'; if (typeof selector === 'string' && /^#[a-zA-Z][\w-]*$/.test(selector)) return 'id'; return 'simple'; } _hasSelectorsCategory(definition) { if (definition?.selectors) return true; if (typeof definition === 'function') { try { const result = definition(this.juris.createContext()); return result?.selectors; } catch (error) { return false; } } return false; } _setupViewportObserver(config) { if (this.intersectionObserver) return; this.intersectionObserver = new IntersectionObserver(entries => { if (config.debounceMs > 0) { this._debouncedProcessViewportChanges(entries); } else { this._processViewportChanges(entries); } }, { root: null, rootMargin: config.viewportMargin, threshold: 0 }); console.debug(log.d('IntersectionObserver created', {margin: config.viewportMargin}, 'framework')); } _processViewportChanges(entries) { entries.forEach(entry => { const element = entry.target; const viewportData = this.viewportElements.get(element); if (!viewportData) return; console.debug(log.d('Viewport visibility changed', { element: element.tagName, isVisible: entry.isIntersecting }, 'framework')); if (entry.isIntersecting) { this._enhanceElement(element, viewportData.definition, viewportData.config); } else { this._cleanupElement(element); if (viewportData.minimal) { this._enhanceElement(element, viewportData.minimal, viewportData.config); } } }); } _debouncedProcessViewportChanges(entries) { entries.forEach(entry => { this.pendingEnhancements.add({ entry, timestamp: Date.now(), type: 'viewport' }); }); if (this.enhancementTimer) clearTimeout(this.enhancementTimer); this.enhancementTimer = setTimeout(() => { this._processPendingViewportChanges(); this.enhancementTimer = null; }, this.options.debounceMs); } _processPendingViewportChanges() { const viewportChanges = Array.from(this.pendingEnhancements) .filter(item => item.type === 'viewport') .map(item => item.entry); this.pendingEnhancements = new Set( Array.from(this.pendingEnhancements).filter(item => item.type !== 'viewport') ); this._processViewportChanges(viewportChanges); } _observeElementForViewport(element, definition, config) { // Following existing element data storage pattern (like componentStates) const minimal = config.minimal || this._createMinimalDefinition(definition); this.viewportElements.set(element, { definition, config, minimal }); this.intersectionObserver.observe(element); // Following existing initial state handling pattern const rect = element.getBoundingClientRect(); const isInitiallyVisible = ( rect.top < window.innerHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0 ); if (isInitiallyVisible) { this._enhanceElement(element, definition, config); } else if (minimal) { this._enhanceElement(element, minimal, config); } } _createMinimalDefinition(definition) { // Following existing property checking pattern if (typeof definition !== 'object' || !definition) return null; const minimal = {}; // Following existing style handling pattern if (definition.style) { minimal.style = {}; // Preserve layout-affecting properties const layoutProps = ['height', 'width', 'display', 'position']; layoutProps.forEach(prop => { if (definition.style[prop]) { minimal.style[prop] = definition.style[prop]; } }); } // Following existing className preservation pattern if (definition.className) { minimal.className = definition.className; } return Object.keys(minimal).length > 0 ? minimal : null; } _cleanupViewportElement(element) { if (this.intersectionObserver) { this.intersectionObserver.unobserve(element); } this.viewportElements.delete(element); this._cleanupElement(element); } _cleanupViewportObserver() { if (this.intersectionObserver && this.viewportElements.size === 0) { this.intersectionObserver.disconnect(); this.intersectionObserver = null; console.debug(log.d('IntersectionObserver disconnected', {}, 'framework')); } } _enhanceExistingElements(selector, definition, config, type) { if (type === 'selectors') { this._enhanceExistingContainers(selector, definition, config); } else { const elements = document.querySelectorAll(selector); if (config.batchUpdates && elements.length > 1) { this._batchEnhanceElements(Array.from(elements), definition, config); } else { elements.forEach(element => this._enhanceElement(element, definition, config)); } } } _enhanceExistingContainers(containerSelector, definition, config) { document.querySelectorAll(containerSelector).forEach(container => this._enhanceContainer(container, definition, config) ); } _batchEnhanceElements(elements, definition, config) { elements.filter(element => !this.enhancedElements.has(element)) .forEach(element => this._enhanceElement(element, definition, config)); } _ensureUnifiedObserver(config) { if (!this.unifiedObserver) { this.unifiedObserver = new MutationObserver(mutations => { if (config.debounceMs > 0) { this._debouncedProcessMutations(mutations); } else { this._processUnifiedMutations(mutations); } }); this.unifiedObserver.observe(document.body, { childList: config.observeChildList, subtree: config.observeSubtree }); } } _processUnifiedMutations(mutations) { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { this._processNodeForAllEnhancements(node); } }); } }); } _processNodeForAllEnhancements(node) { this.enhancementRegistry.forEach(({ definition, config, type }, selector) => { try { switch (type) { case 'id': this._processIdEnhancement(node, selector, definition, config); break; case 'simple': this._processSimpleEnhancement(node, selector, definition, config); break; case 'selectors': this._processSelectorsEnhancement(node, selector, definition, config); break; } } catch (error) { console.error(log.e('Error processing enhancement:', error), 'framework'); } }); } _processIdEnhancement(node, selector, definition, config) { const id = selector.slice(1); // Remove # prefix if (node.id === id) { this._enhanceElement(node, definition, config); } else if (node.querySelector) { const found = node.querySelector(selector); if (found) this._enhanceElement(found, definition, config); } } _processSimpleEnhancement(node, selector, definition, config) { if (node.matches && node.matches(selector)) { this._enhanceElement(node, definition, config); } if (node.querySelectorAll) { node.querySelectorAll(selector).forEach(element => { this._enhanceElement(element, definition, config); }); } } _processSelectorsEnhancement(node, selector, definition, config) { if (node.matches && node.matches(selector)) { this._enhanceContainer(node, definition, config); } if (node.querySelectorAll) { node.querySelectorAll(selector).forEach(container => { this._enhanceContainer(container, definition, config); }); } this._enhanceNewElementsInContainers(node); } _debouncedProcessMutations(mutations) { mutations.forEach(mutation => { if (mutation.type === 'childList') { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { this.pendingEnhancements.add({ node, timestamp: Date.now(), type: 'unified' }); } }); } }); if (this.enhancementTimer) clearTimeout(this.enhancementTimer); this.enhancementTimer = setTimeout(() => { this._processPendingEnhancements(); this.enhancementTimer = null; }, this.options.debounceMs); } _processPendingEnhancements() { const enhancements = Array.from(this.pendingEnhancements); this.pendingEnhancements.clear(); enhancements.forEach(({ node }) => { this._processNodeForAllEnhancements(node); }); } _enhanceNewElementsInContainers(node) { document.querySelectorAll('[data-juris-enhanced-container]').forEach(container => { if (!container.contains(node)) return; const containerData = this.containerEnhancements.get(container); if (!containerData) return; containerData.forEach((selectorData, selector) => { const { definition, enhancedElements } = selectorData; if (node.matches && node.matches(selector)) { this._enhanceSelectorElement(node, definition, container, selector); enhancedElements.add(node); } if (node.querySelectorAll) { node.querySelectorAll(selector).forEach(element => { if (!this.enhancedElements.has(element)) { this._enhanceSelectorElement(element, definition, container, selector); enhancedElements.add(element); } }); } }); }); } _enhanceContainer(container, definition, config) { if (this.enhancedElements.has(container)) return; try { this.enhancedElements.add(container); container.setAttribute('data-juris-enhanced', Date.now()); let actualDefinition = definition; if (typeof definition === 'function') { const context = this.juris.createContext(container); actualDefinition = definition(context); } if (!actualDefinition?.selectors) { console.warn(log.w('Selectors enhancement must have a "selectors" property'), 'framework'); return; } const containerData = new Map(); this.containerEnhancements.set(container, containerData); this._applyContainerProperties(container, actualDefinition); Object.entries(actualDefinition.selectors).forEach(([selector, selectorDefinition]) => { this._enhanceSelector(container, selector, selectorDefinition, containerData, config); }); } catch (error) { console.error(log.e('Error enhancing container:', error), 'application'); this.enhancedElements.delete(container); } } _applyContainerProperties(container, definition) { const containerProps = { ...definition }; delete containerProps.selectors; if (Object.keys(containerProps).length > 0) { this._applyEnhancements(container, containerProps); } } _enhanceSelector(container, selector, definition, containerData, config) { const elements = container.querySelectorAll(selector); const enhancedElements = new Set(); elements.forEach(element => { if (!this.enhancedElements.has(element)) { this._enhanceSelectorElement(element, definition, container, selector); enhancedElements.add(element); } }); containerData.set(selector, { definition, enhancedElements }); } _enhanceSelectorElement(element, definition, container, selector) { try { this.enhancedElements.add(element); element.setAttribute('data-juris-enhanced-selector', Date.now()); let actualDefinition = definition; if (typeof definition === 'function') { const context = this.juris.createContext(element); actualDefinition = definition(context); if (!actualDefinition || typeof actualDefinition !== 'object') { console.warn(log.w(`Selector '${selector}' function must return a definition object`), 'framework'); this.enhancedElements.delete(element); return; } } const processedDefinition = this._processElementAwareFunctions(element, actualDefinition); this._applyEnhancements(element, processedDefinition); } catch (error) { console.error(log.e('Error enhancing selector element:', error), 'application'); this.enhancedElements.delete(element); } } _processElementAwareFunctions(element, definition) { const processed = {}; Object.entries(definition).forEach(([key, value]) => { if (typeof value === 'function') { if (key.startsWith('on')) { processed[key] = value; } else if (value.length > 0) { try { const context = this.juris.createContext(element); const result = value(context); processed[key] = result && typeof result === 'object' ? result : value; } catch (error) { console.warn(log.w(`Error processing element-aware function '${key}':`, error), 'framework'); processed[key] = value; } } else { processed[key] = value; } } else { processed[key] = value; } }); return processed; } _enhanceElement(element, definition, config) { if (this.enhancedElements.has(element)) { console.debug(log.d('Element already enhanced', { tagName: element.tagName }, 'framework')); return; } try { console.debug(log.d('Enhancing element', { tagName: element.tagName, definitionKeys: Object.keys(definition) }, 'framework')); this.enhancedElements.add(element); element.setAttribute('data-juris-enhanced', Date.now()); let actualDefinition = definition; if (typeof definition === 'function') { const context = this.juris.createContext(element); actualDefinition = definition(context); if (!actualDefinition || typeof actualDefinition !== 'object') { console.warn(log.w('Enhancement function must return a definition object'), 'framework'); this.enhancedElements.delete(element); return; } } this._applyEnhancements(element, actualDefinition); config.onEnhanced?.(element, this.juris.createContext(element)); } catch (error) { console.error(log.e('Element enhancement failed', { tagName: element.tagName, error: error.message }, 'framework')); this.enhancedElements.delete(element); } } _applyEnhancements(element, definition) { const subscriptions = [], eventListeners = []; const renderer = this.juris.domRenderer; Object.keys(definition).forEach(key => { const value = definition[key]; try { if (key === 'children') { this._handleChildren(element, value, subscriptions, renderer); } else if (key === 'text') { renderer._handleText(element, value, subscriptions); } else if (key === 'innerHTML') { this._handleInnerHTML(element, value, subscriptions, renderer); } else if (key === 'style') { renderer._handleStyle(element, value, subscriptions); } else if (key.startsWith('on')) { renderer._handleEvent(element, key, value, eventListeners); } else if (typeof value === 'function') { renderer._handleReactiveAttribute(element, key, value, subscriptions); } else { renderer._setStaticAttribute(element, key, value); } } catch (error) { console.error(log.e(`Error processing enhancement property '${key}':`, error), 'framework'); } }); if (subscriptions.length > 0 || eventListeners.length > 0) { this.juris.domRenderer.subscriptions.set(element, { subscriptions, eventListeners }); } } _handleChildren(element, children, subscriptions, renderer) { if (renderer.isFineGrained()) { renderer._handleChildrenFineGrained(element, children, subscriptions); } else { renderer._handleChildrenOptimized(element, children, subscriptions); } } _handleInnerHTML(element, innerHTML, subscriptions, renderer) { if (typeof innerHTML === 'function') { renderer._handleReactiveAttribute(element, 'innerHTML', innerHTML, subscriptions); } else { element.innerHTML = innerHTML; } } _unenhance(selector) { const enhancement = this.enhancementRegistry.get(selector); if (!enhancement) return; // Cleanup observer reference this.observerRefCount--; if (this.observerRefCount === 0 && this.unifiedObserver) { this.unifiedObserver.disconnect(); this.unifiedObserver = null; } // Remove from registry this.enhancementRegistry.delete(selector); // Clean up enhanced elements if (enhancement.type === 'selectors') { document.querySelectorAll(`${selector}[data-juris-enhanced-container]`).forEach(container => { this._cleanupContainer(container); }); } else { document.querySelectorAll(`${selector}[data-juris-enhanced]`).forEach(element => { this._cleanupElement(element); }); } } _cleanupContainer(container) { const containerData = this.containerEnhancements.get(container); if (containerData) { containerData.forEach(selectorData => { selectorData.enhancedElements.forEach(element => this._cleanupElement(element)); }); this.containerEnhancements.delete(container); } this._cleanupElement(container); container.removeAttribute('data-juris-enhanced-container'); } _cleanupElement(element) { this.juris.domRenderer.cleanup(element); this.enhancedElements.delete(element); element.removeAttribute('data-juris-enhanced'); element.removeAttribute('data-juris-enhanced-selector'); } configure(options) { Object.assign(this.options, options); } getStats() { const enhancedElements = document.querySelectorAll('[data-juris-enhanced]').length; const enhancedContainers = document.querySelectorAll('[data-juris-enhanced-container]').length; const enhancedSelectors = document.querySelectorAll('[data-juris-enhanced-selector]').length; return { enhancementRules: this.enhancementRegistry.size, activeObserver: this.unifiedObserver ? 1 : 0, activeIntersectionObserver: this.intersectionObserver ? 1 : 0, // NEW observerRefCount: this.observerRefCount, pendingEnhancements: this.pendingEnhancements.size, viewportElements: this.viewportElements.size || 0, // NEW enhancedElements, enhancedContainers, enhancedSelectors, totalEnhanced: enhancedElements + enhancedSelectors }; } destroy() { // Existing cleanup if (this.unifiedObserver) { this.unifiedObserver.disconnect(); this.unifiedObserver = null; } this.enhancementRegistry.clear(); this.observerRefCount = 0; if (this.enhancementTimer) { clearTimeout(this.enhancementTimer); this.enhancementTimer = null; } if (this.intersectionObserver) { this.intersectionObserver.disconnect(); this.intersectionObserver = null; } this.viewportElements = new WeakMap(); // Existing element cleanup document.querySelectorAll('[data-juris-enhanced], [data-juris-enhanced-selector]').forEach(element => { this._cleanupElement(element); }); document.querySelectorAll('[data-juris-enhanced-container]').forEach(container => { this._cleanupContainer(container); }); this.pendingEnhancements.clear(); } } // Register feature automatically if (typeof window !== 'undefined') { window.DOMEnhancer = DOMEnhancer; Object.freeze(window.DOMEnhancer); Object.freeze(window.DOMEnhancer.prototype); } // Basic CommonJS for compatibility if (typeof module !== 'undefined' && module.exports) { module.exports.DOMEnhancer = DOMEnhancer; module.exports.default = DOMEnhancer; } }