| 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) | |