/** * UI Event Handler for LiveComponents * * Listens to UI-related events from LiveComponents and automatically * displays Toasts, Modals, and other UI components. * * Supported Events: * - toast:show - Show toast notification * - toast:hide - Hide toast notification * - modal:show - Show modal dialog * - modal:close - Close modal dialog * - modal:confirm - Show confirmation dialog * - modal:alert - Show alert dialog */ import { LiveComponentUIHelper } from './LiveComponentUIHelper.js'; export class UIEventHandler { constructor(liveComponentManager) { this.manager = liveComponentManager; this.uiHelper = liveComponentManager.uiHelper || new LiveComponentUIHelper(liveComponentManager); this.eventListeners = new Map(); this.isInitialized = false; this.drawerManager = null; // Lazy-loaded when needed this.popoverManager = null; // Lazy-loaded when needed } /** * Initialize event listeners */ init() { if (this.isInitialized) { return; } // Toast events this.addEventListener('toast:show', (e) => this.handleToastShow(e)); this.addEventListener('toast:hide', (e) => this.handleToastHide(e)); // Modal events this.addEventListener('modal:show', (e) => this.handleModalShow(e)); this.addEventListener('modal:close', (e) => this.handleModalClose(e)); this.addEventListener('modal:confirm', (e) => this.handleModalConfirm(e)); this.addEventListener('modal:alert', (e) => this.handleModalAlert(e)); // Also listen to livecomponent: prefixed events for compatibility this.addEventListener('livecomponent:toast:show', (e) => this.handleToastShow(e)); this.addEventListener('livecomponent:toast:hide', (e) => this.handleToastHide(e)); this.addEventListener('livecomponent:modal:show', (e) => this.handleModalShow(e)); this.addEventListener('livecomponent:modal:close', (e) => this.handleModalClose(e)); this.addEventListener('livecomponent:modal:confirm', (e) => this.handleModalConfirm(e)); this.addEventListener('livecomponent:modal:alert', (e) => this.handleModalAlert(e)); // Drawer events this.addEventListener('drawer:open', (e) => this.handleDrawerOpen(e)); this.addEventListener('drawer:close', (e) => this.handleDrawerClose(e)); this.addEventListener('drawer:toggle', (e) => this.handleDrawerToggle(e)); this.addEventListener('livecomponent:drawer:open', (e) => this.handleDrawerOpen(e)); this.addEventListener('livecomponent:drawer:close', (e) => this.handleDrawerClose(e)); this.addEventListener('livecomponent:drawer:toggle', (e) => this.handleDrawerToggle(e)); // Popover events this.addEventListener('popover:show', (e) => this.handlePopoverShow(e)); this.addEventListener('popover:hide', (e) => this.handlePopoverHide(e)); this.addEventListener('popover:toggle', (e) => this.handlePopoverToggle(e)); this.addEventListener('livecomponent:popover:show', (e) => this.handlePopoverShow(e)); this.addEventListener('livecomponent:popover:hide', (e) => this.handlePopoverHide(e)); this.addEventListener('livecomponent:popover:toggle', (e) => this.handlePopoverToggle(e)); this.isInitialized = true; console.log('[UIEventHandler] Initialized'); } /** * Add event listener * @param {string} eventName - Event name * @param {Function} handler - Event handler */ addEventListener(eventName, handler) { const wrappedHandler = (e) => { try { handler(e); } catch (error) { console.error(`[UIEventHandler] Error handling event ${eventName}:`, error); } }; document.addEventListener(eventName, wrappedHandler); this.eventListeners.set(eventName, wrappedHandler); } /** * Remove event listener * @param {string} eventName - Event name */ removeEventListener(eventName) { const handler = this.eventListeners.get(eventName); if (handler) { document.removeEventListener(eventName, handler); this.eventListeners.delete(eventName); } } /** * Handle toast:show event * @param {CustomEvent} e - Event object */ handleToastShow(e) { const payload = e.detail || {}; const { message = '', type = 'info', duration = 5000, position = 'top-right', componentId = 'global' } = payload; if (!message) { console.warn('[UIEventHandler] toast:show event missing message'); return; } console.log(`[UIEventHandler] Showing toast: ${message} (${type})`); this.uiHelper.showNotification(componentId, { message: message, type: type, duration: duration, position: position }); } /** * Handle toast:hide event * @param {CustomEvent} e - Event object */ handleToastHide(e) { const payload = e.detail || {}; const { componentId = 'global' } = payload; console.log(`[UIEventHandler] Hiding toast for component: ${componentId}`); this.uiHelper.hideNotification(componentId); } /** * Handle modal:show event * @param {CustomEvent} e - Event object */ handleModalShow(e) { const payload = e.detail || {}; const { componentId, title = '', content = '', size = 'medium', buttons = [], closeOnBackdrop = true, closeOnEscape = true, onClose = null, onConfirm = null } = payload; if (!componentId) { console.warn('[UIEventHandler] modal:show event missing componentId'); return; } console.log(`[UIEventHandler] Showing modal for component: ${componentId}`); this.uiHelper.showDialog(componentId, { title: title, content: content, size: size, buttons: buttons, closeOnBackdrop: closeOnBackdrop, closeOnEscape: closeOnEscape, onClose: onClose, onConfirm: onConfirm }); } /** * Handle modal:close event * @param {CustomEvent} e - Event object */ handleModalClose(e) { const payload = e.detail || {}; const { componentId } = payload; if (!componentId) { console.warn('[UIEventHandler] modal:close event missing componentId'); return; } console.log(`[UIEventHandler] Closing modal for component: ${componentId}`); this.uiHelper.closeDialog(componentId); } /** * Handle modal:confirm event * @param {CustomEvent} e - Event object */ async handleModalConfirm(e) { const payload = e.detail || {}; const { componentId, title = 'Confirm', message = 'Are you sure?', confirmText = 'Confirm', cancelText = 'Cancel', confirmClass = 'btn-primary', cancelClass = 'btn-secondary', confirmAction = null, confirmParams = null, onConfirm = null, onCancel = null } = payload; if (!componentId) { console.warn('[UIEventHandler] modal:confirm event missing componentId'); return; } console.log(`[UIEventHandler] Showing confirmation dialog for component: ${componentId}`); try { const confirmed = await this.uiHelper.showConfirm(componentId, { title: title, message: message, confirmText: confirmText, cancelText: cancelText, confirmClass: confirmClass, cancelClass: cancelClass }); // If user confirmed and confirmAction is provided, call the LiveComponent action if (confirmed && confirmAction) { console.log(`[UIEventHandler] User confirmed, calling action: ${confirmAction}`, confirmParams); // Call LiveComponent action via manager if (this.manager && typeof this.manager.callAction === 'function') { await this.manager.callAction(componentId, confirmAction, confirmParams || {}); } else if (this.manager && typeof this.manager.executeAction === 'function') { // Fallback to executeAction for backward compatibility await this.manager.executeAction(componentId, confirmAction, confirmParams || {}); } else { console.warn('[UIEventHandler] Cannot execute confirmAction: manager.callAction/executeAction not available'); } } // Legacy callback support if (confirmed && onConfirm) { onConfirm(); } else if (!confirmed && onCancel) { onCancel(); } // Dispatch result event document.dispatchEvent(new CustomEvent('modal:confirm:result', { detail: { componentId: componentId, confirmed: confirmed } })); } catch (error) { console.error('[UIEventHandler] Error showing confirmation dialog:', error); } } /** * Handle modal:alert event * @param {CustomEvent} e - Event object */ handleModalAlert(e) { const payload = e.detail || {}; const { componentId, title = 'Alert', message = '', buttonText = 'OK', type = 'info', onClose = null } = payload; if (!componentId) { console.warn('[UIEventHandler] modal:alert event missing componentId'); return; } console.log(`[UIEventHandler] Showing alert dialog for component: ${componentId}`); this.uiHelper.showAlert(componentId, { title: title, message: message, buttonText: buttonText, type: type }); if (onClose) { // Note: showAlert doesn't return a promise, so we'll call onClose after a delay // This is a limitation - ideally showAlert would return a promise setTimeout(() => { onClose(); }, 100); } } /** * Handle drawer:open event * @param {CustomEvent} e - Event object */ async handleDrawerOpen(e) { const payload = e.detail || {}; const { componentId, title = '', content = '', position = 'left', width = '400px', showOverlay = true, closeOnOverlay = true, closeOnEscape = true, animation = 'slide' } = payload; if (!componentId) { console.warn('[UIEventHandler] drawer:open event missing componentId'); return; } console.log(`[UIEventHandler] Opening drawer for component: ${componentId}`); // Initialize DrawerManager if not already done if (!this.drawerManager) { const { DrawerManager } = await import('./DrawerManager.js'); this.drawerManager = new DrawerManager(); } this.drawerManager.open(componentId, { title: title, content: content, position: position, width: width, showOverlay: showOverlay, closeOnOverlay: closeOnOverlay, closeOnEscape: closeOnEscape, animation: animation }); } /** * Handle drawer:close event * @param {CustomEvent} e - Event object */ handleDrawerClose(e) { const payload = e.detail || {}; const { componentId } = payload; if (!componentId) { console.warn('[UIEventHandler] drawer:close event missing componentId'); return; } console.log(`[UIEventHandler] Closing drawer for component: ${componentId}`); if (this.drawerManager) { this.drawerManager.close(componentId); } } /** * Handle drawer:toggle event * @param {CustomEvent} e - Event object */ handleDrawerToggle(e) { const payload = e.detail || {}; const { componentId } = payload; if (!componentId) { console.warn('[UIEventHandler] drawer:toggle event missing componentId'); return; } console.log(`[UIEventHandler] Toggling drawer for component: ${componentId}`); if (this.drawerManager) { const isOpen = this.drawerManager.isOpen(componentId); if (isOpen) { this.drawerManager.close(componentId); } else { // For toggle, we need the full options - this is a limitation // In practice, components should use open/close explicitly console.warn('[UIEventHandler] drawer:toggle requires full options - use drawer:open/close instead'); } } } /** * Handle popover:show event * @param {CustomEvent} e - Event object */ async handlePopoverShow(e) { const payload = e.detail || {}; const { componentId, anchorId, content = '', title = '', position = 'top', showArrow = true, offset = 8, closeOnOutsideClick = true } = payload; if (!componentId || !anchorId) { console.warn('[UIEventHandler] popover:show event missing componentId or anchorId'); return; } console.log(`[UIEventHandler] Showing popover for component: ${componentId}`); // Initialize PopoverManager if not already done if (!this.popoverManager) { const { PopoverManager } = await import('./PopoverManager.js'); this.popoverManager = new PopoverManager(); } this.popoverManager.show(componentId, { anchorId: anchorId, content: content, title: title, position: position, showArrow: showArrow, offset: offset, closeOnOutsideClick: closeOnOutsideClick }); } /** * Handle popover:hide event * @param {CustomEvent} e - Event object */ handlePopoverHide(e) { const payload = e.detail || {}; const { componentId } = payload; if (!componentId) { console.warn('[UIEventHandler] popover:hide event missing componentId'); return; } console.log(`[UIEventHandler] Hiding popover for component: ${componentId}`); if (this.popoverManager) { this.popoverManager.hide(componentId); } } /** * Handle popover:toggle event * @param {CustomEvent} e - Event object */ async handlePopoverToggle(e) { const payload = e.detail || {}; const { componentId, anchorId, content = '', title = '', position = 'top', showArrow = true, offset = 8, closeOnOutsideClick = true } = payload; if (!componentId || !anchorId) { console.warn('[UIEventHandler] popover:toggle event missing componentId or anchorId'); return; } // Initialize PopoverManager if not already done if (!this.popoverManager) { const { PopoverManager } = await import('./PopoverManager.js'); this.popoverManager = new PopoverManager(); } // Check if popover is visible const isVisible = this.popoverManager.popovers.has(componentId); if (isVisible) { this.popoverManager.hide(componentId); } else { this.popoverManager.show(componentId, { anchorId: anchorId, content: content, title: title, position: position, showArrow: showArrow, offset: offset, closeOnOutsideClick: closeOnOutsideClick }); } } /** * Cleanup all event listeners */ destroy() { for (const [eventName, handler] of this.eventListeners.entries()) { document.removeEventListener(eventName, handler); } this.eventListeners.clear(); // Cleanup managers if (this.drawerManager) { this.drawerManager.destroy(); } if (this.popoverManager) { this.popoverManager.destroy(); } this.isInitialized = false; console.log('[UIEventHandler] Destroyed'); } }