// 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');
})();