Files
notytex/static/js/components/notification-system.js

297 lines
11 KiB
JavaScript

/**
* NOTIFICATION SYSTEM - Composant unifié pour toasts et alertes
* Fait partie du design system Notytex
*/
class NotificationSystem {
constructor(core, config = {}) {
this.core = core;
this.config = {
position: 'top-right',
duration: 3000,
maxVisible: 5,
animation: 'slide',
...config
};
this.notifications = new Map();
this.container = null;
this.nextId = 1;
}
init() {
this.createContainer();
this.core.components.set('notifications', this);
// Enregistrer dans le core pour accès global
this.core.on('notification:show', (event) => {
this.show(event.detail);
});
}
/**
* Créer le conteneur pour les notifications
*/
createContainer() {
if (this.container) return;
const positions = {
'top-right': 'fixed top-4 right-4 z-50',
'top-left': 'fixed top-4 left-4 z-50',
'bottom-right': 'fixed bottom-4 right-4 z-50',
'bottom-left': 'fixed bottom-4 left-4 z-50',
'top-center': 'fixed top-4 left-1/2 transform -translate-x-1/2 z-50',
'bottom-center': 'fixed bottom-4 left-1/2 transform -translate-x-1/2 z-50'
};
this.container = this.core.utils.createElement('div', {
id: 'notytex-notifications',
className: `${positions[this.config.position]} space-y-2 pointer-events-none`
});
document.body.appendChild(this.container);
}
/**
* Afficher une notification
*/
show(options = {}) {
const notification = {
id: this.nextId++,
type: options.type || 'info',
title: options.title || '',
message: options.message || '',
duration: options.duration || this.config.duration,
actions: options.actions || [],
persistent: options.persistent || false,
...options
};
// Limiter le nombre de notifications visibles
if (this.notifications.size >= this.config.maxVisible) {
const oldestId = Array.from(this.notifications.keys())[0];
this.hide(oldestId);
}
this.notifications.set(notification.id, notification);
this.render(notification);
// Auto-masquage si non persistant
if (!notification.persistent && notification.duration > 0) {
setTimeout(() => {
this.hide(notification.id);
}, notification.duration);
}
return notification.id;
}
/**
* Afficher types spécifiques (méthodes de convenance)
*/
success(message, title = 'Succès', options = {}) {
return this.show({ type: 'success', message, title, ...options });
}
error(message, title = 'Erreur', options = {}) {
return this.show({ type: 'error', message, title, ...options });
}
warning(message, title = 'Attention', options = {}) {
return this.show({ type: 'warning', message, title, ...options });
}
info(message, title = 'Information', options = {}) {
return this.show({ type: 'info', message, title, ...options });
}
/**
* Masquer une notification
*/
hide(notificationId) {
const notification = this.notifications.get(notificationId);
if (!notification) return;
const element = document.getElementById(`notification-${notificationId}`);
if (element) {
// Animation de sortie
element.style.transition = 'all 300ms cubic-bezier(0.4, 0, 0.2, 1)';
element.style.transform = 'translateX(100%)';
element.style.opacity = '0';
setTimeout(() => {
if (element.parentNode) {
element.parentNode.removeChild(element);
}
}, 300);
}
this.notifications.delete(notificationId);
}
/**
* Masquer toutes les notifications
*/
hideAll() {
Array.from(this.notifications.keys()).forEach(id => {
this.hide(id);
});
}
/**
* Rendu d'une notification
*/
render(notification) {
const typeConfig = this.getTypeConfig(notification.type);
const element = this.core.utils.createElement('div', {
id: `notification-${notification.id}`,
className: `pointer-events-auto max-w-sm w-full ${typeConfig.bgColor} border ${typeConfig.borderColor} shadow-lg rounded-lg overflow-hidden transform transition-all duration-300 translate-x-full opacity-0`,
role: 'alert',
'aria-live': 'polite'
});
const content = this.core.utils.createElement('div', {
className: 'p-4'
});
const header = this.core.utils.createElement('div', {
className: 'flex items-start'
});
// Icône
const iconContainer = this.core.utils.createElement('div', {
className: 'flex-shrink-0'
});
iconContainer.innerHTML = typeConfig.icon;
// Contenu
const textContent = this.core.utils.createElement('div', {
className: 'ml-3 w-0 flex-1'
});
if (notification.title) {
const title = this.core.utils.createElement('p', {
className: `text-sm font-medium ${typeConfig.textColor}`
});
title.textContent = notification.title;
textContent.appendChild(title);
}
if (notification.message) {
const message = this.core.utils.createElement('p', {
className: `text-sm ${typeConfig.textColor} ${notification.title ? 'mt-1' : ''}`
});
message.textContent = notification.message;
textContent.appendChild(message);
}
// Actions
if (notification.actions && notification.actions.length > 0) {
const actionsContainer = this.core.utils.createElement('div', {
className: 'mt-3 flex space-x-2'
});
notification.actions.forEach(action => {
const button = this.core.utils.createElement('button', {
className: `text-sm font-medium ${typeConfig.actionColor} hover:${typeConfig.actionHoverColor} transition-colors`,
onclick: action.handler
});
button.textContent = action.text;
actionsContainer.appendChild(button);
});
textContent.appendChild(actionsContainer);
}
// Bouton de fermeture
const closeButton = this.core.utils.createElement('div', {
className: 'ml-4 flex-shrink-0 flex'
});
const closeBtn = this.core.utils.createElement('button', {
className: `rounded-md inline-flex ${typeConfig.textColor} hover:${typeConfig.closeHoverColor} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500`,
onclick: () => this.hide(notification.id)
});
closeBtn.innerHTML = `
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
`;
closeButton.appendChild(closeBtn);
// Assemblage
header.appendChild(iconContainer);
header.appendChild(textContent);
header.appendChild(closeButton);
content.appendChild(header);
element.appendChild(content);
this.container.appendChild(element);
// Animation d'entrée
requestAnimationFrame(() => {
element.classList.remove('translate-x-full', 'opacity-0');
});
return element;
}
/**
* Configuration des types de notification
*/
getTypeConfig(type) {
const configs = {
success: {
bgColor: 'bg-green-50',
borderColor: 'border-green-200',
textColor: 'text-green-800',
actionColor: 'text-green-600',
actionHoverColor: 'text-green-500',
closeHoverColor: 'text-green-500',
icon: `<svg class="w-5 h-5 text-green-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>`
},
error: {
bgColor: 'bg-red-50',
borderColor: 'border-red-200',
textColor: 'text-red-800',
actionColor: 'text-red-600',
actionHoverColor: 'text-red-500',
closeHoverColor: 'text-red-500',
icon: `<svg class="w-5 h-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>`
},
warning: {
bgColor: 'bg-orange-50',
borderColor: 'border-orange-200',
textColor: 'text-orange-800',
actionColor: 'text-orange-600',
actionHoverColor: 'text-orange-500',
closeHoverColor: 'text-orange-500',
icon: `<svg class="w-5 h-5 text-orange-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/>
</svg>`
},
info: {
bgColor: 'bg-blue-50',
borderColor: 'border-blue-200',
textColor: 'text-blue-800',
actionColor: 'text-blue-600',
actionHoverColor: 'text-blue-500',
closeHoverColor: 'text-blue-500',
icon: `<svg class="w-5 h-5 text-blue-400" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg>`
}
};
return configs[type] || configs.info;
}
}
export default NotificationSystem;