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