fitView()}>
+
fitView({ padding: 0.1 })}
+ >
)}
diff --git a/src/additional-components/MiniMap/MiniMapNode.tsx b/src/additional-components/MiniMap/MiniMapNode.tsx
index 3f316df9..e85ba788 100644
--- a/src/additional-components/MiniMap/MiniMapNode.tsx
+++ b/src/additional-components/MiniMap/MiniMapNode.tsx
@@ -13,7 +13,7 @@ const MiniMapNode = ({ node, color, borderRadius }: MiniMapNodeProps) => {
position: { x, y },
width,
height,
- } = node.__rg;
+ } = node.__rf;
const { background, backgroundColor } = node.style || {};
const fill = (background || backgroundColor || color) as string;
diff --git a/src/components/ConnectionLine/index.tsx b/src/components/ConnectionLine/index.tsx
index 9297562c..10db3023 100644
--- a/src/components/ConnectionLine/index.tsx
+++ b/src/components/ConnectionLine/index.tsx
@@ -46,12 +46,12 @@ export default ({
const connectionLineClasses: string = cx('react-flow__connection', className);
const sourceHandle = handleId
- ? sourceNode.__rg.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
- : sourceNode.__rg.handleBounds[connectionHandleType][0];
- const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rg.width / 2;
- const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rg.height;
- const sourceX = sourceNode.__rg.position.x + sourceHandleX;
- const sourceY = sourceNode.__rg.position.y + sourceHandleY;
+ ? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
+ : sourceNode.__rf.handleBounds[connectionHandleType][0];
+ const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
+ const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
+ const sourceX = sourceNode.__rf.position.x + sourceHandleX;
+ const sourceY = sourceNode.__rf.position.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) * (1 / transform[2]);
const targetY = (connectionPositionY - transform[1]) * (1 / transform[2]);
diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx
index df6d1158..9b9d7640 100644
--- a/src/components/Edges/wrapEdge.tsx
+++ b/src/components/Edges/wrapEdge.tsx
@@ -1,7 +1,7 @@
import React, { memo, ComponentType, CSSProperties } from 'react';
import cx from 'classnames';
-import store from '../../store';
+import { useStoreActions } from '../../store/hooks';
import { ElementId, Edge, EdgeCompProps } from '../../types';
interface EdgeWrapperProps {
@@ -38,6 +38,7 @@ export default (EdgeComponent: ComponentType
) => {
className,
...rest
}: EdgeWrapperProps) => {
+ const setSelectedElements = useStoreActions((a) => a.setSelectedElements);
const edgeClasses = cx('react-flow__edge', `react-flow__edge-${type}`, className, { selected, animated });
const edgeGroupStyle: CSSProperties = {
pointerEvents: isInteractive ? 'all' : 'none',
@@ -47,7 +48,7 @@ export default (EdgeComponent: ComponentType) => {
return;
}
- store.dispatch.setSelectedElements({ id, source, target });
+ setSelectedElements({ id, source, target });
if (onClick) {
onClick({ id, source, target, type });
diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx
index 8a707657..c23eb40f 100644
--- a/src/components/NodesSelection/index.tsx
+++ b/src/components/NodesSelection/index.tsx
@@ -17,8 +17,8 @@ function getStartPositions(nodes: Node[]): StartPositions {
return nodes.reduce((res, node) => {
const startPosition = {
- x: node.__rg.position.x || node.position.x,
- y: node.__rg.position.y || node.position.y,
+ x: node.__rf.position.x || node.position.x,
+ y: node.__rf.position.y || node.position.y,
};
res[node.id] = startPosition;
@@ -38,9 +38,14 @@ export default memo(() => {
const nodes = useStoreState((s) => s.nodes);
const updateNodePos = useStoreActions((a) => a.updateNodePos);
+
const position = selectedNodesBbox;
const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number];
+ if (!selectedElements) {
+ return null;
+ }
+
const onStart = (evt: MouseEvent) => {
const scaledClient: XYPosition = {
x: evt.clientX / tScale,
diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx
index 1922e200..6064d05e 100644
--- a/src/components/UserSelection/index.tsx
+++ b/src/components/UserSelection/index.tsx
@@ -2,13 +2,14 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
-import React, { memo } from 'react';
+import React, { memo, useEffect } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { XYPosition } from '../../types';
type UserSelectionProps = {
isInteractive: boolean;
+ selectionKeyPressed: boolean;
};
function getMousePosition(evt: React.MouseEvent): XYPosition | void {
@@ -44,25 +45,37 @@ const SelectionRect = () => {
);
};
-export default memo(({ isInteractive }: UserSelectionProps) => {
+export default memo(({ isInteractive, selectionKeyPressed }: UserSelectionProps) => {
+ const selectionActive = useStoreState((s) => s.selectionActive);
+
const setUserSelection = useStoreActions((a) => a.setUserSelection);
const updateUserSelection = useStoreActions((a) => a.updateUserSelection);
const unsetUserSelection = useStoreActions((a) => a.unsetUserSelection);
+ const renderUserSelectionPane = selectionActive || selectionKeyPressed;
- if (!isInteractive) {
+ useEffect(() => {
+ if (!selectionKeyPressed) {
+ unsetUserSelection();
+ }
+ }, [selectionKeyPressed]);
+
+ if (!isInteractive || !renderUserSelectionPane) {
return null;
}
- function onMouseDown(evt: React.MouseEvent): void {
+ const onMouseDown = (evt: React.MouseEvent): void => {
const mousePos = getMousePosition(evt);
if (!mousePos) {
return;
}
setUserSelection(mousePos);
- }
+ };
- function onMouseMove(evt: React.MouseEvent): void {
+ const onMouseMove = (evt: React.MouseEvent): void => {
+ if (!selectionKeyPressed || !selectionActive) {
+ return;
+ }
const mousePos = getMousePosition(evt);
if (!mousePos) {
@@ -70,11 +83,9 @@ export default memo(({ isInteractive }: UserSelectionProps) => {
}
updateUserSelection(mousePos);
- }
+ };
- function onMouseUp() {
- unsetUserSelection();
- }
+ const onMouseUp = () => unsetUserSelection();
return (
s.height);
const d3Initialised = useStoreState((s) => s.d3Initialised);
const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive);
-
const updateSize = useStoreActions((actions) => actions.updateSize);
const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection);
const setOnConnect = useStoreActions((a) => a.setOnConnect);
@@ -88,6 +87,8 @@ const GraphView = memo(
const setInteractive = useStoreActions((actions) => actions.setInteractive);
const updateTransform = useStoreActions((actions) => actions.updateTransform);
const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
+ const fitView = useStoreActions((actions) => actions.fitView);
+ const zoom = useStoreActions((actions) => actions.zoom);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });
@@ -146,9 +147,9 @@ const GraphView = memo(
useEffect(() => {
if (d3Initialised && onLoad) {
onLoad({
- fitView,
- zoomIn,
- zoomOut,
+ fitView: (params = { padding: 0.1 }) => fitView(params),
+ zoomIn: () => zoom(0.2),
+ zoomOut: () => zoom(-0.2),
project,
getElements,
});
@@ -188,7 +189,7 @@ const GraphView = memo(
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
/>
- {selectionKeyPressed &&
}
+
{nodesSelectionActive &&
}
diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx
index 6c9d684b..df5e68e5 100644
--- a/src/container/NodeRenderer/index.tsx
+++ b/src/container/NodeRenderer/index.tsx
@@ -34,8 +34,8 @@ function renderNode(
id={node.id}
type={nodeType}
data={node.data}
- xPos={node.__rg.position.x}
- yPos={node.__rg.position.y}
+ xPos={node.__rf.position.x}
+ yPos={node.__rf.position.y}
onClick={props.onElementClick}
onNodeDragStart={props.onNodeDragStart}
onNodeDragStop={props.onNodeDragStop}
diff --git a/src/hooks/useD3Zoom.ts b/src/hooks/useD3Zoom.ts
index 95f3a0d9..ec2028bd 100644
--- a/src/hooks/useD3Zoom.ts
+++ b/src/hooks/useD3Zoom.ts
@@ -1,6 +1,5 @@
import { useEffect, MutableRefObject } from 'react';
-import { zoom, zoomIdentity } from 'd3-zoom';
-import { select, event } from 'd3-selection';
+import { event } from 'd3-selection';
import { useStoreState, useStoreActions } from '../store/hooks';
@@ -11,8 +10,6 @@ interface UseD3ZoomParams {
}
export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => {
- const transform = useStoreState((s) => s.transform);
- const d3Selection = useStoreState((s) => s.d3Selection);
const d3Zoom = useStoreState((s) => s.d3Zoom);
const initD3 = useStoreActions((actions) => actions.initD3);
@@ -20,41 +17,27 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
useEffect(() => {
if (zoomPane.current) {
- const nextD3ZoomInstance = zoom();
- const selection = select(zoomPane.current).call(nextD3ZoomInstance);
- initD3({ zoom: nextD3ZoomInstance, selection });
+ initD3(zoomPane.current);
}
}, []);
useEffect(() => {
- if (!d3Zoom) {
- return;
- }
+ if (d3Zoom) {
+ if (selectionKeyPressed) {
+ d3Zoom.on('zoom', null);
+ } else {
+ d3Zoom.on('zoom', function () {
+ if (!event.sourceEvent || (event.sourceEvent && event.sourceEvent.target !== zoomPane.current)) {
+ return;
+ }
- if (selectionKeyPressed) {
- d3Zoom.on('zoom', null);
- } else {
- d3Zoom.on('zoom', () => {
- if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) {
- return;
- }
+ updateTransform(event.transform);
- updateTransform(event.transform);
-
- if (onMove) {
- onMove();
- }
- });
-
- if (d3Selection && d3Zoom) {
- // we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced
- const graphTransform = zoomIdentity.translate(transform[0], transform[1]).scale(transform[2]);
- d3Selection.call(d3Zoom.transform, graphTransform);
+ if (onMove) {
+ onMove();
+ }
+ });
}
}
-
- return () => {
- d3Zoom.on('zoom', null);
- };
}, [selectionKeyPressed, d3Zoom]);
};
diff --git a/src/hooks/useElementUpdater.ts b/src/hooks/useElementUpdater.ts
index c8c6936d..51877d11 100644
--- a/src/hooks/useElementUpdater.ts
+++ b/src/hooks/useElementUpdater.ts
@@ -39,8 +39,8 @@ const useElementUpdater = (elements: Elements): void => {
};
if (positionChanged) {
- nodeProps.__rg = {
- ...existingNode.__rg,
+ nodeProps.__rf = {
+ ...existingNode.__rf,
position: propNode.position,
};
nodeProps.position = propNode.position;
diff --git a/src/store/index.ts b/src/store/index.ts
index 5d843a83..be3e9a68 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,6 +1,8 @@
-import { createStore, Action, action } from 'easy-peasy';
+import { createStore, Action, action, Thunk, thunk } from 'easy-peasy';
import isEqual from 'fast-deep-equal';
import { Selection as D3Selection, ZoomBehavior } from 'd3';
+import { zoom, zoomIdentity, zoomTransform } from 'd3-zoom';
+import { select } from 'd3-selection';
import { getDimensions } from '../utils';
import { getHandleBounds } from '../components/Nodes/utils';
@@ -20,6 +22,7 @@ import {
HandleType,
SetConnectionId,
NodePosUpdate,
+ FitViewParams,
} from '../types';
type TransformXYK = {
@@ -38,11 +41,6 @@ type SelectionUpdate = {
selection?: SelectionRect;
};
-type D3Init = {
- zoom: ZoomBehavior;
- selection: D3Selection;
-};
-
type SetMinMaxZoom = {
minZoom: number;
maxZoom: number;
@@ -107,7 +105,7 @@ export interface StoreModel {
updateSize: Action;
- initD3: Action;
+ initD3: Action;
setMinMaxZoom: Action;
@@ -122,6 +120,11 @@ export interface StoreModel {
setUserSelection: Action;
updateUserSelection: Action;
unsetUserSelection: Action;
+
+ fitView: Action;
+ zoom: Action;
+ zoomIn: Thunk;
+ zoomOut: Thunk;
}
export const storeModel: StoreModel = {
@@ -185,7 +188,7 @@ export const storeModel: StoreModel = {
// only update when size change
if (
!matchingNode ||
- (matchingNode.__rg.width === dimensions.width && matchingNode.__rg.height === dimensions.height)
+ (matchingNode.__rf.width === dimensions.width && matchingNode.__rf.height === dimensions.height)
) {
return;
}
@@ -197,8 +200,8 @@ export const storeModel: StoreModel = {
state.nodes.forEach((n) => {
if (n.id === id) {
- n.__rg = {
- ...n.__rg,
+ n.__rf = {
+ ...n.__rf,
...dimensions,
handleBounds,
};
@@ -220,8 +223,8 @@ export const storeModel: StoreModel = {
state.nodes.forEach((n) => {
if (n.id === id) {
- n.__rg = {
- ...n.__rg,
+ n.__rf = {
+ ...n.__rf,
position,
};
}
@@ -335,11 +338,11 @@ export const storeModel: StoreModel = {
state.height = size.height;
}),
- initD3: action((state, { zoom, selection }) => {
- state.d3Zoom = zoom;
-
- state.d3Zoom.scaleExtent([state.minZoom, state.maxZoom]);
+ initD3: action((state, zoomPaneNode) => {
+ const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]);
+ const selection = select(zoomPaneNode).call(d3ZoomInstance);
+ state.d3Zoom = d3ZoomInstance;
state.d3Selection = selection;
state.d3Initialised = true;
}),
@@ -370,6 +373,45 @@ export const storeModel: StoreModel = {
setInteractive: action((state, isInteractive) => {
state.isInteractive = isInteractive;
}),
+
+ fitView: action((state, { padding = 0.1 }) => {
+ const { nodes, width, height, d3Selection, d3Zoom } = state;
+
+ if (!d3Selection || !d3Zoom || !nodes.length) {
+ return;
+ }
+
+ const bounds = getRectOfNodes(nodes);
+ const maxBoundsSize = Math.max(bounds.width, bounds.height);
+ const k = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
+ const boundsCenterX = bounds.x + bounds.width / 2;
+ const boundsCenterY = bounds.y + bounds.height / 2;
+ const transform = [width / 2 - boundsCenterX * k, height / 2 - boundsCenterY * k];
+ const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
+
+ d3Selection.call(d3Zoom.transform, fittedTransform);
+
+ state.transform = [fittedTransform.x, fittedTransform.y, fittedTransform.k];
+ }),
+
+ zoom: action((state, amount) => {
+ const { d3Zoom, d3Selection, transform } = state;
+ const nextZoom = transform[2] + amount;
+
+ if (d3Zoom && d3Selection) {
+ d3Zoom.scaleTo(d3Selection, nextZoom);
+ const transforms = zoomTransform(d3Selection.node() as Element);
+ state.transform = [transforms.x, transforms.y, transforms.k];
+ }
+ }),
+
+ zoomIn: thunk((actions) => {
+ actions.zoom(0.2);
+ }),
+
+ zoomOut: thunk((actions) => {
+ actions.zoom(-0.2);
+ }),
};
const store = createStore(storeModel);
diff --git a/src/style.css b/src/style.css
index 1424190d..731a6191 100644
--- a/src/style.css
+++ b/src/style.css
@@ -69,6 +69,7 @@
.react-flow__edge-text {
font-size: 12px;
pointer-events: none;
+ user-select: none;
}
.react-flow__edge-textbg {
@@ -132,7 +133,6 @@
.react-flow__node-output {
background: #55dd99;
-
}
.react-flow__nodesselection {
@@ -184,4 +184,4 @@
right: 0;
top: 50%;
transform: translate(0, -50%);
-}
\ No newline at end of file
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 859d2727..050ad740 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -34,7 +34,7 @@ export interface Node {
id: ElementId;
position: XYPosition;
type?: string;
- __rg?: any;
+ __rf?: any;
data?: any;
style?: CSSProperties;
className?: string;
@@ -134,7 +134,7 @@ export interface WrapNodeProps {
}
export type FitViewParams = {
- padding: number;
+ padding?: number;
};
export type FitViewFunc = (fitViewOptions: FitViewParams) => void;
export type ProjectFunc = (position: XYPosition) => XYPosition;
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index 65c06be9..5d6f7f4c 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -1,5 +1,3 @@
-import { zoomIdentity } from 'd3-zoom';
-
import store from '../store';
import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types';
@@ -96,7 +94,7 @@ export const parseElement = (element: Node | Edge): Node | Edge => {
...element,
id: element.id.toString(),
type: element.type || 'default',
- __rg: {
+ __rf: {
position: element.position,
width: null,
height: null,
@@ -131,7 +129,7 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce(
- (currBox, { __rg: { position, width, height } }) =>
+ (currBox, { __rf: { position, width, height } }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
);
@@ -157,7 +155,7 @@ export const getNodesInside = (
height: rect.height / tScale,
});
- return nodes.filter(({ __rg: { position, width, height } }) => {
+ return nodes.filter(({ __rf: { position, width, height } }) => {
const nBox = rectToBox({ ...position, width, height });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
@@ -182,29 +180,12 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
});
};
-export const fitView = ({ padding }: FitViewParams = { padding: 0.1 }): void => {
- const { nodes, width, height, d3Selection, d3Zoom } = store.getState();
-
- if (!d3Selection || !d3Zoom || !nodes.length) {
- return;
- }
-
- const bounds = getRectOfNodes(nodes);
- const maxBoundsSize = Math.max(bounds.width, bounds.height);
- const k = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
- const boundsCenterX = bounds.x + bounds.width / 2;
- const boundsCenterY = bounds.y + bounds.height / 2;
- const transform = [width / 2 - boundsCenterX * k, height / 2 - boundsCenterY * k];
- const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
-
- d3Selection.call(d3Zoom.transform, fittedTransform);
+export const fitView = (params: FitViewParams = { padding: 0.1 }): void => {
+ store.getActions().fitView(params);
};
const zoom = (amount: number): void => {
- const { d3Zoom, d3Selection, transform } = store.getState();
- if (d3Zoom && d3Selection) {
- d3Zoom.scaleTo(d3Selection, transform[2] + amount);
- }
+ store.getActions().zoom(amount);
};
export const zoomIn = (): void => zoom(0.2);
@@ -218,7 +199,7 @@ export const getElements = (): Elements => {
...nodes.map((node) => {
const n = { ...node };
- delete n.__rg;
+ delete n.__rf;
return n;
}),
...edges.map((e) => ({ ...e })),