zoom-in-out.tsx 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. import type { FC } from 'react'
  2. import {
  3. Fragment,
  4. memo,
  5. useCallback,
  6. useState,
  7. } from 'react'
  8. import { useTranslation } from 'react-i18next'
  9. import {
  10. useReactFlow,
  11. useViewport,
  12. } from 'reactflow'
  13. import {
  14. useNodesSyncDraft,
  15. useWorkflowReadOnly,
  16. } from '../hooks'
  17. import {
  18. PortalToFollowElem,
  19. PortalToFollowElemContent,
  20. PortalToFollowElemTrigger,
  21. } from '@/app/components/base/portal-to-follow-elem'
  22. import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
  23. import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
  24. const ZoomInOut: FC = () => {
  25. const { t } = useTranslation()
  26. const {
  27. zoomIn,
  28. zoomOut,
  29. zoomTo,
  30. fitView,
  31. } = useReactFlow()
  32. const { zoom } = useViewport()
  33. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  34. const [open, setOpen] = useState(false)
  35. const {
  36. workflowReadOnly,
  37. getWorkflowReadOnly,
  38. } = useWorkflowReadOnly()
  39. const ZOOM_IN_OUT_OPTIONS = [
  40. [
  41. {
  42. key: 'in',
  43. text: t('workflow.operator.zoomIn'),
  44. },
  45. {
  46. key: 'out',
  47. text: t('workflow.operator.zoomOut'),
  48. },
  49. ],
  50. [
  51. {
  52. key: 'to50',
  53. text: t('workflow.operator.zoomTo50'),
  54. },
  55. {
  56. key: 'to100',
  57. text: t('workflow.operator.zoomTo100'),
  58. },
  59. ],
  60. [
  61. {
  62. key: 'fit',
  63. text: t('workflow.operator.zoomToFit'),
  64. },
  65. ],
  66. ]
  67. const handleZoom = (type: string) => {
  68. if (workflowReadOnly)
  69. return
  70. if (type === 'in')
  71. zoomIn()
  72. if (type === 'out')
  73. zoomOut()
  74. if (type === 'fit')
  75. fitView()
  76. if (type === 'to50')
  77. zoomTo(0.5)
  78. if (type === 'to100')
  79. zoomTo(1)
  80. handleSyncWorkflowDraft()
  81. }
  82. const handleTrigger = useCallback(() => {
  83. if (getWorkflowReadOnly())
  84. return
  85. setOpen(v => !v)
  86. }, [getWorkflowReadOnly])
  87. return (
  88. <PortalToFollowElem
  89. placement='top-start'
  90. open={open}
  91. onOpenChange={setOpen}
  92. offset={{
  93. mainAxis: 4,
  94. crossAxis: -2,
  95. }}
  96. >
  97. <PortalToFollowElemTrigger asChild onClick={handleTrigger}>
  98. <div className={`
  99. flex items-center px-2 h-8 cursor-pointer text-[13px] hover:bg-gray-50 rounded-lg
  100. ${open && 'bg-gray-50'}
  101. ${workflowReadOnly && '!cursor-not-allowed opacity-50'}
  102. `}>
  103. <SearchLg className='mr-1 w-4 h-4' />
  104. <div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
  105. <ChevronDown className='ml-1 w-4 h-4' />
  106. </div>
  107. </PortalToFollowElemTrigger>
  108. <PortalToFollowElemContent className='z-10'>
  109. <div className='w-[168px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'>
  110. {
  111. ZOOM_IN_OUT_OPTIONS.map((options, i) => (
  112. <Fragment key={i}>
  113. {
  114. i !== 0 && (
  115. <div className='h-[1px] bg-gray-100' />
  116. )
  117. }
  118. <div className='p-1'>
  119. {
  120. options.map(option => (
  121. <div
  122. key={option.key}
  123. className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700'
  124. onClick={() => handleZoom(option.key)}
  125. >
  126. {option.text}
  127. </div>
  128. ))
  129. }
  130. </div>
  131. </Fragment>
  132. ))
  133. }
  134. </div>
  135. </PortalToFollowElemContent>
  136. </PortalToFollowElem>
  137. )
  138. }
  139. export default memo(ZoomInOut)