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