| |
| |
| |
| |
|
|
| 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; |
|
|