refactor(zoomPanHelper): export and simplify
This commit is contained in:
@@ -41,6 +41,8 @@ const BasicFlow = () => {
|
||||
|
||||
const logToObject = () => console.log(rfInstance.toObject());
|
||||
|
||||
const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
@@ -57,6 +59,9 @@ const BasicFlow = () => {
|
||||
<Background variant="lines" />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updatePos} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
@@ -39,56 +39,47 @@ const Controls = ({
|
||||
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
|
||||
const mapClasses = cc(['react-flow__controls', className]);
|
||||
|
||||
const onZoomInHandler = useCallback(() => {
|
||||
zoomIn?.();
|
||||
onZoomIn?.();
|
||||
}, [zoomIn, onZoomIn]);
|
||||
|
||||
const onZoomOutHandler = useCallback(() => {
|
||||
zoomOut?.();
|
||||
onZoomOut?.();
|
||||
}, [zoomOut, onZoomOut]);
|
||||
|
||||
const onFitViewHandler = useCallback(() => {
|
||||
fitView?.();
|
||||
onFitView?.();
|
||||
}, [fitView, onFitView]);
|
||||
|
||||
const onInteractiveChangeHandler = useCallback(() => {
|
||||
setInteractive?.(!isInteractive);
|
||||
onInteractiveChange?.(!isInteractive);
|
||||
}, [isInteractive, setInteractive, onInteractiveChange]);
|
||||
|
||||
return (
|
||||
<div className={mapClasses} style={style}>
|
||||
{showZoom && (
|
||||
<>
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-zoomin"
|
||||
onClick={() => {
|
||||
zoomIn();
|
||||
if (onZoomIn) {
|
||||
onZoomIn();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={onZoomInHandler}>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-zoomout"
|
||||
onClick={() => {
|
||||
zoomOut();
|
||||
if (onZoomOut) {
|
||||
onZoomOut();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={onZoomOutHandler}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showFitView && (
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-fitview"
|
||||
onClick={() => {
|
||||
fitView({ padding: 0.1 });
|
||||
if (onFitView) {
|
||||
onFitView();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={onFitViewHandler}>
|
||||
<FitviewIcon />
|
||||
</div>
|
||||
)}
|
||||
{showInteractive && (
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-interactive"
|
||||
onClick={() => {
|
||||
setInteractive(!isInteractive);
|
||||
if (onInteractiveChange) {
|
||||
onInteractiveChange(!isInteractive);
|
||||
}
|
||||
}}
|
||||
onClick={onInteractiveChangeHandler}
|
||||
>
|
||||
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react';
|
||||
|
||||
import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
|
||||
import { useStoreActions, useStore } from '../../store/hooks';
|
||||
import FlowRenderer from '../FlowRenderer';
|
||||
import NodeRenderer from '../NodeRenderer';
|
||||
import EdgeRenderer from '../EdgeRenderer';
|
||||
@@ -132,7 +132,6 @@ const GraphView = ({
|
||||
onPaneContextMenu,
|
||||
}: GraphViewProps) => {
|
||||
const isInitialised = useRef<boolean>(false);
|
||||
const d3Zoom = useStoreState((state) => state.d3Zoom);
|
||||
const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
|
||||
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
|
||||
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
|
||||
@@ -142,34 +141,32 @@ const GraphView = ({
|
||||
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
|
||||
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
|
||||
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
|
||||
const setInitTransform = useStoreActions((actions) => actions.setInitTransform);
|
||||
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
|
||||
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
|
||||
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
|
||||
const currentStore = useStore();
|
||||
const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper();
|
||||
const { zoomIn, zoomOut, zoomTo, transform, fitView } = useZoomPanHelper();
|
||||
|
||||
useElementUpdater(elements);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialised.current && d3Zoom) {
|
||||
if (!isInitialised.current && zoomIn && zoomOut && zoomTo && transform && fitView) {
|
||||
if (onLoad) {
|
||||
onLoad({
|
||||
fitView: (params = { padding: 0.1 }) => fitView(params),
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
setTransform: transform,
|
||||
project: onLoadProject(currentStore),
|
||||
getElements: onLoadGetElements(currentStore),
|
||||
setTransform: (transform: FlowTransform) =>
|
||||
setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }),
|
||||
toObject: onLoadToObject(currentStore),
|
||||
});
|
||||
}
|
||||
|
||||
isInitialised.current = true;
|
||||
}
|
||||
}, [d3Zoom, onLoad]);
|
||||
}, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onConnect) {
|
||||
|
||||
@@ -123,7 +123,7 @@ const ZoomPane = ({
|
||||
d3Zoom.on('zoom', null);
|
||||
} else {
|
||||
d3Zoom.on('zoom', (event: any) => {
|
||||
updateTransform(event.transform);
|
||||
updateTransform([event.transform.x, event.transform.y, event.transform.k]);
|
||||
|
||||
if (onMove) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
|
||||
@@ -1,67 +1,60 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
import { useStoreState, useStore } from '../store/hooks';
|
||||
|
||||
import { clamp } from '../utils';
|
||||
import { getRectOfNodes } from '../utils/graph';
|
||||
import { FitViewParams } from '../types';
|
||||
import { FitViewParams, FlowTransform } from '../types';
|
||||
|
||||
const initialHelpers = {
|
||||
zoomIn: null,
|
||||
zoomOut: null,
|
||||
zoomTo: null,
|
||||
transform: null,
|
||||
fitView: null,
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const store = useStore();
|
||||
const d3Zoom = useStoreState((s) => s.d3Zoom);
|
||||
const d3Selection = useStoreState((s) => s.d3Selection);
|
||||
|
||||
const zoomIn = useCallback(() => {
|
||||
if (d3Selection) {
|
||||
d3Zoom?.scaleBy(d3Selection, 1.2);
|
||||
const zoomPanHelperFunctions = useMemo(() => {
|
||||
if (d3Selection && d3Zoom) {
|
||||
return {
|
||||
zoomIn: () => d3Zoom.scaleBy(d3Selection, 1.2),
|
||||
zoomOut: () => d3Zoom.scaleBy(d3Selection, 1 / 1.2),
|
||||
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
|
||||
transform: (transform: FlowTransform) => {
|
||||
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom);
|
||||
|
||||
d3Zoom.transform(d3Selection, nextTransform);
|
||||
},
|
||||
fitView: (options: FitViewParams = { padding: 0.1 }) => {
|
||||
const { nodes, width, height, minZoom, maxZoom } = store.getState();
|
||||
|
||||
if (!nodes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(nodes);
|
||||
const xZoom = width / (bounds.width * (1 + options.padding));
|
||||
const yZoom = height / (bounds.height * (1 + options.padding));
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
const transform = zoomIdentity.translate(x, y).scale(clampedZoom);
|
||||
|
||||
d3Zoom.transform(d3Selection, transform);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return initialHelpers;
|
||||
}, [d3Zoom, d3Selection]);
|
||||
|
||||
const zoomOut = useCallback(() => {
|
||||
if (d3Selection) {
|
||||
d3Zoom?.scaleBy(d3Selection, 1 / 1.2);
|
||||
}
|
||||
}, [d3Zoom, d3Selection]);
|
||||
|
||||
const zoomTo = useCallback(
|
||||
(zoomLevel: number) => {
|
||||
if (d3Selection) {
|
||||
d3Zoom?.scaleTo(d3Selection, zoomLevel);
|
||||
}
|
||||
},
|
||||
[d3Zoom, d3Selection]
|
||||
);
|
||||
|
||||
const fitView = useCallback(
|
||||
(options: FitViewParams) => {
|
||||
const { padding = 0.1 } = options;
|
||||
const { nodes, width, height, minZoom, maxZoom } = store.getState();
|
||||
|
||||
if (!d3Selection || !nodes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = getRectOfNodes(nodes);
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
const k = clampedZoom;
|
||||
const transform = zoomIdentity.translate(x, y).scale(k);
|
||||
|
||||
d3Zoom?.transform(d3Selection, transform);
|
||||
},
|
||||
[store, d3Zoom, d3Selection]
|
||||
);
|
||||
|
||||
return {
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
zoomTo,
|
||||
fitView,
|
||||
};
|
||||
return zoomPanHelperFunctions;
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ export { getSmoothStepPath } from './components/Edges/SmoothStepEdge';
|
||||
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
|
||||
|
||||
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph';
|
||||
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
|
||||
|
||||
export * from './additional-components';
|
||||
export * from './store/hooks';
|
||||
|
||||
+7
-28
@@ -1,12 +1,11 @@
|
||||
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
|
||||
import { getDimensions } from '../utils';
|
||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
|
||||
import { getHandleBounds } from '../components/Nodes/utils';
|
||||
|
||||
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
|
||||
import {
|
||||
ElementId,
|
||||
Elements,
|
||||
@@ -29,12 +28,6 @@ import {
|
||||
SnapGrid,
|
||||
} from '../types';
|
||||
|
||||
type TransformXYK = {
|
||||
x: number;
|
||||
y: number;
|
||||
k: number;
|
||||
};
|
||||
|
||||
type NodeDimensionUpdate = {
|
||||
id: ElementId;
|
||||
nodeElement: HTMLDivElement;
|
||||
@@ -110,9 +103,7 @@ export interface StoreModel {
|
||||
setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
|
||||
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
|
||||
|
||||
updateTransform: Action<StoreModel, TransformXYK>;
|
||||
|
||||
setInitTransform: Action<StoreModel, TransformXYK>;
|
||||
updateTransform: Action<StoreModel, Transform>;
|
||||
|
||||
updateSize: Action<StoreModel, Dimensions>;
|
||||
|
||||
@@ -148,8 +139,8 @@ export const storeModel: StoreModel = {
|
||||
viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
|
||||
transform: [0, 0, 1],
|
||||
elements: [],
|
||||
nodes: computed((state) => state.elements.filter((el) => isNode(el)) as Node[]),
|
||||
edges: computed((state) => state.elements.filter((el) => isEdge(el)) as Edge[]),
|
||||
nodes: computed((state) => state.elements.filter(isNode)),
|
||||
edges: computed((state) => state.elements.filter(isEdge)),
|
||||
selectedElements: null,
|
||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||
|
||||
@@ -363,21 +354,9 @@ export const storeModel: StoreModel = {
|
||||
}),
|
||||
|
||||
updateTransform: action((state, transform) => {
|
||||
state.transform[0] = transform.x;
|
||||
state.transform[1] = transform.y;
|
||||
state.transform[2] = transform.k;
|
||||
}),
|
||||
|
||||
setInitTransform: action((state, transform) => {
|
||||
state.transform[0] = transform.x;
|
||||
state.transform[1] = transform.y;
|
||||
state.transform[2] = transform.k;
|
||||
|
||||
if (state.d3Selection) {
|
||||
const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k);
|
||||
// we need to sync the d3 zoom transform with the updated transform
|
||||
state.d3Selection.property('__zoom', updatedTransform);
|
||||
}
|
||||
state.transform[0] = transform[0];
|
||||
state.transform[1] = transform[1];
|
||||
state.transform[2] = transform[2];
|
||||
}),
|
||||
|
||||
updateSize: action((state, size) => {
|
||||
|
||||
Reference in New Issue
Block a user