import { html, render } from "/js/lib/lit-html.js"; import { alertIconTemplate } from "/js/templates/icons/alertIcon.template.js"; import { circleCheckIconTemplate } from "/js/templates/icons/circleCheckIcon.template.js"; import { infoIconTemplate } from "/js/templates/icons/infoIcon.template.js"; import { wait, raf } from "/js/utils.js"; const TOAST_GAP_PX = 8; const activeToasts = []; const STYLE_ICONS = { default: circleCheckIconTemplate, success: circleCheckIconTemplate, error: alertIconTemplate, warning: alertIconTemplate, info: infoIconTemplate, }; function restackToasts() { let offset = 0; for (const entry of activeToasts) { entry.element.style.setProperty("--toast-stack-offset", `${offset}px`); offset += entry.height + TOAST_GAP_PX; } } function mountToast(toast, { timeout = 3000, onDismiss = () => {} } = {}) { toast.setAttribute("popover", "manual"); document.body.appendChild(toast); let entry = null; let shown = false; let dismissed = false; let timeoutId = null; function dismiss() { if (dismissed) return; dismissed = true; if (timeoutId != null) clearTimeout(timeoutId); toast.classList.remove("active"); if (entry) { const index = activeToasts.indexOf(entry); if (index !== -1) { activeToasts.splice(index, 1); restackToasts(); } } if (shown) toast.hidePopover(); setTimeout(() => toast.remove(), 1000); onDismiss(); } async function show() { await raf(); await raf(); if (dismissed) { toast.remove(); return; } toast.showPopover(); // this puts the element in the top layer, so it will be displayed above dialogs shown = true; entry = { element: toast, height: toast.offsetHeight }; activeToasts.unshift(entry); restackToasts(); toast.classList.add("active"); if (timeout) { timeoutId = setTimeout(dismiss, timeout); } } show(); return { dismiss, element: toast }; } export function showToast( message, { style = "default", timeout = 3000, iconTemplate } = {}, ) { const toast = document.createElement("div"); toast.classList.add("toast", style); toast.dataset.testid = "toast"; const resolvedIconTemplate = iconTemplate ?? STYLE_ICONS[style] ?? STYLE_ICONS.default; render( html` ${resolvedIconTemplate()} ${message} `, toast, ); return mountToast(toast, { timeout }); } const pluginToasts = new Map(); export function showPluginToast({ pluginRenderer, pluginId, toastId, element, timeout, }) { const key = `${pluginId}:${toastId}`; if (pluginToasts.has(key)) return; const toast = pluginRenderer.createRoot().render(element); const handle = mountToast(toast, { timeout, onDismiss: () => pluginToasts.delete(key), }); pluginToasts.set(key, handle); } export function hidePluginToast({ pluginId, toastId }) { const handle = pluginToasts.get(`${pluginId}:${toastId}`); if (handle) handle.dismiss(); }