Files
vna_system/vna_system/web_ui/static/js/modules/notifications.js
2025-09-23 18:42:55 +03:00

466 lines
13 KiB
JavaScript

/**
* Notification Manager
* Handles toast notifications and user feedback
*/
export class NotificationManager {
constructor() {
this.container = null;
this.notifications = new Map(); // id -> notification element
this.nextId = 1;
// Configuration
this.config = {
maxNotifications: 5,
defaultTimeout: 5000,
animationDuration: 300,
position: 'top-right' // top-right, top-left, bottom-right, bottom-left
};
// Notification types configuration
this.typeConfig = {
success: {
icon: 'check-circle',
timeout: 3000,
class: 'notification--success'
},
error: {
icon: 'alert-circle',
timeout: 7000,
class: 'notification--error'
},
warning: {
icon: 'alert-triangle',
timeout: 5000,
class: 'notification--warning'
},
info: {
icon: 'info',
timeout: 4000,
class: 'notification--info'
}
};
this.init();
}
/**
* Initialize notification system
*/
init() {
// Create or find notification container
this.container = document.getElementById('notifications');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'notifications';
this.container.className = 'notifications';
document.body.appendChild(this.container);
}
console.log('📢 Notification Manager initialized');
}
/**
* Show a notification
*/
show(options) {
const notification = this.createNotification(options);
this.addNotification(notification);
return notification.id;
}
/**
* Create notification object
*/
createNotification(options) {
const {
type = 'info',
title,
message,
timeout = null,
persistent = false,
actions = []
} = options;
const id = this.nextId++;
const config = this.typeConfig[type] || this.typeConfig.info;
const finalTimeout = persistent ? null : (timeout ?? config.timeout);
return {
id,
type,
title,
message,
timeout: finalTimeout,
persistent,
actions,
config,
element: null,
timer: null,
createdAt: new Date()
};
}
/**
* Add notification to DOM and manage queue
*/
addNotification(notification) {
// Remove oldest notifications if we exceed the limit
this.enforceMaxNotifications();
// Create DOM element
notification.element = this.createNotificationElement(notification);
// Add to container
this.container.appendChild(notification.element);
// Store notification
this.notifications.set(notification.id, notification);
// Animate in
setTimeout(() => {
notification.element.classList.add('notification--visible');
}, 10);
// Set up auto-dismiss timer
if (notification.timeout) {
notification.timer = setTimeout(() => {
this.dismiss(notification.id);
}, notification.timeout);
}
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
console.log(`📢 Showing ${notification.type} notification:`, notification.title);
}
/**
* Create notification DOM element
*/
createNotificationElement(notification) {
const element = document.createElement('div');
element.className = `notification ${notification.config.class}`;
element.dataset.id = notification.id;
const iconHtml = `<i data-lucide="${notification.config.icon}" class="notification__icon"></i>`;
const titleHtml = notification.title ?
`<div class="notification__title">${this.escapeHtml(notification.title)}</div>` : '';
const messageHtml = notification.message ?
`<div class="notification__message">${this.escapeHtml(notification.message)}</div>` : '';
const actionsHtml = notification.actions.length > 0 ?
this.createActionsHtml(notification.actions) : '';
const closeHtml = `
<button class="notification__close" data-action="close" title="Close">
<i data-lucide="x"></i>
</button>
`;
element.innerHTML = `
${iconHtml}
<div class="notification__content">
${titleHtml}
${messageHtml}
${actionsHtml}
</div>
${closeHtml}
`;
// Add event listeners
this.setupNotificationEvents(element, notification);
return element;
}
/**
* Create actions HTML for notification
*/
createActionsHtml(actions) {
if (actions.length === 0) return '';
const actionsHtml = actions.map(action => {
const buttonClass = action.primary ? 'btn btn--primary btn--sm' : 'btn btn--ghost btn--sm';
return `
<button class="${buttonClass}" data-action="custom" data-action-id="${action.id}">
${action.icon ? `<i data-lucide="${action.icon}"></i>` : ''}
${this.escapeHtml(action.label)}
</button>
`;
}).join('');
return `<div class="notification__actions">${actionsHtml}</div>`;
}
/**
* Set up notification event listeners
*/
setupNotificationEvents(element, notification) {
element.addEventListener('click', (event) => {
const action = event.target.closest('[data-action]')?.dataset.action;
if (action === 'close') {
this.dismiss(notification.id);
} else if (action === 'custom') {
const actionId = event.target.closest('[data-action-id]')?.dataset.actionId;
const actionConfig = notification.actions.find(a => a.id === actionId);
if (actionConfig && actionConfig.handler) {
try {
actionConfig.handler(notification);
} catch (error) {
console.error('❌ Error in notification action handler:', error);
}
}
// Auto-dismiss after action unless specified otherwise
if (!actionConfig || actionConfig.dismissOnClick !== false) {
this.dismiss(notification.id);
}
}
});
// Pause auto-dismiss on hover
element.addEventListener('mouseenter', () => {
if (notification.timer) {
clearTimeout(notification.timer);
notification.timer = null;
}
});
// Resume auto-dismiss on leave (if not persistent)
element.addEventListener('mouseleave', () => {
if (notification.timeout && !notification.persistent && !notification.timer) {
notification.timer = setTimeout(() => {
this.dismiss(notification.id);
}, 1000); // Shorter timeout after hover
}
});
}
/**
* Dismiss notification by ID
*/
dismiss(id) {
const notification = this.notifications.get(id);
if (!notification) return;
console.log(`📢 Dismissing notification: ${id}`);
// Clear timer
if (notification.timer) {
clearTimeout(notification.timer);
notification.timer = null;
}
// Animate out
if (notification.element) {
notification.element.classList.add('notification--dismissing');
setTimeout(() => {
if (notification.element && notification.element.parentNode) {
notification.element.parentNode.removeChild(notification.element);
}
this.notifications.delete(id);
}, this.config.animationDuration);
} else {
this.notifications.delete(id);
}
}
/**
* Dismiss all notifications
*/
dismissAll() {
console.log('📢 Dismissing all notifications');
for (const id of this.notifications.keys()) {
this.dismiss(id);
}
}
/**
* Dismiss notifications by type
*/
dismissByType(type) {
console.log(`📢 Dismissing all ${type} notifications`);
for (const notification of this.notifications.values()) {
if (notification.type === type) {
this.dismiss(notification.id);
}
}
}
/**
* Update existing notification
*/
update(id, updates) {
const notification = this.notifications.get(id);
if (!notification) return false;
// Update properties
Object.assign(notification, updates);
// Update DOM if needed
if (updates.title || updates.message) {
const titleEl = notification.element.querySelector('.notification__title');
const messageEl = notification.element.querySelector('.notification__message');
if (titleEl && updates.title !== undefined) {
titleEl.textContent = updates.title;
}
if (messageEl && updates.message !== undefined) {
messageEl.textContent = updates.message;
}
}
// Update timeout
if (updates.timeout !== undefined) {
if (notification.timer) {
clearTimeout(notification.timer);
}
if (updates.timeout > 0) {
notification.timer = setTimeout(() => {
this.dismiss(id);
}, updates.timeout);
}
}
return true;
}
/**
* Enforce maximum number of notifications
*/
enforceMaxNotifications() {
const notifications = Array.from(this.notifications.values())
.sort((a, b) => a.createdAt - b.createdAt);
while (notifications.length >= this.config.maxNotifications) {
const oldest = notifications.shift();
if (oldest && !oldest.persistent) {
this.dismiss(oldest.id);
}
}
}
/**
* Convenience methods for different notification types
*/
success(title, message, options = {}) {
return this.show({
type: 'success',
title,
message,
...options
});
}
error(title, message, options = {}) {
return this.show({
type: 'error',
title,
message,
...options
});
}
warning(title, message, options = {}) {
return this.show({
type: 'warning',
title,
message,
...options
});
}
info(title, message, options = {}) {
return this.show({
type: 'info',
title,
message,
...options
});
}
/**
* Get notification statistics
*/
getStats() {
return {
total: this.notifications.size,
byType: this.getNotificationsByType(),
oldestTimestamp: this.getOldestNotificationTime(),
newestTimestamp: this.getNewestNotificationTime()
};
}
/**
* Get notifications grouped by type
*/
getNotificationsByType() {
const byType = {};
for (const notification of this.notifications.values()) {
byType[notification.type] = (byType[notification.type] || 0) + 1;
}
return byType;
}
/**
* Get oldest notification timestamp
*/
getOldestNotificationTime() {
const notifications = Array.from(this.notifications.values());
if (notifications.length === 0) return null;
return Math.min(...notifications.map(n => n.createdAt.getTime()));
}
/**
* Get newest notification timestamp
*/
getNewestNotificationTime() {
const notifications = Array.from(this.notifications.values());
if (notifications.length === 0) return null;
return Math.max(...notifications.map(n => n.createdAt.getTime()));
}
/**
* Escape HTML to prevent XSS
*/
escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Cleanup
*/
destroy() {
console.log('🧹 Cleaning up Notification Manager...');
this.dismissAll();
if (this.container && this.container.parentNode) {
this.container.parentNode.removeChild(this.container);
}
console.log('✅ Notification Manager cleanup complete');
}
}