zoom-in-out.tsx 7.3 KB

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