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
JavaScript
/**
* 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);
});
}