Files
michaelschiemer/resources/js/modules/livecomponent/UIEventHandler.js
2025-11-24 21:28:25 +01:00

530 lines
17 KiB
JavaScript

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