UNPKG

besper-frontend-site-dev-0935

Version:

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

354 lines (314 loc) 10 kB
/** * Tab Management Utilities * Modern lifecycle management for tab components without setTimeout dependencies */ import { waitForElement, waitForElements, waitForWidgetReady, getSafeElement, } from './DOMReady.js'; /** * Enhanced Tab Manager with proper lifecycle management */ export class TabManager { constructor() { this.tabs = new Map(); this.activeTab = null; this.readyPromises = new Map(); } /** * Register a tab with lifecycle hooks * @param {string} tabId - Unique identifier for the tab * @param {Object} tabComponent - Tab component instance * @param {string[]} requiredElements - Array of element selectors that must be present */ registerTab(tabId, tabComponent, requiredElements = []) { this.tabs.set(tabId, { component: tabComponent, requiredElements, isReady: false, readyPromise: null, }); // Create ready promise for this tab const readyPromise = this.createTabReadyPromise( tabId, tabComponent, requiredElements ); this.readyPromises.set(tabId, readyPromise); return readyPromise; } /** * Create a promise that resolves when a tab is fully ready * @param {string} tabId - Tab identifier * @param {Object} tabComponent - Tab component instance * @param {string[]} requiredElements - Required element selectors * @returns {Promise<Object>} Promise that resolves with tab info */ createTabReadyPromise(tabId, tabComponent, requiredElements) { return new Promise((resolve, reject) => { const initializeTab = async () => { try { // Wait for the tab widget to be available if (tabComponent.widget) { await waitForWidgetReady( tabComponent.widget, requiredElements, 10000 ); } else { // If no widget yet, wait for it to be set await this.waitForTabWidget(tabComponent); await waitForWidgetReady( tabComponent.widget, requiredElements, 10000 ); } // Mark tab as ready const tabInfo = this.tabs.get(tabId); if (tabInfo) { tabInfo.isReady = true; } // Dispatch ready event const event = new CustomEvent(`tab-ready:${tabId}`, { detail: { tabId, component: tabComponent }, }); document.dispatchEvent(event); resolve({ tabId, component: tabComponent, isReady: true }); } catch (error) { reject( new Error(`Tab ${tabId} failed to initialize: ${error.message}`) ); } }; initializeTab(); }); } /** * Wait for a tab component to have its widget set * @param {Object} tabComponent - Tab component instance * @returns {Promise<Element>} Promise that resolves with the widget */ waitForTabWidget(tabComponent) { return new Promise((resolve, reject) => { if (tabComponent.widget) { resolve(tabComponent.widget); return; } const timeout = setTimeout(() => { reject(new Error('Tab widget not set within timeout')); }, 5000); // Check periodically for widget to be set const checkWidget = () => { if (tabComponent.widget) { clearTimeout(timeout); resolve(tabComponent.widget); } else { setTimeout(checkWidget, 50); } }; checkWidget(); }); } /** * Wait for a specific tab to be ready * @param {string} tabId - Tab identifier * @returns {Promise<Object>} Promise that resolves when tab is ready */ waitForTabReady(tabId) { const readyPromise = this.readyPromises.get(tabId); if (readyPromise) { return readyPromise; } return Promise.reject(new Error(`Tab ${tabId} not registered`)); } /** * Wait for multiple tabs to be ready * @param {string[]} tabIds - Array of tab identifiers * @returns {Promise<Object[]>} Promise that resolves when all tabs are ready */ waitForTabsReady(tabIds) { const promises = tabIds.map(tabId => this.waitForTabReady(tabId)); return Promise.all(promises); } /** * Check if a tab is ready synchronously * @param {string} tabId - Tab identifier * @returns {boolean} True if tab is ready */ isTabReady(tabId) { const tabInfo = this.tabs.get(tabId); return tabInfo ? tabInfo.isReady : false; } /** * Safely execute code when a tab is ready * @param {string} tabId - Tab identifier * @param {Function} callback - Function to execute when ready * @returns {Promise<any>} Promise that resolves with callback result */ async executeWhenReady(tabId, callback) { await this.waitForTabReady(tabId); return callback(this.tabs.get(tabId).component); } /** * Load data into a tab safely, waiting for it to be ready first * @param {string} tabId - Tab identifier * @param {string} methodName - Method name to call on the tab component * @param {...any} args - Arguments to pass to the method * @returns {Promise<any>} Promise that resolves with method result */ async loadDataSafely(tabId, methodName, ...args) { return this.executeWhenReady(tabId, component => { if (typeof component[methodName] === 'function') { return component[methodName](...args); } else { throw new Error(`Method ${methodName} not found on tab ${tabId}`); } }); } /** * Get elements safely from a tab with fallback strategies * @param {string} tabId - Tab identifier * @param {string} elementId - Element ID to find * @returns {Promise<Element|null>} Promise that resolves with element or null */ async getTabElement(tabId, elementId) { await this.waitForTabReady(tabId); const tabInfo = this.tabs.get(tabId); if (tabInfo && tabInfo.component.widget) { return getSafeElement(elementId, tabInfo.component.widget); } return null; } /** * Enhanced element getter with retry logic * @param {string} tabId - Tab identifier * @param {string} elementId - Element ID to find * @param {number} maxRetries - Maximum number of retries * @param {number} retryDelay - Delay between retries in milliseconds * @returns {Promise<Element>} Promise that resolves with element */ async getTabElementWithRetry( tabId, elementId, maxRetries = 3, retryDelay = 100 ) { let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const element = await this.getTabElement(tabId, elementId); if (element) { return element; } throw new Error(`Element ${elementId} not found in tab ${tabId}`); } catch (error) { lastError = error; if (attempt < maxRetries - 1) { await new Promise(resolve => setTimeout(resolve, retryDelay)); } } } throw lastError; } } /** * Lifecycle management for individual tab components */ export class TabComponent { constructor(widget, state, options = {}) { this.widget = widget; this.state = state; this.options = options; this.isReady = false; this.readyPromise = null; this.requiredElements = []; } /** * Set required elements for this tab to be considered ready * @param {string[]} selectors - Array of CSS selectors */ setRequiredElements(selectors) { this.requiredElements = selectors; } /** * Initialize the tab and wait for it to be ready * @returns {Promise<void>} Promise that resolves when tab is ready */ async initialize() { if (this.readyPromise) { return this.readyPromise; } this.readyPromise = this.createReadyPromise(); return this.readyPromise; } /** * Create the ready promise for this tab * @returns {Promise<void>} Promise that resolves when ready */ async createReadyPromise() { if (!this.widget) { throw new Error('Widget must be set before initializing tab'); } // Wait for required elements to be available if (this.requiredElements.length > 0) { await waitForElements(this.requiredElements, this.widget, 10000); } // Additional custom ready checks can be implemented in subclasses await this.customReadyCheck(); this.isReady = true; this.onReady(); } /** * Override this method in subclasses for custom ready checks * @returns {Promise<void>} Promise that resolves when custom checks pass */ async customReadyCheck() { // Default implementation - no additional checks return Promise.resolve(); } /** * Override this method in subclasses for ready event handling */ onReady() { // Default implementation - no action } /** * Get an element safely with fallback strategies * @param {string} id - Element ID * @returns {Element|null} Element or null if not found */ getElement(id) { return getSafeElement(id, this.widget); } /** * Get an element and wait if it's not immediately available * @param {string} id - Element ID * @param {number} timeout - Maximum time to wait * @returns {Promise<Element>} Promise that resolves with element */ async waitForElement(id, timeout = 5000) { return waitForElement(`#${id}`, this.widget, timeout); } /** * Load data safely, waiting for the tab to be ready first * @param {any} data - Data to load * @returns {Promise<void>} Promise that resolves when data is loaded */ async loadDataSafely(data) { await this.initialize(); return this.loadData(data); } /** * Override this method in subclasses to implement data loading * @param {any} _data - Data to load (unused in base class) * @returns {Promise<void>|void} Promise or void */ loadData(_data) { // Default implementation - no action } } // Global tab manager instance export const globalTabManager = new TabManager();