zoom-in-out.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 { useTranslation } from 'react-i18next'
  13. import {
  14. useReactFlow,
  15. useViewport,
  16. } from 'reactflow'
  17. import {
  18. useNodesSyncDraft,
  19. useWorkflowReadOnly,
  20. } from '../hooks'
  21. import ShortcutsName from '../shortcuts-name'
  22. import Divider from '../../base/divider'
  23. import TipPopup from './tip-popup'
  24. import cn from '@/utils/classnames'
  25. import {
  26. PortalToFollowElem,
  27. PortalToFollowElemContent,
  28. PortalToFollowElemTrigger,
  29. } from '@/app/components/base/portal-to-follow-elem'
  30. enum ZoomType {
  31. zoomIn = 'zoomIn',
  32. zoomOut = 'zoomOut',
  33. zoomToFit = 'zoomToFit',
  34. zoomTo25 = 'zoomTo25',
  35. zoomTo50 = 'zoomTo50',
  36. zoomTo75 = 'zoomTo75',
  37. zoomTo100 = 'zoomTo100',
  38. zoomTo200 = 'zoomTo200',
  39. }
  40. const ZoomInOut: FC = () => {
  41. const { t } = useTranslation()
  42. const {
  43. zoomIn,
  44. zoomOut,
  45. zoomTo,
  46. fitView,
  47. } = useReactFlow()
  48. const { zoom } = useViewport()
  49. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  50. const [open, setOpen] = useState(false)
  51. const {
  52. workflowReadOnly,
  53. getWorkflowReadOnly,
  54. } = useWorkflowReadOnly()
  55. const ZOOM_IN_OUT_OPTIONS = [
  56. [
  57. {
  58. key: ZoomType.zoomTo200,
  59. text: '200%',
  60. },
  61. {
  62. key: ZoomType.zoomTo100,
  63. text: '100%',
  64. },
  65. {
  66. key: ZoomType.zoomTo75,
  67. text: '75%',
  68. },
  69. {
  70. key: ZoomType.zoomTo50,
  71. text: '50%',
  72. },
  73. {
  74. key: ZoomType.zoomTo25,
  75. text: '25%',
  76. },
  77. ],
  78. [
  79. {
  80. key: ZoomType.zoomToFit,
  81. text: t('workflow.operator.zoomToFit'),
  82. },
  83. ],
  84. ]
  85. const handleZoom = (type: string) => {
  86. if (workflowReadOnly)
  87. return
  88. if (type === ZoomType.zoomToFit)
  89. fitView()
  90. if (type === ZoomType.zoomTo25)
  91. zoomTo(0.25)
  92. if (type === ZoomType.zoomTo50)
  93. zoomTo(0.5)
  94. if (type === ZoomType.zoomTo75)
  95. zoomTo(0.75)
  96. if (type === ZoomType.zoomTo100)
  97. zoomTo(1)
  98. if (type === ZoomType.zoomTo200)
  99. zoomTo(2)
  100. handleSyncWorkflowDraft()
  101. }
  102. const handleTrigger = useCallback(() => {
  103. if (getWorkflowReadOnly())
  104. return
  105. setOpen(v => !v)
  106. }, [getWorkflowReadOnly])
  107. return (
  108. <PortalToFollowElem
  109. placement='top-start'
  110. open={open}
  111. onOpenChange={setOpen}
  112. offset={{
  113. mainAxis: 4,
  114. crossAxis: -2,
  115. }}
  116. >
  117. <PortalToFollowElemTrigger asChild>
  118. <div className={`
  119. h-9 cursor-pointer rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg
  120. p-0.5 text-[13px] shadow-lg backdrop-blur-[5px]
  121. hover:bg-state-base-hover
  122. ${workflowReadOnly && '!cursor-not-allowed opacity-50'}
  123. `}>
  124. <div className={cn(
  125. 'flex h-8 w-[98px] items-center justify-between rounded-lg',
  126. )}>
  127. <TipPopup
  128. title={t('workflow.operator.zoomOut')}
  129. shortcuts={['ctrl', '-']}
  130. >
  131. <div
  132. className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom <= 0.25 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
  133. onClick={(e) => {
  134. if (zoom <= 0.25)
  135. return
  136. e.stopPropagation()
  137. zoomOut()
  138. }}
  139. >
  140. <RiZoomOutLine className='h-4 w-4 text-text-tertiary hover:text-text-secondary' />
  141. </div>
  142. </TipPopup>
  143. <div onClick={handleTrigger} className={cn('system-sm-medium w-[34px] text-text-tertiary hover:text-text-secondary')}>{Number.parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
  144. <TipPopup
  145. title={t('workflow.operator.zoomIn')}
  146. shortcuts={['ctrl', '+']}
  147. >
  148. <div
  149. className={`flex h-8 w-8 items-center justify-center rounded-lg ${zoom >= 2 ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-black/5'}`}
  150. onClick={(e) => {
  151. if (zoom >= 2)
  152. return
  153. e.stopPropagation()
  154. zoomIn()
  155. }}
  156. >
  157. <RiZoomInLine className='h-4 w-4 text-text-tertiary hover:text-text-secondary' />
  158. </div>
  159. </TipPopup>
  160. </div>
  161. </div>
  162. </PortalToFollowElemTrigger>
  163. <PortalToFollowElemContent className='z-10'>
  164. <div className='w-[145px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
  165. {
  166. ZOOM_IN_OUT_OPTIONS.map((options, i) => (
  167. <Fragment key={i}>
  168. {
  169. i !== 0 && (
  170. <Divider className='m-0' />
  171. )
  172. }
  173. <div className='p-1'>
  174. {
  175. options.map(option => (
  176. <div
  177. key={option.key}
  178. className='system-md-regular flex h-8 cursor-pointer items-center justify-between space-x-1 rounded-lg py-1.5 pl-3 pr-2 text-text-secondary hover:bg-state-base-hover'
  179. onClick={() => handleZoom(option.key)}
  180. >
  181. <span>{option.text}</span>
  182. <div className='flex items-center space-x-0.5'>
  183. {
  184. option.key === ZoomType.zoomToFit && (
  185. <ShortcutsName keys={['ctrl', '1']} />
  186. )
  187. }
  188. {
  189. option.key === ZoomType.zoomTo50 && (
  190. <ShortcutsName keys={['shift', '5']} />
  191. )
  192. }
  193. {
  194. option.key === ZoomType.zoomTo100 && (
  195. <ShortcutsName keys={['shift', '1']} />
  196. )
  197. }
  198. </div>
  199. </div>
  200. ))
  201. }
  202. </div>
  203. </Fragment>
  204. ))
  205. }
  206. </div>
  207. </PortalToFollowElemContent>
  208. </PortalToFollowElem>
  209. )
  210. }
  211. export default memo(ZoomInOut)