ぼざクリタグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

190 lines
4.6 KiB

  1. import { createContext, useCallback, useContext, useMemo, useState } from 'react'
  2. import { Button } from '@/components/ui/button'
  3. import { Dialog,
  4. DialogContent,
  5. DialogDescription,
  6. DialogFooter,
  7. DialogHeader,
  8. DialogTitle } from '@/components/ui/dialog'
  9. import type { FC, ReactNode } from 'react'
  10. type DialogueVariant = 'default' | 'danger'
  11. type ConfirmOptions = { title: string
  12. description?: ReactNode
  13. confirmText?: string
  14. cancelText?: string
  15. variant?: DialogueVariant }
  16. type AlertOptions = { title: string
  17. description?: ReactNode
  18. okText?: string }
  19. type Choice<T extends string> = { value: T
  20. label: string
  21. variant?: DialogueVariant }
  22. type ChoiceOptions<T extends string> = { title: string
  23. description?: ReactNode
  24. choices: Choice<T>[]
  25. cancelText?: string }
  26. type DialogueRequest =
  27. | { id: number
  28. kind: 'confirm'
  29. options: ConfirmOptions
  30. resolve: (value: boolean) => void }
  31. | { id: number
  32. kind: 'alert'
  33. options: AlertOptions
  34. resolve: () => void }
  35. | { id: number
  36. kind: 'choice'
  37. options: ChoiceOptions<string>
  38. resolve: (value: string | null) => void }
  39. type DialogueAPI =
  40. { confirm: (options: ConfirmOptions) => Promise<boolean>
  41. alert: (options: AlertOptions) => Promise<void>
  42. choice: <T extends string> (options: ChoiceOptions<T>) => Promise<T | null> }
  43. const DialogueContext = createContext<DialogueAPI | null> (null)
  44. let nextDialogueId = 1
  45. type Props = { children: ReactNode }
  46. const DialogueProvider: FC<Props> = ({ children }) => {
  47. const [queue, setQueue] = useState<DialogueRequest[]> ([])
  48. const push = useCallback ((request: Omit<DialogueRequest, 'id'>) => {
  49. const id = nextDialogueId
  50. ++nextDialogueId
  51. setQueue (q => [...q, { ...request, id } as DialogueRequest])
  52. }, [])
  53. const closeActive = useCallback ((result?: unknown) => {
  54. setQueue (q => {
  55. const [active, ...rest] = q
  56. if (!(active))
  57. return rest
  58. switch (active.kind)
  59. {
  60. case 'confirm':
  61. active.resolve (Boolean (result))
  62. break
  63. case 'alert':
  64. active.resolve ()
  65. break
  66. case 'choice':
  67. active.resolve ((result ?? null) as string | null)
  68. break
  69. }
  70. return rest
  71. })
  72. }, [])
  73. const api = useMemo<DialogueAPI> (() => ({
  74. confirm: options => new Promise<boolean> (resolve => {
  75. push ({ kind: 'confirm', options, resolve })
  76. }),
  77. alert: options => new Promise<void> (resolve => {
  78. push ({ kind: 'alert', options, resolve })
  79. }),
  80. choice: options => new Promise (resolve => {
  81. push ({ kind: 'choice',
  82. options: options as ChoiceOptions<string>,
  83. resolve: resolve as (value: string | null) => void })
  84. }) }), [push])
  85. const active = queue[0]
  86. return (
  87. <DialogueContext.Provider value={api}>
  88. {children}
  89. <Dialog
  90. open={Boolean (active)}
  91. onOpenChange={open => {
  92. if (!(open))
  93. closeActive (active?.kind !== 'confirm' && null)
  94. }}>
  95. {active && (
  96. <DialogContent className="px-6 pb-6 pt-7">
  97. <DialogHeader className="pl-8">
  98. <DialogTitle>{active.options.title}</DialogTitle>
  99. {active.options.description && (
  100. <DialogDescription asChild>
  101. <div>{active.options.description}</div>
  102. </DialogDescription>)}
  103. </DialogHeader>
  104. <DialogFooter>
  105. {active.kind === 'confirm' && (
  106. <>
  107. <Button
  108. variant="outline"
  109. onClick={() => closeActive (false)}>
  110. {active.options.cancelText ?? '取消'}
  111. </Button>
  112. <Button
  113. variant={(active.options.variant === 'danger')
  114. ? 'destructive'
  115. : 'default'}
  116. onClick={() => closeActive (true)}>
  117. {active.options.confirmText ?? '確定'}
  118. </Button>
  119. </>)}
  120. {active.kind === 'alert' && (
  121. <Button onClick={() => closeActive ()}>
  122. {active.options.okText ?? '確定'}
  123. </Button>)}
  124. {active.kind === 'choice' && (
  125. <>
  126. <Button
  127. variant="outline"
  128. onClick={() => closeActive (null)}>
  129. {active.options.cancelText ?? '取消'}
  130. </Button>
  131. {active.options.choices.map (choice => (
  132. <Button
  133. key={choice.value}
  134. variant={(choice.variant === 'danger')
  135. ? 'destructive'
  136. : 'default'}
  137. onClick={() => closeActive (choice.value)}>
  138. {choice.label}
  139. </Button>))}
  140. </>)}
  141. </DialogFooter>
  142. </DialogContent>)}
  143. </Dialog>
  144. </DialogueContext.Provider>)
  145. }
  146. export const useDialogue = () => {
  147. const dialogue = useContext (DialogueContext)
  148. if (!(dialogue))
  149. throw new Error ('useDialogue must be used inside DialogueProvider')
  150. return dialogue
  151. }
  152. export default DialogueProvider