UNPKG

@zag-js/toast

Version:

Core logic for the toast widget implemented as a state machine

247 lines (246 loc) • 7.13 kB
// src/toast.store.ts import { compact, runIfFn, uuid, warn } from "@zag-js/utils"; var withDefaults = (options, defaults) => { return { ...defaults, ...compact(options) }; }; var priorities = { error: [1, 2], warning: [3, 6], loading: [4, 5], success: [5, 7], info: [6, 8] }; var DEFAULT_TYPE = "info"; var getPriorityForType = (type, hasAction) => { const [actionable, nonActionable] = priorities[type ?? DEFAULT_TYPE]; return hasAction ? actionable : nonActionable; }; var sortToastsByPriority = (toastArray) => { return toastArray.sort((a, b) => { const priorityA = a.priority ?? getPriorityForType(a.type, !!a.action); const priorityB = b.priority ?? getPriorityForType(b.type, !!b.action); return priorityA - priorityB; }); }; function createToastStore(props = {}) { const attrs = withDefaults(props, { placement: "bottom", overlap: false, max: 24, gap: 16, offsets: "1rem", hotkey: ["altKey", "KeyT"], removeDelay: 200, pauseOnPageIdle: true }); let subscribers = []; let toasts = []; let dismissedToasts = /* @__PURE__ */ new Set(); let toastQueue = []; const subscribe = (subscriber) => { subscribers.push(subscriber); return () => { const index = subscribers.indexOf(subscriber); subscribers.splice(index, 1); }; }; const publish = (data) => { subscribers.forEach((subscriber) => subscriber(data)); return data; }; const addToast = (data) => { if (toasts.length >= attrs.max) { toastQueue.push(data); return; } publish(data); toasts.unshift(data); }; const processQueue = () => { toastQueue = sortToastsByPriority(toastQueue); while (toastQueue.length > 0 && toasts.length < attrs.max) { const nextToast = toastQueue.shift(); if (nextToast) { publish(nextToast); toasts.unshift(nextToast); } } }; const create = (data) => { const id = data.id ?? `toast:${uuid()}`; const exists = toasts.find((toast) => toast.id === id); if (dismissedToasts.has(id)) dismissedToasts.delete(id); if (exists) { toasts = toasts.map((toast) => { if (toast.id === id) { return publish({ ...toast, ...data, id }); } return toast; }); } else { const newToast = { id, duration: attrs.duration, removeDelay: attrs.removeDelay, type: DEFAULT_TYPE, ...data, stacked: !attrs.overlap, gap: attrs.gap }; const priority = newToast.priority ?? getPriorityForType(newToast.type, !!newToast.action); addToast({ ...newToast, priority }); } return id; }; const remove = (id) => { dismissedToasts.add(id); if (!id) { toasts.forEach((toast) => { subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true })); }); toasts = []; toastQueue = []; } else { subscribers.forEach((subscriber) => subscriber({ id, dismiss: true })); toasts = toasts.filter((toast) => toast.id !== id); processQueue(); } return id; }; const error = (data) => { return create({ ...data, type: "error" }); }; const success = (data) => { return create({ ...data, type: "success" }); }; const info = (data) => { return create({ ...data, type: "info" }); }; const warning = (data) => { return create({ ...data, type: "warning" }); }; const loading = (data) => { return create({ ...data, type: "loading" }); }; const getVisibleToasts = () => { return toasts.filter((toast) => !dismissedToasts.has(toast.id)); }; const getCount = () => { return toasts.length; }; const promise = (promise2, options, shared = {}) => { if (!options || !options.loading) { warn("[zag-js > toast] toaster.promise() requires at least a 'loading' option to be specified"); return; } const id = create({ ...shared, ...options.loading, promise: promise2, type: "loading" }); let removable = true; let result; const prom = runIfFn(promise2).then(async (response) => { result = ["resolve", response]; if (isHttpResponse(response) && !response.ok) { removable = false; const errorOptions = runIfFn(options.error, `HTTP Error! status: ${response.status}`); create({ ...shared, ...errorOptions, id, type: "error" }); } else if (options.success !== void 0) { removable = false; const successOptions = runIfFn(options.success, response); create({ ...shared, ...successOptions, id, type: successOptions.type ?? "success" }); } }).catch(async (error2) => { result = ["reject", error2]; if (options.error !== void 0) { removable = false; const errorOptions = runIfFn(options.error, error2); create({ ...shared, ...errorOptions, id, type: "error" }); } }).finally(() => { if (removable) { remove(id); } options.finally?.(); }); const unwrap = () => new Promise( (resolve, reject) => prom.then(() => result[0] === "reject" ? reject(result[1]) : resolve(result[1])).catch(reject) ); return { id, unwrap }; }; const update = (id, data) => { return create({ id, ...data }); }; const pause = (id) => { if (id != null) { toasts = toasts.map((toast) => { if (toast.id === id) return publish({ ...toast, message: "PAUSE" }); return toast; }); } else { toasts = toasts.map((toast) => publish({ ...toast, message: "PAUSE" })); } }; const resume = (id) => { if (id != null) { toasts = toasts.map((toast) => { if (toast.id === id) return publish({ ...toast, message: "RESUME" }); return toast; }); } else { toasts = toasts.map((toast) => publish({ ...toast, message: "RESUME" })); } }; const dismiss = (id) => { if (id != null) { toasts = toasts.map((toast) => { if (toast.id === id) return publish({ ...toast, message: "DISMISS" }); return toast; }); } else { toasts = toasts.map((toast) => publish({ ...toast, message: "DISMISS" })); } }; const isVisible = (id) => { return !dismissedToasts.has(id) && !!toasts.find((toast) => toast.id === id); }; const isDismissed = (id) => { return dismissedToasts.has(id); }; const expand = () => { toasts = toasts.map((toast) => publish({ ...toast, stacked: true })); }; const collapse = () => { toasts = toasts.map((toast) => publish({ ...toast, stacked: false })); }; return { attrs, subscribe, create, update, remove, dismiss, error, success, info, warning, loading, getVisibleToasts, getCount, promise, pause, resume, isVisible, isDismissed, expand, collapse }; } var isHttpResponse = (data) => { return data && typeof data === "object" && "ok" in data && typeof data.ok === "boolean" && "status" in data && typeof data.status === "number"; }; export { createToastStore };