Spaces:
Running
Running
| /** | |
| * 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; | |