Files
xyflow/packages/react/src/container/ZoomPane/index.tsx

156 lines
4.2 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
import { useCallback, useEffect, useRef } from 'react';
import { shallow } from 'zustand/shallow';
import { XYPanZoom, PanOnScrollMode, type Transform, type PanZoomInstance } from '@xyflow/system';
import { useKeyPress } from '../../hooks/useKeyPress';
import { useResizeHandler } from '../../hooks/useResizeHandler';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils';
import type { FlowRendererProps } from '../FlowRenderer';
import type { ReactFlowState } from '../../types';
type ZoomPaneProps = Omit<
FlowRendererProps,
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName' | 'disableKeyboardA11y'
> & {
isControlledViewport: boolean;
};
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
lib: s.lib,
connectionInProgress: s.connection.inProgress,
});
export function ZoomPane({
onPaneContextMenu,
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
panOnDrag = true,
defaultViewport,
translateExtent,
minZoom,
maxZoom,
zoomActivationKeyCode,
preventScrolling = true,
children,
noWheelClassName,
noPanClassName,
onViewportChange,
isControlledViewport,
paneClickDistance,
selectionOnDrag,
}: ZoomPaneProps) {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const { userSelectionActive, lib, connectionInProgress } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
const panZoom = useRef<PanZoomInstance>();
useResizeHandler(zoomPane);
const onTransformChange = useCallback(
(transform: Transform) => {
onViewportChange?.({ x: transform[0], y: transform[1], zoom: transform[2] });
if (!isControlledViewport) {
store.setState({ transform });
}
},
[onViewportChange, isControlledViewport]
);
useEffect(() => {
if (zoomPane.current) {
panZoom.current = XYPanZoom({
domNode: zoomPane.current,
minZoom,
maxZoom,
translateExtent,
viewport: defaultViewport,
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
onPanZoomStart: (event, vp) => {
const { onViewportChangeStart, onMoveStart } = store.getState();
onMoveStart?.(event, vp);
onViewportChangeStart?.(vp);
},
onPanZoom: (event, vp) => {
const { onViewportChange, onMove } = store.getState();
onMove?.(event, vp);
onViewportChange?.(vp);
},
onPanZoomEnd: (event, vp) => {
const { onViewportChangeEnd, onMoveEnd } = store.getState();
onMoveEnd?.(event, vp);
onViewportChangeEnd?.(vp);
},
});
const { x, y, zoom } = panZoom.current.getViewport();
store.setState({
panZoom: panZoom.current,
transform: [x, y, zoom],
domNode: zoomPane.current.closest('.react-flow') as HTMLDivElement,
});
return () => {
panZoom.current?.destroy();
};
}
}, []);
useEffect(() => {
panZoom.current?.update({
onPaneContextMenu,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
zoomActivationKeyPressed,
preventScrolling,
noPanClassName,
userSelectionActive,
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
});
}, [
onPaneContextMenu,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
zoomActivationKeyPressed,
preventScrolling,
noPanClassName,
userSelectionActive,
noWheelClassName,
lib,
onTransformChange,
connectionInProgress,
selectionOnDrag,
paneClickDistance,
]);
return (
<div className="react-flow__renderer" ref={zoomPane} style={containerStyle}>
{children}
</div>
);
}