// Notification System Module // Advanced notifications with variants, queue management, and accessibility (function() { 'use strict'; AppModules.require('ui-controls'); AppModules.register('notifications'); // Notification configuration const NotificationConfig = { maxNotifications: 5, // Maximum notifications shown simultaneously defaultDuration: 4000, // Default auto-dismiss duration (ms) duration: { success: 3000, error: 7000, warning: 5000, info: 4000 }, position: 'top-right', // top-right, top-left, bottom-right, bottom-left, center positionClasses: { 'top-right': '', 'top-left': 'notifications-container--left', 'bottom-right': 'notifications-container--bottom', 'bottom-left': 'notifications-container--bottom notifications-container--left', 'center': 'notifications-container--center' } }; // Notification queue and state let notificationQueue = []; let activeNotifications = []; let notificationCounter = 0; // DOM elements let notificationsContainer = null; // Notification class class Notification { constructor(options = {}) { this.id = `notification-${++notificationCounter}`; this.title = options.title || ''; this.message = options.message || ''; this.type = options.type || 'info'; // success, error, warning, info, default this.duration = options.duration === 0 ? 0 : (options.duration || NotificationConfig.duration[this.type] || NotificationConfig.defaultDuration); this.actions = options.actions || []; this.position = options.position || NotificationConfig.position; this.persistent = options.persistent || false; this.icon = options.icon !== undefined ? options.icon : true; this.timestamp = Date.now(); this.element = null; } createElement() { const notification = document.createElement('div'); notification.id = this.id; notification.className = `notification notification--${this.type}`; notification.setAttribute('role', 'alert'); notification.setAttribute('aria-live', 'assertive'); notification.setAttribute('aria-atomic', 'true'); let html = ''; // Icon if (this.icon) { html += ''; } // Content html += '
'; if (this.title) { html += `
${escapeHtml(this.title)}
`; } html += `
${escapeHtml(this.message)}
`; html += '
'; // Actions if (this.actions.length > 0) { html += '
'; this.actions.forEach(action => { const actionId = `action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}`; html += ``; }); html += '
'; } // Progress bar for auto-dismissing notifications if (this.duration > 0) { html += '
'; } notification.innerHTML = html; // Add close button if no custom actions if (this.actions.length === 0) { const closeBtn = document.createElement('button'); closeBtn.className = 'notification__action'; closeBtn.setAttribute('aria-label', 'Close notification'); closeBtn.innerHTML = '×'; closeBtn.addEventListener('click', () => this.dismiss()); notification.appendChild(closeBtn); } // Set progress bar animation duration if (this.duration > 0) { const progressBar = notification.querySelector('.notification__progress-bar'); if (progressBar) { progressBar.style.animationDuration = `${this.duration}ms`; } } // Bind action handlers this.actions.forEach((action, index) => { const actionBtn = notification.querySelector(`[data-action-id="action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}"]`); if (actionBtn && action.handler) { actionBtn.addEventListener('click', () => { action.handler(this); }); } }); this.element = notification; return notification; } show() { if (!notificationsContainer) return; // Create element if not exists if (!this.element) { this.createElement(); } // Add position class const positionClass = NotificationConfig.positionClasses[this.position] || ''; notificationsContainer.className = `notifications-container ${positionClass}`; // Add to container notificationsContainer.appendChild(this.element); // Set up auto-dismiss if duration is set if (this.duration > 0) { this.timeoutId = setTimeout(() => { this.dismiss(); }, this.duration); } // Pause timeout on hover this.element.addEventListener('mouseenter', () => { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } }); this.element.addEventListener('mouseleave', () => { if (this.duration > 0 && !this.dismissed) { this.timeoutId = setTimeout(() => { this.dismiss(); }, this.duration); } }); return this; } dismiss() { if (this.dismissed) return; this.dismissed = true; // Clear timeout if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } // Add exit animation if (this.element) { this.element.classList.add('exiting'); // Remove after animation setTimeout(() => { if (this.element && this.element.parentNode) { this.element.parentNode.removeChild(this.element); } removeFromActive(this); processQueue(); }, 300); } } update(options = {}) { if (options.message !== undefined) { this.message = options.message; const messageEl = this.element.querySelector('.notification__message'); if (messageEl) { messageEl.textContent = this.message; } } if (options.title !== undefined) { this.title = options.title; const titleEl = this.element.querySelector('.notification__title'); if (titleEl) { titleEl.textContent = this.title; } } } } // Queue management functions function addToQueue(notification) { notificationQueue.push(notification); // Process queue if not at max capacity if (activeNotifications.length < NotificationConfig.maxNotifications) { processQueue(); } } function removeFromActive(notification) { const index = activeNotifications.indexOf(notification); if (index > -1) { activeNotifications.splice(index, 1); } } function processQueue() { // Remove any notifications that have been dismissed activeNotifications = activeNotifications.filter(n => !n.dismissed); // Show queued notifications if space available while (activeNotifications.length < NotificationConfig.maxNotifications && notificationQueue.length > 0) { const notification = notificationQueue.shift(); activeNotifications.push(notification); notification.show(); } } // Public API functions function show(options = {}) { if (typeof options === 'string') { options = { message: options }; } const notification = new Notification(options); addToQueue(notification); return notification; } function success(message, options = {}) { return show(Object.assign({ message, type: 'success' }, options)); } function error(message, options = {}) { return show(Object.assign({ message, type: 'error' }, options)); } function warning(message, options = {}) { return show(Object.assign({ message, type: 'warning' }, options)); } function info(message, options = {}) { return show(Object.assign({ message, type: 'info' }, options)); } function dismissAll() { // Clear queue notificationQueue = []; // Dismiss all active notifications activeNotifications.forEach(notification => { notification.dismiss(); }); activeNotifications = []; } function clearQueue() { notificationQueue = []; } function setConfig(newConfig = {}) { Object.assign(NotificationConfig, newConfig); // Update container class if position changed if (notificationsContainer && newConfig.position) { const positionClass = NotificationConfig.positionClasses[newConfig.position] || ''; notificationsContainer.className = `notifications-container ${positionClass}`; } } // Utility functions function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return (text || '').replace(/[&<>"']/g, m => map[m]); } // Initialize the notification system function initialize() { // Get container element notificationsContainer = document.getElementById('notificationsContainer'); if (!notificationsContainer) { console.warn('Notification container element not found. Notifications will not be displayed.'); return; } // Set initial position class const positionClass = NotificationConfig.positionClasses[NotificationConfig.position] || ''; notificationsContainer.className = `notifications-container ${positionClass}`; // Add keyboard navigation support document.addEventListener('keydown', handleKeyboardNavigation); // Add container click delegation notificationsContainer.addEventListener('click', handleContainerClick); AppLogger.log('Notification system initialized'); } // Keyboard navigation handler function handleKeyboardNavigation(event) { // Close notification on Escape if (event.key === 'Escape') { const activeNotification = activeNotifications[activeNotifications.length - 1]; if (activeNotification) { activeNotification.dismiss(); event.preventDefault(); } } } // Container click handler for delegation function handleContainerClick(event) { // Handle close button clicks if (event.target.classList.contains('notification__action') && event.target.textContent === '×') { const notificationEl = event.target.closest('.notification'); if (notificationEl) { const notificationId = notificationEl.id; const notification = activeNotifications.find(n => n.id === notificationId); if (notification) { notification.dismiss(); } } } } // Legacy toast support - redirect to new system function legacyShowToast(message, duration) { const toastElement = document.getElementById('toast'); if (toastElement) { toastElement.style.display = 'none'; } // Show using new system instead show({ message, duration: duration === 0 ? 0 : duration, type: 'info', position: 'bottom' }); } function legacyHideToast() { // Legacy function - kept for compatibility, but new system is auto-managing AppLogger.warn('hideToast() called - this is deprecated. Notifications auto-manage themselves now.'); } // Update ui-controls module to use new notification system function updateUIControls() { if (window.UIControls && window.UIControls.showToast) { window.UIControls.showToast = legacyShowToast; window.UIControls.hideToast = legacyHideToast; } } // Public API window.Notifications = { show: show, success: success, error: error, warning: warning, info: info, dismissAll: dismissAll, clearQueue: clearQueue, setConfig: setConfig, config: NotificationConfig }; // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } // Update UI controls after a short delay to ensure they load setTimeout(updateUIControls, 100); AppLogger.log('Notification system module loaded'); })();