zoom-in-out.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import type { FC } from 'react'
  2. import {
  3. Fragment,
  4. memo,
  5. useCallback,
  6. useState,
  7. } from 'react'
  8. import {
  9. RiZoomInLine,
  10. RiZoomOutLine,
  11. } from '@remixicon/react'
  12. import { useKeyPress } from 'ahooks'
  13. import { useTranslation } from 'react-i18next'
  14. import {
  15. useReactFlow,
  16. useViewport,
  17. } from 'reactflow'
  18. import {
  19. useNodesSyncDraft,
  20. useWorkflowReadOnly,
  21. } from '../hooks'
  22. import {
  23. getKeyboardKeyCodeBySystem,
  24. getKeyboardKeyNameBySystem,
  25. isEventTargetInputArea,
  26. } from '../utils'
  27. import ShortcutsName from '../shortcuts-name'
  28. import TipPopup from './tip-popup'
  29. import cn from '@/utils/classnames'
  30. import {
  31. PortalToFollowElem,
  32. PortalToFollowElemContent,
  33. PortalToFollowElemTrigger,
  34. } from '@/app/components/base/portal-to-follow-elem'
  35. enum ZoomType {
  36. zoomIn = 'zoomIn',
  37. zoomOut = 'zoomOut',
  38. zoomToFit = 'zoomToFit',
  39. zoomTo25 = 'zoomTo25',
  40. zoomTo50 = 'zoomTo50',
  41. zoomTo75 = 'zoomTo75',
  42. zoomTo100 = 'zoomTo100',
  43. zoomTo200 = 'zoomTo200',
  44. }
  45. const ZoomInOut: FC = () => {
  46. const { t } = useTranslation()
  47. const {
  48. zoomIn,
  49. zoomOut,
  50. zoomTo,
  51. fitView,
  52. } = useReactFlow()
  53. const { zoom } = useViewport()
  54. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  55. const [open, setOpen] = useState(false)
  56. const {
  57. workflowReadOnly,
  58. getWorkflowReadOnly,
  59. } = useWorkflowReadOnly()
  60. const ZOOM_IN_OUT_OPTIONS = [
  61. [
  62. {
  63. key: ZoomType.zoomTo200,
  64. text: '200%',
  65. },
  66. {
  67. key: ZoomType.zoomTo100,
  68. text: '100%',
  69. },
  70. {
  71. key: ZoomType.zoomTo75,
  72. text: '75%',
  73. },
  74. {
  75. key: ZoomType.zoomTo50,
  76. text: '50%',
  77. },
  78. {
  79. key: ZoomType.zoomTo25,
  80. text: '25%',
  81. },
  82. ],
  83. [
  84. {
  85. key: ZoomType.zoomToFit,
  86. text: t('workflow.operator.zoomToFit'),
  87. },
  88. ],
  89. ]
  90. const handleZoom = (type: string) => {
  91. if (workflowReadOnly)
  92. return
  93. if (type === ZoomType.zoomToFit)
  94. fitView()
  95. if (type === ZoomType.zoomTo25)
  96. zoomTo(0.25)
  97. if (type === ZoomType.zoomTo50)
  98. zoomTo(0.5)
  99. if (type === ZoomType.zoomTo75)
  100. zoomTo(0.75)
  101. if (type === ZoomType.zoomTo100)
  102. zoomTo(1)
  103. if (type === ZoomType.zoomTo200)
  104. zoomTo(2)
  105. handleSyncWorkflowDraft()
  106. }
  107. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
  108. e.preventDefault()
  109. if (workflowReadOnly)
  110. return
  111. fitView()
  112. handleSyncWorkflowDraft()
  113. }, {
  114. exactMatch: true,
  115. useCapture: true,
  116. })
  117. useKeyPress('shift.1', (e) => {
  118. if (workflowReadOnly)
  119. return
  120. if (isEventTargetInputArea(e.target as HTMLElement))
  121. return
  122. e.preventDefault()
  123. zoomTo(1)
  124. handleSyncWorkflowDraft()
  125. }, {
  126. exactMatch: true,
  127. useCapture: true,
  128. })
  129. useKeyPress('shift.2', (e) => {
  130. if (workflowReadOnly)
  131. return
  132. if (isEventTargetInputArea(e.target as HTMLElement))
  133. return
  134. e.preventDefault()
  135. zoomTo(2)
  136. handleSyncWorkflowDraft()
  137. }, {
  138. exactMatch: true,
  139. useCapture: true,
  140. })
  141. useKeyPress('shift.5', (e) => {
  142. if (workflowReadOnly)
  143. return
  144. if (isEventTargetInputArea(e.target as HTMLElement))
  145. return
  146. e.preventDefault()
  147. zoomTo(0.5)
  148. handleSyncWorkflowDraft()
  149. }, {
  150. exactMatch: true,
  151. useCapture: true,
  152. })
  153. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
  154. e.preventDefault()
  155. if (workflowReadOnly)
  156. return
  157. zoomOut()
  158. handleSyncWorkflowDraft()
  159. }, {
  160. exactMatch: true,
  161. useCapture: true,
  162. })
  163. useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
  164. e.preventDefault()
  165. if (workflowReadOnly)
  166. return
  167. zoomIn()
  168. handleSyncWorkflowDraft()
  169. }, {
  170. exactMatch: true,
  171. useCapture: true,
  172. })
  173. const handleTrigger = useCallback(() => {
  174. if (getWorkflowReadOnly())
  175. return
  176. setOpen(v => !v)
  177. }, [getWorkflowReadOnly])
  178. return (
  179. <PortalToFollowElem
  180. placement='top-start'
  181. open={open}
  182. onOpenChange={setOpen}
  183. offset={{
  184. mainAxis: 4,
  185. crossAxis: -2,
  186. }}
  187. >
  188. <PortalToFollowElemTrigger asChild onClick={handleTrigger}>
  189. <div className={`
  190. p-0.5 h-9 cursor-pointer text-[13px] text-gray-500 font-medium rounded-lg bg-white shadow-lg border-[0.5px] border-gray-100
  191. ${workflowReadOnly && '!cursor-not-allowed opacity-50'}
  192. `}>
  193. <div className={cn(
  194. 'flex items-center justify-between w-[98px] h-8 hover:bg-gray-50 rounded-lg',
  195. open && 'bg-gray-50',
  196. )}>
  197. <TipPopup
  198. title={t('workflow.operator.zoomOut')}
  199. shortcuts={['ctrl', '-']}
  200. >
  201. <div
  202. className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
  203. onClick={(e) => {
  204. e.stopPropagation()
  205. zoomOut()
  206. }}
  207. >
  208. <RiZoomOutLine className='w-4 h-4' />
  209. </div>
  210. </TipPopup>
  211. <div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
  212. <TipPopup
  213. title={t('workflow.operator.zoomIn')}
  214. shortcuts={['ctrl', '+']}
  215. >
  216. <div
  217. className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
  218. onClick={(e) => {
  219. e.stopPropagation()
  220. zoomIn()
  221. }}
  222. >
  223. <RiZoomInLine className='w-4 h-4' />
  224. </div>
  225. </TipPopup>
  226. </div>
  227. </div>
  228. </PortalToFollowElemTrigger>
  229. <PortalToFollowElemContent className='z-10'>
  230. <div className='w-[145px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'>
  231. {
  232. ZOOM_IN_OUT_OPTIONS.map((options, i) => (
  233. <Fragment key={i}>
  234. {
  235. i !== 0 && (
  236. <div className='h-[1px] bg-gray-100' />
  237. )
  238. }
  239. <div className='p-1'>
  240. {
  241. options.map(option => (
  242. <div
  243. key={option.key}
  244. className='flex items-center justify-between px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700'
  245. onClick={() => handleZoom(option.key)}
  246. >
  247. {option.text}
  248. {
  249. option.key === ZoomType.zoomToFit && (
  250. <ShortcutsName keys={[`${getKeyboardKeyNameBySystem('ctrl')}`, '1']} />
  251. )
  252. }
  253. {
  254. option.key === ZoomType.zoomTo50 && (
  255. <ShortcutsName keys={['shift', '5']} />
  256. )
  257. }
  258. {
  259. option.key === ZoomType.zoomTo100 && (
  260. <ShortcutsName keys={['shift', '1']} />
  261. )
  262. }
  263. {
  264. option.key === ZoomType.zoomTo200 && (
  265. <ShortcutsName keys={['shift', '2']} />
  266. )
  267. }
  268. </div>
  269. ))
  270. }
  271. </div>
  272. </Fragment>
  273. ))
  274. }
  275. </div>
  276. </PortalToFollowElemContent>
  277. </PortalToFollowElem>
  278. )
  279. }
  280. export default memo(ZoomInOut)