feat(interactive-minimap): add zoom and pan
This commit is contained in:
@@ -19,25 +19,25 @@ const initialNodes: Node[] = [
|
|||||||
id: '1',
|
id: '1',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: 0, y: 0 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 100, y: 100 },
|
position: { x: 0, y: 200 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
data: { label: 'Node 3' },
|
data: { label: 'Node 3' },
|
||||||
position: { x: 400, y: 100 },
|
position: { x: 200, y: 200 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
data: { label: 'Node 4' },
|
data: { label: 'Node 4' },
|
||||||
position: { x: 400, y: 200 },
|
position: { x: 200, y: 400 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -88,9 +88,9 @@ const BasicFlow = () => {
|
|||||||
className="react-flow-basic-example"
|
className="react-flow-basic-example"
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={4}
|
maxZoom={4}
|
||||||
fitView
|
|
||||||
defaultEdgeOptions={defaultEdgeOptions}
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
selectNodesOnDrag={false}
|
selectNodesOnDrag={false}
|
||||||
|
fitView
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Dots} />
|
<Background variant={BackgroundVariant.Dots} />
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
|
|||||||
@@ -173,9 +173,12 @@ const ZoomPane = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (d3Zoom) {
|
if (d3Zoom) {
|
||||||
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (!event.sourceEvent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { onViewportChangeStart } = store.getState();
|
const { onViewportChangeStart } = store.getState();
|
||||||
isZoomingOrPanning.current = true;
|
isZoomingOrPanning.current = true;
|
||||||
|
|
||||||
if (event.sourceEvent?.type === 'mousedown') {
|
if (event.sourceEvent?.type === 'mousedown') {
|
||||||
store.setState({ paneDragging: true });
|
store.setState({ paneDragging: true });
|
||||||
}
|
}
|
||||||
@@ -194,6 +197,9 @@ const ZoomPane = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (d3Zoom) {
|
if (d3Zoom) {
|
||||||
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (!event.sourceEvent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const { onViewportChangeEnd } = store.getState();
|
const { onViewportChangeEnd } = store.getState();
|
||||||
|
|
||||||
isZoomingOrPanning.current = false;
|
isZoomingOrPanning.current = false;
|
||||||
|
|||||||
@@ -2,9 +2,19 @@
|
|||||||
import { memo, useEffect, useRef } from 'react';
|
import { memo, useEffect, useRef } from 'react';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import shallow from 'zustand/shallow';
|
import shallow from 'zustand/shallow';
|
||||||
import { zoom, D3ZoomEvent } from 'd3-zoom';
|
import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom';
|
||||||
import { select } from 'd3-selection';
|
import { select } from 'd3-selection';
|
||||||
import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects } from '@reactflow/core';
|
import {
|
||||||
|
useStore,
|
||||||
|
getRectOfNodes,
|
||||||
|
ReactFlowState,
|
||||||
|
Rect,
|
||||||
|
Panel,
|
||||||
|
getBoundsOfRects,
|
||||||
|
useStoreApi,
|
||||||
|
useReactFlow,
|
||||||
|
XYPosition,
|
||||||
|
} from '@reactflow/core';
|
||||||
|
|
||||||
import MiniMapNode from './MiniMapNode';
|
import MiniMapNode from './MiniMapNode';
|
||||||
import { MiniMapProps, GetMiniMapNodeAttribute } from './types';
|
import { MiniMapProps, GetMiniMapNodeAttribute } from './types';
|
||||||
@@ -46,7 +56,9 @@ function MiniMap({
|
|||||||
maskColor = 'rgb(240, 242, 243, 0.7)',
|
maskColor = 'rgb(240, 242, 243, 0.7)',
|
||||||
position = 'bottom-right',
|
position = 'bottom-right',
|
||||||
}: MiniMapProps) {
|
}: MiniMapProps) {
|
||||||
|
const store = useStoreApi();
|
||||||
const svg = useRef<SVGSVGElement>(null);
|
const svg = useRef<SVGSVGElement>(null);
|
||||||
|
const initialized = useRef(false);
|
||||||
const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow);
|
const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow);
|
||||||
const elementWidth = (style?.width as number) ?? defaultWidth;
|
const elementWidth = (style?.width as number) ?? defaultWidth;
|
||||||
const elementHeight = (style?.height as number) ?? defaultHeight;
|
const elementHeight = (style?.height as number) ?? defaultHeight;
|
||||||
@@ -65,15 +77,69 @@ function MiniMap({
|
|||||||
const height = viewHeight + offset * 2;
|
const height = viewHeight + offset * 2;
|
||||||
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
|
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
|
||||||
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
|
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
|
||||||
|
const { setCenter } = useReactFlow();
|
||||||
|
const startTransform = useRef<XYPosition>({ x: 0, y: 0 });
|
||||||
|
const viewScaleRef = useRef(0);
|
||||||
|
const viewScaleWRef = useRef(0);
|
||||||
|
const viewScaleHRef = useRef(0);
|
||||||
|
|
||||||
|
viewScaleRef.current = viewScale;
|
||||||
|
viewScaleWRef.current = scaledWidth;
|
||||||
|
viewScaleHRef.current = scaledHeight;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const d3ZoomInstance = zoom();
|
if (!initialized.current && svg.current) {
|
||||||
const selection = select(svg.current as Element).call(d3ZoomInstance);
|
const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 };
|
||||||
|
|
||||||
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
const selection = select(svg.current as Element);
|
||||||
console.log(event);
|
|
||||||
});
|
const zoomHandler = zoom()
|
||||||
}, []);
|
.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
const { transform } = store.getState();
|
||||||
|
|
||||||
|
const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
|
||||||
|
const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
|
||||||
|
|
||||||
|
startTransform.current = {
|
||||||
|
x: px - transform[0] / transform[2],
|
||||||
|
y: py - transform[1] / transform[2],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.on('zoom.wheel', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
const { transform, d3Selection, d3Zoom } = store.getState();
|
||||||
|
|
||||||
|
if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinchDelta =
|
||||||
|
-event.sourceEvent.deltaY *
|
||||||
|
(event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
|
||||||
|
10;
|
||||||
|
const zoom = transform[2] * Math.pow(2, pinchDelta);
|
||||||
|
d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]);
|
||||||
|
})
|
||||||
|
.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||||
|
if (event.sourceEvent.type !== 'mousemove') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transform, d3Selection, d3Zoom } = store.getState();
|
||||||
|
const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
|
||||||
|
const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
|
||||||
|
|
||||||
|
const position = {
|
||||||
|
x: (px - startTransform.current.x) * transform[2],
|
||||||
|
y: (py - startTransform.current.y) * transform[2],
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]);
|
||||||
|
d3Zoom!.transform(d3Selection!, nextTransform);
|
||||||
|
});
|
||||||
|
selection.call(zoomHandler);
|
||||||
|
initialized.current = true;
|
||||||
|
}
|
||||||
|
}, [setCenter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Panel position={position} style={style} className={cc(['react-flow__minimap', className])}>
|
<Panel position={position} style={style} className={cc(['react-flow__minimap', className])}>
|
||||||
|
|||||||
Reference in New Issue
Block a user