zoom-in-out.tsx 5.8 KB

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