Spaces:
Running
Running
File size: 5,750 Bytes
9bd422a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | /**
* 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;
|