Files
SteelBase/public/js/ui/toast-manager.js

571 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
/**
* ToastManager - Sistema de Notificações Visuais
* Responsável por exibir mensagens de feedback ao usuário de forma elegante e não-intrusiva
*/
class ToastManager {
constructor() {
this.container = null;
this.toasts = new Map();
this.config = {
position: 'top-right',
maxToasts: 5,
duration: {
success: 3000,
error: 5000,
warning: 4000,
info: 3000
}
};
this.init();
}
/**
* Inicializa o ToastManager criando o container
*/
init() {
this.createContainer();
console.log('🍞 ToastManager inicializado');
}
/**
* Cria o container de toasts no DOM
*/
createContainer() {
// Remover container existente se houver
const existingContainer = document.querySelector('.toast-container');
if (existingContainer) {
existingContainer.remove();
}
// Criar novo container
this.container = document.createElement('div');
this.container.className = `toast-container toast-${this.config.position}`;
// Adicionar estilos CSS
this.addStyles();
// Adicionar ao body
document.body.appendChild(this.container);
}
/**
* Adiciona estilos CSS para os toasts
*/
addStyles() {
// Verificar se os estilos já existem
if (document.querySelector('#toast-styles')) {
return;
}
const styles = document.createElement('style');
styles.id = 'toast-styles';
styles.textContent = `
.toast-container {
position: fixed;
z-index: 10000;
pointer-events: none;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 400px;
width: 100%;
}
.toast-top-right {
top: 20px;
right: 20px;
}
.toast-top-left {
top: 20px;
left: 20px;
}
.toast-bottom-right {
bottom: 20px;
right: 20px;
}
.toast-bottom-left {
bottom: 20px;
left: 20px;
}
.toast-top-center {
top: 20px;
left: 50%;
transform: translateX(-50%);
}
.toast-bottom-center {
bottom: 20px;
left: 50%;
transform: translateX(-50%);
}
.toast {
background: var(--color-bg-2);
color: var(--color-text);
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-left: 4px solid var(--color-primary);
display: flex;
align-items: flex-start;
gap: 12px;
animation: toast-in 0.3s ease-out;
pointer-events: auto;
position: relative;
overflow: hidden;
}
.toast-success {
border-left-color: #10b981;
background: linear-gradient(135deg, #10b98120 0%, transparent 100%);
}
.toast-error {
border-left-color: #ef4444;
background: linear-gradient(135deg, #ef444420 0%, transparent 100%);
}
.toast-warning {
border-left-color: #f59e0b;
background: linear-gradient(135deg, #f59e0b20 0%, transparent 100%);
}
.toast-info {
border-left-color: #3b82f6;
background: linear-gradient(135deg, #3b82f620 0%, transparent 100%);
}
.toast-icon {
flex-shrink: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.toast-content {
flex: 1;
min-width: 0;
}
.toast-title {
font-weight: 600;
margin-bottom: 4px;
font-size: 14px;
}
.toast-message {
font-size: 13px;
line-height: 1.4;
opacity: 0.9;
}
.toast-close {
flex-shrink: 0;
width: 20px;
height: 20px;
border: none;
background: transparent;
color: var(--color-text);
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
padding: 0;
}
.toast-close:hover {
opacity: 1;
}
.toast-progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: var(--color-primary);
opacity: 0.3;
animation: toast-progress linear forwards;
}
.toast-success .toast-progress {
background: #10b981;
}
.toast-error .toast-progress {
background: #ef4444;
}
.toast-warning .toast-progress {
background: #f59e0b;
}
.toast-info .toast-progress {
background: #3b82f6;
}
.toast.removing {
animation: toast-out 0.3s ease-in forwards;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes toast-out {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(100%);
}
}
@keyframes toast-progress {
from {
width: 100%;
}
to {
width: 0%;
}
}
/* Responsividade */
@media (max-width: 480px) {
.toast-container {
left: 10px !important;
right: 10px !important;
max-width: none;
}
.toast {
padding: 14px 16px;
}
}
`;
document.head.appendChild(styles);
}
/**
* Mostra um toast genérico
* @param {string} message - Mensagem a exibir
* @param {string} type - Tipo do toast (success, error, warning, info)
* @param {number} duration - Duração em milissegundos
* @param {Object} options - Opções adicionais
*/
show(message, type = 'info', duration = null, options = {}) {
// Limitar número de toasts simultâneos
if (this.toasts.size >= this.config.maxToasts) {
// Remover o toast mais antigo
const oldestToast = this.toasts.values().next().value;
if (oldestToast) {
this.removeToast(oldestToast.id);
}
}
const toastId = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const toastDuration = duration || this.config.duration[type] || 3000;
const toast = this.createToastElement(toastId, message, type, options);
this.container.appendChild(toast);
const toastData = {
id: toastId,
element: toast,
type: type,
message: message,
startTime: Date.now(),
duration: toastDuration
};
this.toasts.set(toastId, toastData);
// Configurar remoção automática
if (toastDuration > 0) {
setTimeout(() => {
this.removeToast(toastId);
}, toastDuration);
// Configurar barra de progresso
const progressBar = toast.querySelector('.toast-progress');
if (progressBar) {
progressBar.style.animationDuration = `${toastDuration}ms`;
}
}
console.log(`🍞 Toast ${type}: ${message}`);
return toastId;
}
/**
* Cria o elemento HTML do toast
* @param {string} id - ID do toast
* @param {string} message - Mensagem
* @param {string} type - Tipo do toast
* @param {Object} options - Opções adicionais
* @returns {HTMLElement} Elemento do toast
*/
createToastElement(id, message, type, options = {}) {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.id = id;
// Ícone baseado no tipo
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
const icon = options.icon || icons[type] || '💬';
const title = options.title || this.getTitleByType(type);
toast.innerHTML = `
<div class="toast-icon">${icon}</div>
<div class="toast-content">
${title ? `<div class="toast-title">${title}</div>` : ''}
<div class="toast-message">${this.escapeHtml(message)}</div>
</div>
<button class="toast-close" onclick="window.toastManager.removeToast('${id}')">✕</button>
<div class="toast-progress"></div>
`;
// Adicionar eventos de hover
toast.addEventListener('mouseenter', () => {
const progressBar = toast.querySelector('.toast-progress');
if (progressBar) {
progressBar.style.animationPlayState = 'paused';
}
});
toast.addEventListener('mouseleave', () => {
const progressBar = toast.querySelector('.toast-progress');
if (progressBar) {
progressBar.style.animationPlayState = 'running';
}
});
// Permitir clique para fechar (exceto no botão de fechar)
toast.addEventListener('click', (e) => {
if (!e.target.classList.contains('toast-close')) {
this.removeToast(id);
}
});
return toast;
}
/**
* Obtém título baseado no tipo
* @param {string} type - Tipo do toast
* @returns {string} Título
*/
getTitleByType(type) {
const titles = {
success: 'Sucesso!',
error: 'Erro!',
warning: 'Atenção!',
info: 'Informação'
};
return titles[type] || '';
}
/**
* Remove um toast específico
* @param {string} toastId - ID do toast a remover
*/
removeToast(toastId) {
const toastData = this.toasts.get(toastId);
if (!toastData) {
return;
}
const toast = toastData.element;
toast.classList.add('removing');
// Remover após animação
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
this.toasts.delete(toastId);
}, 300);
}
/**
* Remove todos os toasts ativos
*/
removeAllToasts() {
const toastIds = Array.from(this.toasts.keys());
toastIds.forEach(id => this.removeToast(id));
}
/**
* Escapa HTML para prevenir XSS
* @param {string} text - Texto a escapar
* @returns {string} Texto escapado
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Métodos de conveniência
/**
* Mostra um toast de sucesso
* @param {string} message - Mensagem
* @param {number} duration - Duração em milissegundos
* @param {Object} options - Opções adicionais
*/
success(message, duration = null, options = {}) {
return this.show(message, 'success', duration, options);
}
/**
* Mostra um toast de erro
* @param {string} message - Mensagem
* @param {number} duration - Duração em milissegundos
* @param {Object} options - Opções adicionais
*/
error(message, duration = null, options = {}) {
return this.show(message, 'error', duration, options);
}
/**
* Mostra um toast de aviso
* @param {string} message - Mensagem
* @param {number} duration - Duração em milissegundos
* @param {Object} options - Opções adicionais
*/
warning(message, duration = null, options = {}) {
return this.show(message, 'warning', duration, options);
}
/**
* Mostra um toast de informação
* @param {string} message - Mensagem
* @param {number} duration - Duração em milissegundos
* @param {Object} options - Opções adicionais
*/
info(message, duration = null, options = {}) {
return this.show(message, 'info', duration, options);
}
/**
* Mostra um toast de loading (persistente)
* @param {string} message - Mensagem
* @param {Object} options - Opções adicionais
*/
loading(message = 'Carregando...', options = {}) {
const toastId = this.show(message, 'info', 0, {
icon: '⏳',
title: 'Aguarde...',
...options
});
// Remover barra de progresso para loading persistente
const toast = this.toasts.get(toastId);
if (toast) {
const progressBar = toast.element.querySelector('.toast-progress');
if (progressBar) {
progressBar.remove();
}
}
return toastId;
}
/**
* Atualiza o conteúdo de um toast existente
* @param {string} toastId - ID do toast
* @param {string} message - Nova mensagem
* @param {string} type - Novo tipo (opcional)
*/
updateToast(toastId, message, type = null) {
const toastData = this.toasts.get(toastId);
if (!toastData) {
return false;
}
const toast = toastData.element;
const messageElement = toast.querySelector('.toast-message');
if (messageElement) {
messageElement.textContent = message;
}
if (type && type !== toastData.type) {
toast.className = toast.className.replace(`toast-${toastData.type}`, `toast-${type}`);
toastData.type = type;
// Atualizar ícone
const iconElement = toast.querySelector('.toast-icon');
if (iconElement) {
const icons = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
iconElement.textContent = icons[type] || '💬';
}
}
return true;
}
/**
* Configura o ToastManager
* @param {Object} config - Configurações
*/
setConfig(config) {
this.config = { ...this.config, ...config };
// Recriar container se a posição mudou
if (config.position) {
this.createContainer();
}
}
/**
* Obtém estatísticas dos toasts
* @returns {Object} Estatísticas
*/
getStats() {
return {
activeToasts: this.toasts.size,
maxToasts: this.config.maxToasts,
position: this.config.position
};
}
}
// Criar instância global
window.toastManager = new ToastManager();
console.log('🍞 ToastManager inicializado com sucesso');