420 lines
14 KiB
JavaScript
420 lines
14 KiB
JavaScript
// 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 += '<div class="notification__icon" aria-hidden="true"></div>';
|
||
}
|
||
|
||
// Content
|
||
html += '<div class="notification__content">';
|
||
if (this.title) {
|
||
html += `<div class="notification__title">${escapeHtml(this.title)}</div>`;
|
||
}
|
||
html += `<div class="notification__message">${escapeHtml(this.message)}</div>`;
|
||
html += '</div>';
|
||
|
||
// Actions
|
||
if (this.actions.length > 0) {
|
||
html += '<div class="notification__actions">';
|
||
this.actions.forEach(action => {
|
||
const actionId = `action-${this.id}-${action.id || action.label.toLowerCase().replace(/\s+/g, '-')}`;
|
||
html += `<button class="notification__action" data-action-id="${actionId}" aria-label="${escapeHtml(action.label)}">${escapeHtml(action.label)}</button>`;
|
||
});
|
||
html += '</div>';
|
||
}
|
||
|
||
// Progress bar for auto-dismissing notifications
|
||
if (this.duration > 0) {
|
||
html += '<div class="notification__progress"><div class="notification__progress-bar"></div></div>';
|
||
}
|
||
|
||
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');
|
||
|
||
})();
|