/** * ErrorDisplay - Renders error/warning/info/success messages in #errorContainer. * Supports auto-hide after CONFIG.UI.ERROR_DISPLAY_DURATION and a close button. * Requirements: 11.1, 11.2, 11.3, 11.4, 11.5 */ const ErrorDisplay = (function () { // ─── Private State ──────────────────────────────────────────────────────── /** @type {HTMLElement|null} */ let _container = null; /** @type {number|null} Auto-hide timer id */ let _autoHideTimer = null; // ─── Private Helpers ────────────────────────────────────────────────────── /** * Lazily resolve the #errorContainer element. * @returns {HTMLElement|null} */ function _getContainer() { if (!_container) { _container = document.getElementById('errorContainer'); } return _container; } /** * Map a message type to Bootstrap alert class and icon. * @param {'error'|'warning'|'info'|'success'} type * @returns {{ alertClass: string, iconClass: string }} */ function _getTypeStyles(type) { switch (type) { case 'warning': return { alertClass: 'alert-warning', iconClass: 'fas fa-exclamation-triangle' }; case 'info': return { alertClass: 'alert-info', iconClass: 'fas fa-info-circle' }; case 'success': return { alertClass: 'alert-success', iconClass: 'fas fa-check-circle' }; case 'error': default: return { alertClass: 'alert-danger', iconClass: 'fas fa-times-circle' }; } } /** * Cancel any pending auto-hide timer. */ function _clearTimer() { if (_autoHideTimer !== null) { clearTimeout(_autoHideTimer); _autoHideTimer = null; } } // ─── Public API ─────────────────────────────────────────────────────────── return { /** * Display a message in #errorContainer. * * @param {string} message - The message text to display * @param {'error'|'warning'|'info'|'success'} [type='error'] - Message type * @param {boolean} [autoHide=true] - Whether to auto-hide after the configured duration */ show(message, type = 'error', autoHide = true) { const container = _getContainer(); if (!container) { console.warn('[ErrorDisplay] #errorContainer not found in DOM.'); return; } // Cancel any existing auto-hide _clearTimer(); const { alertClass, iconClass } = _getTypeStyles(type); // Build the alert element const alert = document.createElement('div'); alert.className = `alert ${alertClass} alert-dismissible fade show d-flex align-items-start`; alert.setAttribute('role', 'alert'); alert.setAttribute('aria-live', 'assertive'); alert.setAttribute('aria-atomic', 'true'); // Icon const icon = document.createElement('i'); icon.className = `${iconClass} me-2 mt-1 flex-shrink-0`; icon.setAttribute('aria-hidden', 'true'); // Message text const text = document.createElement('span'); text.className = 'flex-grow-1'; text.textContent = message; // Close button const closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'btn-close ms-2 flex-shrink-0'; closeBtn.setAttribute('aria-label', 'Close'); closeBtn.addEventListener('click', () => { _clearTimer(); ErrorDisplay.hide(); }); alert.appendChild(icon); alert.appendChild(text); alert.appendChild(closeBtn); // Replace any existing content container.innerHTML = ''; container.appendChild(alert); // Auto-hide after configured duration if (autoHide) { const duration = typeof CONFIG !== 'undefined' && CONFIG.UI && CONFIG.UI.ERROR_DISPLAY_DURATION ? CONFIG.UI.ERROR_DISPLAY_DURATION : 5000; _autoHideTimer = setTimeout(() => { ErrorDisplay.hide(); }, duration); } }, /** * Hide and clear the current error message. */ hide() { _clearTimer(); const container = _getContainer(); if (!container) return; const alert = container.querySelector('.alert'); if (alert) { // Fade out by removing the 'show' class, then remove element alert.classList.remove('show'); setTimeout(() => { if (container.contains(alert)) { container.removeChild(alert); } }, 150); // matches Bootstrap fade transition } }, /** * Convenience wrappers for each message type. */ showError(message, autoHide = true) { this.show(message, 'error', autoHide); }, showWarning(message, autoHide = true) { this.show(message, 'warning', autoHide); }, showInfo(message, autoHide = true) { this.show(message, 'info', autoHide); }, showSuccess(message, autoHide = true) { this.show(message, 'success', autoHide); }, }; })(); // Subscribe to StateManager error changes so the display stays in sync if (typeof StateManager !== 'undefined') { StateManager.subscribe('error', function (errorState) { if (errorState && errorState.message) { ErrorDisplay.show(errorState.message, errorState.type || 'error'); } else { ErrorDisplay.hide(); } }); } // Export for global access in vanilla JS context window.ErrorDisplay = ErrorDisplay;