iptv-stream-web/assets/js/notifications.js

420 lines
14 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '&#039;'
};
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');
})();