model-explorer / js /ui /errorDisplay.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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;