|
- import { createContext, useCallback, useContext, useMemo, useState } from 'react'
-
- import { Button } from '@/components/ui/button'
- import { Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle } from '@/components/ui/dialog'
-
- import type { FC, ReactNode } from 'react'
-
- type DialogueVariant = 'default' | 'danger'
-
- type ConfirmOptions = { title: string
- description?: ReactNode
- confirmText?: string
- cancelText?: string
- variant?: DialogueVariant }
-
- type AlertOptions = { title: string
- description?: ReactNode
- okText?: string }
-
- type Choice<T extends string> = { value: T
- label: string
- variant?: DialogueVariant }
-
- type ChoiceOptions<T extends string> = { title: string
- description?: ReactNode
- choices: Choice<T>[]
- cancelText?: string }
-
- type DialogueRequest =
- | { id: number
- kind: 'confirm'
- options: ConfirmOptions
- resolve: (value: boolean) => void }
- | { id: number
- kind: 'alert'
- options: AlertOptions
- resolve: () => void }
- | { id: number
- kind: 'choice'
- options: ChoiceOptions<string>
- resolve: (value: string | null) => void }
-
- type DialogueAPI =
- { confirm: (options: ConfirmOptions) => Promise<boolean>
- alert: (options: AlertOptions) => Promise<void>
- choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
-
- const DialogueContext = createContext<DialogueAPI | null> (null)
-
- let nextDialogueId = 1
-
- type Props = { children: ReactNode }
-
-
- const DialogueProvider: FC<Props> = ({ children }) => {
- const [queue, setQueue] = useState<DialogueRequest[]> ([])
-
- const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
- const id = nextDialogueId
- ++nextDialogueId
-
- setQueue (q => [...q, { ...request, id } as DialogueRequest])
- }, [])
-
- const closeActive = useCallback ((result?: unknown) => {
- setQueue (q => {
- const [active, ...rest] = q
-
- if (!(active))
- return rest
-
- switch (active.kind)
- {
- case 'confirm':
- active.resolve (Boolean (result))
- break
-
- case 'alert':
- active.resolve ()
- break
-
- case 'choice':
- active.resolve ((result ?? null) as string | null)
- break
- }
-
- return rest
- })
- }, [])
-
- const api = useMemo<DialogueAPI> (() => ({
- confirm: options => new Promise<boolean> (resolve => {
- push ({ kind: 'confirm', options, resolve })
- }),
- alert: options => new Promise<void> (resolve => {
- push ({ kind: 'alert', options, resolve })
- }),
- choice: options => new Promise (resolve => {
- push ({ kind: 'choice',
- options: options as ChoiceOptions<string>,
- resolve: resolve as (value: string | null) => void })
- }) }), [push])
-
- const active = queue[0]
-
- return (
- <DialogueContext.Provider value={api}>
- {children}
-
- <Dialog
- open={Boolean (active)}
- onOpenChange={open => {
- if (!(open))
- closeActive (active?.kind !== 'confirm' && null)
- }}>
- {active && (
- <DialogContent className="px-6 pb-6 pt-7">
- <DialogHeader className="pl-8">
- <DialogTitle>{active.options.title}</DialogTitle>
-
- {active.options.description && (
- <DialogDescription asChild>
- <div>{active.options.description}</div>
- </DialogDescription>)}
- </DialogHeader>
-
- <DialogFooter>
- {active.kind === 'confirm' && (
- <>
- <Button
- variant="outline"
- onClick={() => closeActive (false)}>
- {active.options.cancelText ?? '取消'}
- </Button>
-
- <Button
- variant={(active.options.variant === 'danger')
- ? 'destructive'
- : 'default'}
- onClick={() => closeActive (true)}>
- {active.options.confirmText ?? '確定'}
- </Button>
- </>)}
-
- {active.kind === 'alert' && (
- <Button onClick={() => closeActive ()}>
- {active.options.okText ?? '確定'}
- </Button>)}
-
- {active.kind === 'choice' && (
- <>
- <Button
- variant="outline"
- onClick={() => closeActive (null)}>
- {active.options.cancelText ?? '取消'}
- </Button>
-
- {active.options.choices.map (choice => (
- <Button
- key={choice.value}
- variant={(choice.variant === 'danger')
- ? 'destructive'
- : 'default'}
- onClick={() => closeActive (choice.value)}>
- {choice.label}
- </Button>))}
- </>)}
- </DialogFooter>
- </DialogContent>)}
- </Dialog>
- </DialogueContext.Provider>)
- }
-
-
- export const useDialogue = () => {
- const dialogue = useContext (DialogueContext)
-
- if (!(dialogue))
- throw new Error ('useDialogue must be used inside DialogueProvider')
-
- return dialogue
- }
-
- export default DialogueProvider
|