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