UNPKG

besper-frontend-site-dev-main

Version:

Professional B-esper Frontend Site - Site-wide integration toolkit for full website bot deployment

234 lines (211 loc) 7.17 kB
/** * DOM Ready Utilities * Modern best practices for handling DOM element availability without setTimeout */ /** * Wait for an element to be available in the DOM * @param {string} selector - CSS selector for the element * @param {Element} [context=document] - Context to search within * @param {number} [timeout=5000] - Maximum time to wait in milliseconds * @returns {Promise<Element>} Promise that resolves with the element */ export function waitForElement(selector, context = document, timeout = 5000) { return new Promise((resolve, reject) => { // Check if element already exists const existingElement = context.querySelector(selector); if (existingElement) { resolve(existingElement); return; } // Set up timeout const timeoutId = setTimeout(() => { observer.disconnect(); reject(new Error(`Element ${selector} not found within ${timeout}ms`)); }, timeout); // Set up MutationObserver to watch for the element const observer = new MutationObserver(_mutations => { const element = context.querySelector(selector); if (element) { clearTimeout(timeoutId); observer.disconnect(); resolve(element); } }); // Start observing observer.observe(context, { childList: true, subtree: true, }); }); } /** * Wait for multiple elements to be available * @param {string[]} selectors - Array of CSS selectors * @param {Element} [context=document] - Context to search within * @param {number} [timeout=5000] - Maximum time to wait in milliseconds * @returns {Promise<Element[]>} Promise that resolves with array of elements */ export function waitForElements(selectors, context = document, timeout = 5000) { return Promise.all( selectors.map(selector => waitForElement(selector, context, timeout)) ); } /** * Wait for an element and ensure it's ready for interaction * @param {string} selector - CSS selector for the element * @param {Element} [context=document] - Context to search within * @param {number} [timeout=5000] - Maximum time to wait in milliseconds * @returns {Promise<Element>} Promise that resolves with the ready element */ export function waitForElementReady( selector, context = document, timeout = 5000 ) { return waitForElement(selector, context, timeout).then(element => { // Additional check to ensure element is fully rendered return new Promise(resolve => { if ( element.offsetParent !== null || element.offsetWidth > 0 || element.offsetHeight > 0 ) { resolve(element); } else { // Use requestAnimationFrame to wait for next render cycle requestAnimationFrame(() => resolve(element)); } }); }); } /** * Wait for a tab or widget to be fully initialized * @param {Element} widget - The widget container element * @param {string[]} requiredSelectors - Array of selectors that must be present * @param {number} [timeout=5000] - Maximum time to wait in milliseconds * @returns {Promise<Element>} Promise that resolves with the widget when ready */ export function waitForWidgetReady( widget, requiredSelectors = [], timeout = 5000 ) { if (!widget) { return Promise.reject(new Error('Widget element is required')); } if (requiredSelectors.length === 0) { return Promise.resolve(widget); } return waitForElements(requiredSelectors, widget, timeout).then(() => widget); } /** * Create a safe element getter with multiple fallback strategies * @param {string} id - Element ID to find * @param {Element} [widget] - Widget context to search within * @returns {Element|null} Found element or null */ export function getSafeElement(id, widget = null) { const strategies = [ // Strategy 1: Widget-scoped search () => widget?.querySelector(`#${id}`), // Strategy 2: Widget attribute search () => widget?.querySelector(`[id="${id}"]`), // Strategy 3: Global search () => document.querySelector(`#${id}`), // Strategy 4: Global attribute search () => document.querySelector(`[id="${id}"]`), ]; for (const strategy of strategies) { try { const element = strategy(); if (element) { return element; } } catch (error) { // Continue to next strategy } } return null; } /** * Wait for a custom event to be fired * @param {string} eventName - Name of the event to wait for * @param {Element} [target=document] - Element to listen on * @param {number} [timeout=5000] - Maximum time to wait in milliseconds * @returns {Promise<Event>} Promise that resolves with the event */ export function waitForEvent(eventName, target = document, timeout = 5000) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { target.removeEventListener(eventName, eventHandler); reject(new Error(`Event ${eventName} not fired within ${timeout}ms`)); }, timeout); const eventHandler = event => { clearTimeout(timeoutId); target.removeEventListener(eventName, eventHandler); resolve(event); }; target.addEventListener(eventName, eventHandler); }); } /** * Dispatch a custom event when a condition is met * @param {string} eventName - Name of the event to dispatch * @param {Function} condition - Function that returns true when condition is met * @param {Element} [target=document] - Element to dispatch event on * @param {any} [detail] - Event detail data * @param {number} [checkInterval=100] - How often to check condition in milliseconds */ export function dispatchWhenReady( eventName, condition, target = document, detail = null, checkInterval = 100 ) { const check = () => { if (condition()) { const event = new CustomEvent(eventName, { detail }); target.dispatchEvent(event); } else { setTimeout(check, checkInterval); } }; check(); } /** * Enhanced DOM ready utility that works with modern frameworks * @param {Function} callback - Function to call when DOM is ready */ export function onDOMReady(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { // DOM is already ready, use requestAnimationFrame to ensure rendering is complete requestAnimationFrame(callback); } } /** * Wait for images and other resources to load * @param {Element} [context=document] - Context to check for loading resources * @returns {Promise<void>} Promise that resolves when all resources are loaded */ export function waitForResourcesLoaded(context = document) { return new Promise(resolve => { if (document.readyState === 'complete') { resolve(); return; } const images = context.querySelectorAll('img'); const promises = Array.from(images).map(img => { if (img.complete) { return Promise.resolve(); } return new Promise(imgResolve => { img.addEventListener('load', imgResolve); img.addEventListener('error', imgResolve); // Resolve even on error }); }); Promise.all(promises).then(resolve); }); }