/** * `materialSnackbar`: the queue behind ``, and `window.materialToast()`. * * One snackbar at a time, as M3 shows them. Each waits its turn, stays for its timeout (paused * while hovered or focused, so it is never pulled away from someone reading or reaching for its * action) and is replaced by the next. */ const DEFAULT_TIMEOUT_MS = 4000 let sequence = 0 // The listener lives here, not on the Alpine component: a toast dispatched before Alpine has // started (on page load, straight after a redirect) would otherwise be lost. Until a host // registers, toasts wait in `pending`. let host = null const pending = [] window.addEventListener('toast', (event) => (host ? host.add(event.detail) : pending.push(event.detail))) window.materialToast = (title, options = {}) => { window.dispatchEvent(new CustomEvent('toast', { detail: { title, ...options } })) } document.addEventListener('alpine:init', () => { window.Alpine.data('materialSnackbar', () => ({ queue: [], current: null, timer: null, remaining: 0, startedAt: 0, init() { host = this pending.splice(0).forEach((detail) => this.add(detail)) }, destroy() { if (host === this) { host = null } }, add(detail) { // Livewire dispatches named arguments as the detail object; a positional dispatch // arrives as an array whose first entry is that object. const toast = Array.isArray(detail) ? detail[0] : detail this.queue.push({ id: ++sequence, type: toast.type ?? null, title: toast.title ?? '', description: toast.description ?? null, timeout: toast.timeout === 0 || toast.timeout === null ? 0 : (toast.timeout ?? DEFAULT_TIMEOUT_MS), action: toast.action ?? null, }) if (!this.current) { this.next() } }, next() { clearTimeout(this.timer) this.current = this.queue.shift() ?? null if (this.current?.timeout) { this.remaining = this.current.timeout this.resume() } }, pause() { if (!this.current?.timeout || !this.timer) { return } clearTimeout(this.timer) this.timer = null this.remaining -= performance.now() - this.startedAt }, resume() { if (!this.current?.timeout || this.timer) { return } this.startedAt = performance.now() this.timer = setTimeout(() => { this.timer = null this.next() }, Math.max(this.remaining, 0)) }, dismiss() { this.timer = null this.next() }, act() { this.current?.action?.handler?.() this.dismiss() }, icon(type) { return ['success', 'error', 'warning', 'info'].includes(type) }, })) })