You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
162 lines
4.2 KiB
162 lines
4.2 KiB
import { Controller } from '@hotwired/stimulus'; |
|
|
|
/** |
|
* Central toast notification controller |
|
* |
|
* Usage from other controllers: |
|
* |
|
* // Get the toast controller instance |
|
* const toastController = this.application.getControllerForElementAndIdentifier( |
|
* document.querySelector('[data-controller~="utility--toast"]'), |
|
* 'utility--toast' |
|
* ); |
|
* |
|
* // Show a toast |
|
* toastController.show('Success!', 'success'); |
|
* toastController.show('Error occurred', 'danger'); |
|
* toastController.show('Processing...', 'info'); |
|
* toastController.show('Warning!', 'warning'); |
|
* |
|
* // Or use the global helper (recommended) |
|
* window.showToast('Success!', 'success'); |
|
*/ |
|
export default class extends Controller { |
|
static targets = ['container']; |
|
|
|
connect() { |
|
console.log('Toast controller connected'); |
|
this.queue = []; |
|
this.currentToast = null; |
|
this.isProcessing = false; |
|
|
|
// Expose globally for easy access from any controller |
|
window.showToast = (message, type = 'info', duration = 4000) => { |
|
this.show(message, type, duration); |
|
}; |
|
} |
|
|
|
disconnect() { |
|
// Clean up global reference |
|
if (window.showToast) { |
|
delete window.showToast; |
|
} |
|
} |
|
|
|
/** |
|
* Show a toast notification |
|
* @param {string} message - The message to display |
|
* @param {string} type - Type of toast: 'success', 'danger', 'warning', 'info' |
|
* @param {number} duration - How long to show the toast in milliseconds (default: 4000) |
|
*/ |
|
show(message, type = 'info', duration = 4000) { |
|
this.queue.push({ message, type, duration }); |
|
this.processQueue(); |
|
} |
|
|
|
/** |
|
* Process the toast queue one at a time |
|
*/ |
|
processQueue() { |
|
// If already showing a toast, wait |
|
if (this.isProcessing) { |
|
return; |
|
} |
|
|
|
// If queue is empty, nothing to do |
|
if (this.queue.length === 0) { |
|
return; |
|
} |
|
|
|
this.isProcessing = true; |
|
const { message, type, duration } = this.queue.shift(); |
|
|
|
// Create toast element |
|
const toast = this.createToastElement(message, type); |
|
this.containerTarget.appendChild(toast); |
|
this.currentToast = toast; |
|
|
|
// Trigger animation after a small delay (for CSS transition) |
|
requestAnimationFrame(() => { |
|
requestAnimationFrame(() => { |
|
toast.classList.add('toast--show'); |
|
}); |
|
}); |
|
|
|
// Auto-dismiss after duration |
|
setTimeout(() => { |
|
this.dismissToast(toast); |
|
}, duration); |
|
} |
|
|
|
/** |
|
* Create a toast DOM element |
|
* @param {string} message |
|
* @param {string} type |
|
* @returns {HTMLElement} |
|
*/ |
|
createToastElement(message, type) { |
|
const toast = document.createElement('div'); |
|
toast.className = `toast toast--${type}`; |
|
toast.setAttribute('role', 'alert'); |
|
toast.setAttribute('aria-live', 'polite'); |
|
toast.setAttribute('aria-atomic', 'true'); |
|
|
|
const content = document.createElement('div'); |
|
content.className = 'toast__content'; |
|
content.textContent = message; |
|
|
|
const closeButton = document.createElement('button'); |
|
closeButton.className = 'toast__close'; |
|
closeButton.setAttribute('type', 'button'); |
|
closeButton.setAttribute('aria-label', 'Close'); |
|
closeButton.innerHTML = '×'; |
|
closeButton.addEventListener('click', () => this.dismissToast(toast)); |
|
|
|
toast.appendChild(content); |
|
toast.appendChild(closeButton); |
|
|
|
return toast; |
|
} |
|
|
|
/** |
|
* Dismiss a toast with animation |
|
* @param {HTMLElement} toast |
|
*/ |
|
dismissToast(toast) { |
|
if (!toast || !toast.parentNode) { |
|
this.isProcessing = false; |
|
this.processQueue(); |
|
return; |
|
} |
|
|
|
// Start fade out animation |
|
toast.classList.remove('toast--show'); |
|
toast.classList.add('toast--hide'); |
|
|
|
// Remove from DOM after animation completes |
|
setTimeout(() => { |
|
if (toast.parentNode) { |
|
toast.remove(); |
|
} |
|
this.currentToast = null; |
|
this.isProcessing = false; |
|
|
|
// Process next toast in queue |
|
this.processQueue(); |
|
}, 300); // Match CSS transition duration |
|
} |
|
|
|
/** |
|
* Clear all toasts immediately |
|
*/ |
|
clearAll() { |
|
this.queue = []; |
|
const toasts = this.containerTarget.querySelectorAll('.toast'); |
|
toasts.forEach(toast => { |
|
toast.remove(); |
|
}); |
|
this.currentToast = null; |
|
this.isProcessing = false; |
|
} |
|
} |
|
|
|
|