@@ -73,6 +73,7 @@ const BasicFlow = () => <ReactFlow elements={elements} />;
|
||||
- `minZoom`: default: `0.5`
|
||||
- `maxZoom`: default: `2`
|
||||
- `defaultZoom`: default: `1`
|
||||
- `defaultPosition`: default: `[0, 0]`
|
||||
- `snapToGrid`: default: `false`
|
||||
- `snapGrid`: [x, y] array - default: `[16, 16]`
|
||||
- `onlyRenderVisibleNodes`: default: `true`
|
||||
@@ -88,7 +89,9 @@ const BasicFlow = () => <ReactFlow elements={elements} />;
|
||||
- `onNodeContextMenu(evt: MouseEvent, node: Node)`: node context menu
|
||||
- `onConnect({ source, target })`: called when user connects two nodes
|
||||
- `onLoad(reactFlowInstance)`: called after flow is initialized
|
||||
- `onMove()`: called when user pans or zooms
|
||||
- `onMove()`: called when user is panning or zooming
|
||||
- `onMoveStart()`: called when user starts panning or zooming
|
||||
- `onMoveEnd()`: called when user ends panning or zooming
|
||||
- `onSelectionChange(elements: Elements)`: called when user selects one or multiple elements
|
||||
|
||||
#### Interaction
|
||||
|
||||
@@ -11,6 +11,8 @@ const onLoad = (reactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
};
|
||||
|
||||
const onMoveEnd = (transform) => console.log('zoom/move end', transform);
|
||||
|
||||
const initialElements = [
|
||||
{
|
||||
id: '1',
|
||||
@@ -116,6 +118,7 @@ const OverviewFlow = () => {
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onMoveEnd={onMoveEnd}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onLoad={onLoad}
|
||||
connectionLineStyle={{ stroke: '#ddd' }}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Edge,
|
||||
Connection,
|
||||
ConnectionLineType,
|
||||
FlowTransform,
|
||||
} from '../../types';
|
||||
|
||||
export interface GraphViewProps {
|
||||
@@ -35,7 +36,9 @@ export interface GraphViewProps {
|
||||
onNodeDragStop?: (node: Node) => void;
|
||||
onConnect?: (connection: Connection | Edge) => void;
|
||||
onLoad?: OnLoadFunc;
|
||||
onMove?: () => void;
|
||||
onMove?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveStart?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveEnd?: (flowTransform?: FlowTransform) => void;
|
||||
selectionKeyCode: number;
|
||||
nodeTypes: NodeTypesType;
|
||||
edgeTypes: EdgeTypesType;
|
||||
@@ -52,6 +55,7 @@ export interface GraphViewProps {
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
defaultZoom: number;
|
||||
defaultPosition: [number, number];
|
||||
arrowHeadColor: string;
|
||||
markerEndId?: string;
|
||||
zoomOnScroll: boolean;
|
||||
@@ -63,6 +67,8 @@ const GraphView = memo(
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
onLoad,
|
||||
onElementClick,
|
||||
onNodeMouseEnter,
|
||||
@@ -88,6 +94,7 @@ const GraphView = memo(
|
||||
minZoom,
|
||||
maxZoom,
|
||||
defaultZoom,
|
||||
defaultPosition,
|
||||
arrowHeadColor,
|
||||
markerEndId,
|
||||
zoomOnScroll,
|
||||
@@ -106,7 +113,7 @@ const GraphView = memo(
|
||||
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
|
||||
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
|
||||
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
|
||||
const updateTransform = useStoreActions((actions) => actions.updateTransform);
|
||||
const setInitTransform = useStoreActions((actions) => actions.setInitTransform);
|
||||
const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
|
||||
const fitView = useStoreActions((actions) => actions.fitView);
|
||||
const zoom = useStoreActions((actions) => actions.zoom);
|
||||
@@ -139,10 +146,6 @@ const GraphView = memo(
|
||||
setOnConnect(onConnect);
|
||||
}
|
||||
|
||||
if (defaultZoom !== 1) {
|
||||
updateTransform({ x: 0, y: 0, k: defaultZoom });
|
||||
}
|
||||
|
||||
if (rendererNode.current) {
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let _ of entries) {
|
||||
@@ -162,7 +165,15 @@ const GraphView = memo(
|
||||
};
|
||||
}, []);
|
||||
|
||||
useD3Zoom({ zoomPane, onMove, selectionKeyPressed, zoomOnScroll, zoomOnDoubleClick });
|
||||
useD3Zoom({
|
||||
zoomPane,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
selectionKeyPressed,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Initialised && onLoad) {
|
||||
@@ -174,6 +185,18 @@ const GraphView = memo(
|
||||
getElements,
|
||||
});
|
||||
}
|
||||
|
||||
if (d3Initialised) {
|
||||
const initialTransform = {
|
||||
x: defaultPosition[0],
|
||||
y: defaultPosition[1],
|
||||
k: defaultZoom,
|
||||
};
|
||||
|
||||
if (initialTransform.x !== 0 || initialTransform.y !== 0 || initialTransform.k !== 1) {
|
||||
setInitTransform(initialTransform);
|
||||
}
|
||||
}
|
||||
}, [d3Initialised, onLoad]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Edge,
|
||||
Connection,
|
||||
ConnectionLineType,
|
||||
FlowTransform,
|
||||
} from '../../types';
|
||||
|
||||
import '../../style.css';
|
||||
@@ -42,7 +43,9 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
||||
onNodeDragStop?: (node: Node) => void;
|
||||
onConnect?: (connection: Edge | Connection) => void;
|
||||
onLoad?: OnLoadFunc;
|
||||
onMove?: () => void;
|
||||
onMove?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveStart?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveEnd?: (flowTransform?: FlowTransform) => void;
|
||||
onSelectionChange?: (elements: Elements | null) => void;
|
||||
nodeTypes: NodeTypesType;
|
||||
edgeTypes: EdgeTypesType;
|
||||
@@ -60,6 +63,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
defaultZoom: number;
|
||||
defaultPosition: [number, number];
|
||||
arrowHeadColor: string;
|
||||
markerEndId?: string;
|
||||
zoomOnScroll: boolean;
|
||||
@@ -76,6 +80,8 @@ const ReactFlow = ({
|
||||
edgeTypes,
|
||||
onLoad,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
onElementsRemove,
|
||||
onConnect,
|
||||
onNodeMouseEnter,
|
||||
@@ -99,6 +105,7 @@ const ReactFlow = ({
|
||||
minZoom,
|
||||
maxZoom,
|
||||
defaultZoom,
|
||||
defaultPosition,
|
||||
arrowHeadColor,
|
||||
markerEndId,
|
||||
zoomOnScroll,
|
||||
@@ -114,6 +121,8 @@ const ReactFlow = ({
|
||||
<GraphView
|
||||
onLoad={onLoad}
|
||||
onMove={onMove}
|
||||
onMoveStart={onMoveStart}
|
||||
onMoveEnd={onMoveEnd}
|
||||
onElementClick={onElementClick}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
@@ -140,6 +149,7 @@ const ReactFlow = ({
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
defaultZoom={defaultZoom}
|
||||
defaultPosition={defaultPosition}
|
||||
arrowHeadColor={arrowHeadColor}
|
||||
markerEndId={markerEndId}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
@@ -179,6 +189,7 @@ ReactFlow.defaultProps = {
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
defaultZoom: 1,
|
||||
defaultPosition: [0, 0],
|
||||
arrowHeadColor: '#bbb',
|
||||
zoomOnScroll: true,
|
||||
zoomOnDoubleClick: true,
|
||||
|
||||
+63
-5
@@ -1,17 +1,40 @@
|
||||
import { useEffect, MutableRefObject } from 'react';
|
||||
import { useEffect, useRef, MutableRefObject } from 'react';
|
||||
import { event } from 'd3-selection';
|
||||
|
||||
import { useStoreState, useStoreActions } from '../store/hooks';
|
||||
import { FlowTransform } from '../types';
|
||||
|
||||
interface UseD3ZoomParams {
|
||||
zoomPane: MutableRefObject<Element | null>;
|
||||
selectionKeyPressed: boolean;
|
||||
zoomOnScroll: boolean;
|
||||
zoomOnDoubleClick: boolean;
|
||||
onMove?: () => void;
|
||||
onMove?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveStart?: (flowTransform?: FlowTransform) => void;
|
||||
onMoveEnd?: (flowTransform?: FlowTransform) => void;
|
||||
}
|
||||
|
||||
export default ({ zoomPane, onMove, zoomOnScroll, zoomOnDoubleClick, selectionKeyPressed }: UseD3ZoomParams): void => {
|
||||
const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean =>
|
||||
prevTransform.x !== eventTransform.x ||
|
||||
prevTransform.y !== eventTransform.y ||
|
||||
prevTransform.zoom !== eventTransform.k;
|
||||
|
||||
const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
|
||||
x: eventTransform.x,
|
||||
y: eventTransform.y,
|
||||
zoom: eventTransform.k,
|
||||
});
|
||||
|
||||
export default ({
|
||||
zoomPane,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
zoomOnScroll,
|
||||
zoomOnDoubleClick,
|
||||
selectionKeyPressed,
|
||||
}: UseD3ZoomParams): void => {
|
||||
const prevTransform = useRef<FlowTransform>({ x: 0, y: 0, zoom: 0 });
|
||||
const d3Zoom = useStoreState((s) => s.d3Zoom);
|
||||
|
||||
const initD3 = useStoreActions((actions) => actions.initD3);
|
||||
@@ -32,12 +55,47 @@ export default ({ zoomPane, onMove, zoomOnScroll, zoomOnDoubleClick, selectionKe
|
||||
updateTransform(event.transform);
|
||||
|
||||
if (onMove) {
|
||||
onMove();
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
onMove(flowTransform);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectionKeyPressed, d3Zoom]);
|
||||
}, [selectionKeyPressed, d3Zoom, updateTransform, onMove]);
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Zoom) {
|
||||
if (onMoveStart) {
|
||||
d3Zoom.on('start', () => {
|
||||
if (viewChanged(prevTransform.current, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
prevTransform.current = flowTransform;
|
||||
|
||||
onMoveStart(flowTransform);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3Zoom.on('start', null);
|
||||
}
|
||||
}
|
||||
}, [d3Zoom, onMoveStart]);
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Zoom) {
|
||||
if (onMoveEnd) {
|
||||
d3Zoom.on('end', () => {
|
||||
if (viewChanged(prevTransform.current, event.transform)) {
|
||||
const flowTransform = eventToFlowTransform(event.transform);
|
||||
prevTransform.current = flowTransform;
|
||||
|
||||
onMoveEnd(flowTransform);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
d3Zoom.on('end', null);
|
||||
}
|
||||
}
|
||||
}, [d3Zoom, onMoveEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
if (d3Zoom) {
|
||||
|
||||
@@ -105,6 +105,8 @@ export interface StoreModel {
|
||||
|
||||
updateTransform: Action<StoreModel, TransformXYK>;
|
||||
|
||||
setInitTransform: Action<StoreModel, TransformXYK>;
|
||||
|
||||
updateSize: Action<StoreModel, Dimensions>;
|
||||
|
||||
initD3: Action<StoreModel, Element>;
|
||||
@@ -342,6 +344,18 @@ export const storeModel: StoreModel = {
|
||||
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);
|
||||
}
|
||||
}),
|
||||
|
||||
updateSize: action((state, size) => {
|
||||
state.width = size.width;
|
||||
state.height = size.height;
|
||||
|
||||
@@ -241,3 +241,9 @@ export type NodePosUpdate = {
|
||||
id: ElementId;
|
||||
pos: XYPosition;
|
||||
};
|
||||
|
||||
export type FlowTransform = {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user