import type { Component, VNode } from 'vue'; import type { ToastProps } from '.'; import { computed, shallowReactive } from 'vue'; const TOAST_LIMIT = 1; const TOAST_REMOVE_DELAY = 1000000; export type StringOrVNode = | string | VNode | (() => VNode); type ToasterToast = ToastProps & { id: string; title?: string; description?: StringOrVNode; action?: Component; }; const actionTypes = { ADD_TOAST: 'ADD_TOAST', UPDATE_TOAST: 'UPDATE_TOAST', DISMISS_TOAST: 'DISMISS_TOAST', REMOVE_TOAST: 'REMOVE_TOAST', } as const; let count = 0; function genId() { count = (count + 1) % Number.MAX_VALUE; return count.toString(); } type ActionType = typeof actionTypes; type Action = | { type: ActionType['ADD_TOAST']; toast: ToasterToast; } | { type: ActionType['UPDATE_TOAST']; toast: Partial; } | { type: ActionType['DISMISS_TOAST']; toastId?: ToasterToast['id']; } | { type: ActionType['REMOVE_TOAST']; toastId?: ToasterToast['id']; }; interface State { toasts: ToasterToast[]; } const toastTimeouts = new Map>(); function addToRemoveQueue(toastId: string) { if (toastTimeouts.has(toastId)) return; const timeout = setTimeout(() => { toastTimeouts.delete(toastId); dispatch({ type: actionTypes.REMOVE_TOAST, toastId, }); }, TOAST_REMOVE_DELAY); toastTimeouts.set(toastId, timeout); } const state = shallowReactive({ toasts: [], }); function dispatch(action: Action) { switch (action.type) { case actionTypes.ADD_TOAST: state.toasts = [action.toast].slice(0, TOAST_LIMIT); break; case actionTypes.UPDATE_TOAST: state.toasts = state.toasts.map(t => t.id === action.toast.id ? { ...t, ...action.toast } : t, ); break; case actionTypes.DISMISS_TOAST: { const { toastId } = action; if (toastId) { addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { addToRemoveQueue(toast.id); }); } state.toasts = state.toasts.map(t => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t, ); break; } case actionTypes.REMOVE_TOAST: if (action.toastId === undefined) state.toasts = []; else state.toasts = state.toasts.filter(t => t.id !== action.toastId); break; } } function useToast() { return { toasts: computed(() => state.toasts), toast, dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), }; } type Toast = Omit; function toast(props: Toast) { const id = genId(); const update = (props: ToasterToast) => dispatch({ type: actionTypes.UPDATE_TOAST, toast: { ...props, id }, }); const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }); dispatch({ type: actionTypes.ADD_TOAST, toast: { ...props, id, open: true, onOpenChange: (open: boolean) => { if (!open) dismiss(); }, }, }); return { id, dismiss, update, }; } export { toast, useToast };