|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { CONFIG } from '../core/config.js';
|
|
|
|
|
|
export class Toast {
|
|
|
static container = null;
|
|
|
static toasts = [];
|
|
|
static maxToasts = CONFIG.TOAST.MAX_VISIBLE;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static init() {
|
|
|
if (this.container) return;
|
|
|
|
|
|
this.container = document.getElementById('toast-container');
|
|
|
if (!this.container) {
|
|
|
this.container = document.createElement('div');
|
|
|
this.container.id = 'toast-container';
|
|
|
this.container.className = 'toast-container';
|
|
|
document.body.appendChild(this.container);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static show(message, type = 'info', options = {}) {
|
|
|
this.init();
|
|
|
|
|
|
const toast = {
|
|
|
id: Date.now() + Math.random(),
|
|
|
message,
|
|
|
type,
|
|
|
duration: options.duration || (type === 'error' ? CONFIG.TOAST.ERROR_DURATION : CONFIG.TOAST.DEFAULT_DURATION),
|
|
|
dismissible: options.dismissible !== false,
|
|
|
action: options.action || null,
|
|
|
};
|
|
|
|
|
|
|
|
|
if (this.toasts.length >= this.maxToasts) {
|
|
|
const oldest = this.toasts.shift();
|
|
|
this.dismiss(oldest.id);
|
|
|
}
|
|
|
|
|
|
this.toasts.push(toast);
|
|
|
this.render(toast);
|
|
|
|
|
|
|
|
|
if (toast.duration > 0) {
|
|
|
setTimeout(() => this.dismiss(toast.id), toast.duration);
|
|
|
}
|
|
|
|
|
|
return toast.id;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static render(toast) {
|
|
|
const el = document.createElement('div');
|
|
|
el.className = `toast toast-${toast.type}`;
|
|
|
el.setAttribute('data-toast-id', toast.id);
|
|
|
el.setAttribute('role', 'alert');
|
|
|
el.setAttribute('aria-live', 'polite');
|
|
|
|
|
|
const icon = this.getIcon(toast.type);
|
|
|
|
|
|
el.innerHTML = `
|
|
|
<div class="toast-icon">${icon}</div>
|
|
|
<div class="toast-content">
|
|
|
<div class="toast-message">${this.escapeHtml(toast.message)}</div>
|
|
|
${toast.action ? `<button class="toast-action">${toast.action.label}</button>` : ''}
|
|
|
</div>
|
|
|
${toast.dismissible ? '<button class="toast-close" aria-label="Close">×</button>' : ''}
|
|
|
${toast.duration > 0 ? `<div class="toast-progress" style="animation-duration: ${toast.duration}ms"></div>` : ''}
|
|
|
`;
|
|
|
|
|
|
|
|
|
if (toast.dismissible) {
|
|
|
const closeBtn = el.querySelector('.toast-close');
|
|
|
closeBtn.addEventListener('click', () => this.dismiss(toast.id));
|
|
|
}
|
|
|
|
|
|
|
|
|
if (toast.action) {
|
|
|
const actionBtn = el.querySelector('.toast-action');
|
|
|
actionBtn.addEventListener('click', () => {
|
|
|
toast.action.callback();
|
|
|
this.dismiss(toast.id);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
this.container.appendChild(el);
|
|
|
|
|
|
|
|
|
setTimeout(() => el.classList.add('toast-show'), 10);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static dismiss(toastId) {
|
|
|
const el = this.container.querySelector(`[data-toast-id="${toastId}"]`);
|
|
|
if (!el) return;
|
|
|
|
|
|
el.classList.remove('toast-show');
|
|
|
el.classList.add('toast-hide');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
if (el.parentNode) {
|
|
|
el.parentNode.removeChild(el);
|
|
|
}
|
|
|
}, 300);
|
|
|
|
|
|
|
|
|
this.toasts = this.toasts.filter(t => t.id !== toastId);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static dismissAll() {
|
|
|
this.toasts.forEach(toast => this.dismiss(toast.id));
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static success(message, options = {}) {
|
|
|
return this.show(message, 'success', options);
|
|
|
}
|
|
|
|
|
|
static error(message, options = {}) {
|
|
|
return this.show(message, 'error', options);
|
|
|
}
|
|
|
|
|
|
static warning(message, options = {}) {
|
|
|
return this.show(message, 'warning', options);
|
|
|
}
|
|
|
|
|
|
static info(message, options = {}) {
|
|
|
return this.show(message, 'info', options);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getIcon(type) {
|
|
|
const icons = {
|
|
|
success: '✅',
|
|
|
error: '❌',
|
|
|
warning: '⚠️',
|
|
|
info: 'ℹ️',
|
|
|
};
|
|
|
return icons[type] || 'ℹ️';
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static escapeHtml(text) {
|
|
|
const div = document.createElement('div');
|
|
|
div.textContent = text;
|
|
|
return div.innerHTML;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export default Toast;
|
|
|
|