Files
voidraft/frontend/src/composables/useConfirm.ts
2025-12-14 02:19:50 +08:00

175 lines
4.4 KiB
TypeScript

import { ref, readonly, onUnmounted, type Ref, type DeepReadonly } from 'vue';
export interface UseConfirmOptions<T extends string | number = string | number> {
/** Auto cancel timeout in ms (default: 3000, set 0 to disable) */
timeout?: number;
/** Callback when confirmed */
onConfirm?: (id: T) => void | Promise<void>;
/** Callback when cancelled (timeout or manual) */
onCancel?: (id: T) => void;
}
export interface UseConfirmReturn<T extends string | number = string | number> {
/** Current confirming id (readonly) */
confirmId: DeepReadonly<Ref<T | null>>;
/** Whether confirm action is executing */
isPending: DeepReadonly<Ref<boolean>>;
/** 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<boolean>;
/** Manually confirm current id */
confirm: () => Promise<void>;
/** 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
* <button @click="requestConfirm('delete')">
* {{ isConfirming('delete') ? 'Confirm?' : 'Delete' }}
* </button>
*
* // With loading state
* const { isPending, requestConfirm } = useConfirm({ ... })
* <button :disabled="isPending" @click="requestConfirm('id')">
* {{ isPending ? 'Processing...' : 'Delete' }}
* </button>
* ```
*/
export function useConfirm<T extends string | number = string | number>(
options: UseConfirmOptions<T> = {}
): UseConfirmReturn<T> {
const { timeout = 3000, onConfirm, onCancel } = options;
const confirmId = ref<T | null>(null) as Ref<T | null>;
const isPending = ref(false);
let timeoutId: ReturnType<typeof setTimeout> | 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<boolean> => {
// 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<void> => {
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
};
}