import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue'; export interface UseConfirmOptions { /** Auto cancel timeout in ms (default: 3000, set 0 to disable) */ timeout?: number; /** Callback when confirmed */ onConfirm?: (id: T) => void | Promise; /** Callback when cancelled (timeout or manual) */ onCancel?: (id: T) => void; } export interface UseConfirmReturn { /** Current confirming id (readonly) */ confirmId: DeepReadonly>; /** Whether confirm action is executing */ isPending: DeepReadonly>; /** Check if a specific id is in confirming state */ isConfirming: (id: T) => boolean; /** Start confirming state (with auto timeout) */ startConfirm: (id: T) => void; /** Request confirmation (toggle between request and execute) */ requestConfirm: (id: T) => Promise; /** Manually confirm current id */ confirm: () => Promise; /** Cancel confirmation */ cancel: () => void; /** Reset without triggering callbacks */ reset: () => void; } /** * Composable for handling confirm actions (e.g., delete confirmation) * * @example * ```ts * // Basic usage * const { isConfirming, requestConfirm } = useConfirm({ * timeout: 3000, * onConfirm: async (id) => { await deleteItem(id) } * }) * * // In template * * * // With loading state * const { isPending, requestConfirm } = useConfirm({ ... }) * * ``` */ export function useConfirm( options: UseConfirmOptions = {} ): UseConfirmReturn { const { timeout = 3000, onConfirm, onCancel } = options; const confirmId = ref(null) as Ref; const isPending = ref(false); let timeoutId: ReturnType | null = null; const clearTimer = (): void => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } }; /** * Check if a specific id is in confirming state */ const isConfirming = (id: T): boolean => { return confirmId.value === id; }; /** * Start confirming state for an id (with auto timeout) */ const startConfirm = (id: T): void => { clearTimer(); confirmId.value = id; // Auto cancel after timeout (0 = disabled) if (timeout > 0) { timeoutId = setTimeout(() => { if (confirmId.value === id) { confirmId.value = null; onCancel?.(id); } }, timeout); } }; /** * Request confirmation for an id * - First click: enter confirming state * - Second click: execute confirm action * @returns true if confirmed, false if entered confirming state */ const requestConfirm = async (id: T): Promise => { // Prevent action while pending if (isPending.value) return false; if (confirmId.value === id) { // Already confirming, execute action clearTimer(); isPending.value = true; try { await onConfirm?.(id); return true; } finally { confirmId.value = null; isPending.value = false; } } else { // Enter confirming state startConfirm(id); return false; } }; /** * Manually confirm the current id */ const confirm = async (): Promise => { if (confirmId.value === null || isPending.value) return; clearTimer(); const id = confirmId.value; isPending.value = true; try { await onConfirm?.(id); } finally { confirmId.value = null; isPending.value = false; } }; /** * Cancel the confirming state */ const cancel = (): void => { if (confirmId.value === null) return; const id = confirmId.value; clearTimer(); confirmId.value = null; onCancel?.(id); }; /** * Reset state without triggering callbacks */ const reset = (): void => { clearTimer(); confirmId.value = null; isPending.value = false; }; // Cleanup on unmount onUnmounted(clearTimer); return { confirmId: readonly(confirmId), isPending: readonly(isPending), isConfirming, startConfirm, requestConfirm, confirm, cancel, reset }; }