| import type { FC } from 'react' |
| import { |
| Fragment, |
| memo, |
| useCallback, |
| useState, |
| } from 'react' |
| import { |
| RiZoomInLine, |
| RiZoomOutLine, |
| } from '@remixicon/react' |
| import { useTranslation } from 'react-i18next' |
| import { |
| useReactFlow, |
| useViewport, |
| } from 'reactflow' |
| import { |
| useNodesSyncDraft, |
| useWorkflowReadOnly, |
| } from '../hooks' |
| import { |
| getKeyboardKeyNameBySystem, |
| } from '../utils' |
| import ShortcutsName from '../shortcuts-name' |
| import TipPopup from './tip-popup' |
| import cn from '@/utils/classnames' |
| import { |
| PortalToFollowElem, |
| PortalToFollowElemContent, |
| PortalToFollowElemTrigger, |
| } from '@/app/components/base/portal-to-follow-elem' |
|
|
| enum ZoomType { |
| zoomIn = 'zoomIn', |
| zoomOut = 'zoomOut', |
| zoomToFit = 'zoomToFit', |
| zoomTo25 = 'zoomTo25', |
| zoomTo50 = 'zoomTo50', |
| zoomTo75 = 'zoomTo75', |
| zoomTo100 = 'zoomTo100', |
| zoomTo200 = 'zoomTo200', |
| } |
|
|
| const ZoomInOut: FC = () => { |
| const { t } = useTranslation() |
| const { |
| zoomIn, |
| zoomOut, |
| zoomTo, |
| fitView, |
| } = useReactFlow() |
| const { zoom } = useViewport() |
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() |
| const [open, setOpen] = useState(false) |
| const { |
| workflowReadOnly, |
| getWorkflowReadOnly, |
| } = useWorkflowReadOnly() |
|
|
| const ZOOM_IN_OUT_OPTIONS = [ |
| [ |
| { |
| key: ZoomType.zoomTo200, |
| text: '200%', |
| }, |
| { |
| key: ZoomType.zoomTo100, |
| text: '100%', |
| }, |
| { |
| key: ZoomType.zoomTo75, |
| text: '75%', |
| }, |
| { |
| key: ZoomType.zoomTo50, |
| text: '50%', |
| }, |
| { |
| key: ZoomType.zoomTo25, |
| text: '25%', |
| }, |
| ], |
| [ |
| { |
| key: ZoomType.zoomToFit, |
| text: t('workflow.operator.zoomToFit'), |
| }, |
| ], |
| ] |
|
|
| const handleZoom = (type: string) => { |
| if (workflowReadOnly) |
| return |
|
|
| if (type === ZoomType.zoomToFit) |
| fitView() |
|
|
| if (type === ZoomType.zoomTo25) |
| zoomTo(0.25) |
|
|
| if (type === ZoomType.zoomTo50) |
| zoomTo(0.5) |
|
|
| if (type === ZoomType.zoomTo75) |
| zoomTo(0.75) |
|
|
| if (type === ZoomType.zoomTo100) |
| zoomTo(1) |
|
|
| if (type === ZoomType.zoomTo200) |
| zoomTo(2) |
|
|
| handleSyncWorkflowDraft() |
| } |
|
|
| const handleTrigger = useCallback(() => { |
| if (getWorkflowReadOnly()) |
| return |
|
|
| setOpen(v => !v) |
| }, [getWorkflowReadOnly]) |
|
|
| return ( |
| <PortalToFollowElem |
| placement='top-start' |
| open={open} |
| onOpenChange={setOpen} |
| offset={{ |
| mainAxis: 4, |
| crossAxis: -2, |
| }} |
| > |
| <PortalToFollowElemTrigger asChild onClick={handleTrigger}> |
| <div className={` |
| 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 |
| ${workflowReadOnly && '!cursor-not-allowed opacity-50'} |
| `}> |
| <div className={cn( |
| 'flex items-center justify-between w-[98px] h-8 hover:bg-gray-50 rounded-lg', |
| open && 'bg-gray-50', |
| )}> |
| <TipPopup |
| title={t('workflow.operator.zoomOut')} |
| shortcuts={['ctrl', '-']} |
| > |
| <div |
| className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5' |
| onClick={(e) => { |
| e.stopPropagation() |
| zoomOut() |
| }} |
| > |
| <RiZoomOutLine className='w-4 h-4' /> |
| </div> |
| </TipPopup> |
| <div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div> |
| <TipPopup |
| title={t('workflow.operator.zoomIn')} |
| shortcuts={['ctrl', '+']} |
| > |
| <div |
| className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5' |
| onClick={(e) => { |
| e.stopPropagation() |
| zoomIn() |
| }} |
| > |
| <RiZoomInLine className='w-4 h-4' /> |
| </div> |
| </TipPopup> |
| </div> |
| </div> |
| </PortalToFollowElemTrigger> |
| <PortalToFollowElemContent className='z-10'> |
| <div className='w-[145px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'> |
| { |
| ZOOM_IN_OUT_OPTIONS.map((options, i) => ( |
| <Fragment key={i}> |
| { |
| i !== 0 && ( |
| <div className='h-[1px] bg-gray-100' /> |
| ) |
| } |
| <div className='p-1'> |
| { |
| options.map(option => ( |
| <div |
| key={option.key} |
| className='flex items-center justify-between px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700' |
| onClick={() => handleZoom(option.key)} |
| > |
| {option.text} |
| { |
| option.key === ZoomType.zoomToFit && ( |
| <ShortcutsName keys={[`${getKeyboardKeyNameBySystem('ctrl')}`, '1']} /> |
| ) |
| } |
| { |
| option.key === ZoomType.zoomTo50 && ( |
| <ShortcutsName keys={['shift', '5']} /> |
| ) |
| } |
| { |
| option.key === ZoomType.zoomTo100 && ( |
| <ShortcutsName keys={['shift', '1']} /> |
| ) |
| } |
| </div> |
| )) |
| } |
| </div> |
| </Fragment> |
| )) |
| } |
| </div> |
| </PortalToFollowElemContent> |
| </PortalToFollowElem> |
| ) |
| } |
|
|
| export default memo(ZoomInOut) |
|
|