+
{showZoom && (
<>
> = ({
{showInteractive && (
diff --git a/src/additional-components/MiniMap/index.tsx b/src/additional-components/MiniMap/index.tsx
index 7b579d28..bf3fe8d6 100644
--- a/src/additional-components/MiniMap/index.tsx
+++ b/src/additional-components/MiniMap/index.tsx
@@ -3,7 +3,6 @@ import cc from 'classcat';
import shallow from 'zustand/shallow';
import MiniMapNode from './MiniMapNode';
-
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
@@ -19,9 +18,11 @@ const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
- nodeInternals: s.nodeInternals,
+ nodes: Array.from(s.nodeInternals.values()),
});
+const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
+
const MiniMap = ({
style,
className,
@@ -32,30 +33,19 @@ const MiniMap = ({
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
- const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
- const [tX, tY, tScale] = transform;
-
- const mapClasses = cc(['react-flow__minimap', className]);
- const elementWidth = (style?.width || defaultWidth)! as number;
- const elementHeight = (style?.height || defaultHeight)! as number;
- const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
- const nodeStrokeColorFunc = (
- nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
- ) as GetMiniMapNodeAttribute;
- const nodeClassNameFunc = (
- nodeClassName instanceof Function ? nodeClassName : () => nodeClassName
- ) as GetMiniMapNodeAttribute;
- const hasNodes = nodeInternals && nodeInternals.size > 0;
- // @TODO: work with nodeInternals instead of converting it to an array
- const nodes = Array.from(nodeInternals).map(([_, node]) => node);
- const bb = getRectOfNodes(nodes);
+ const { width: containerWidth, height: containerHeight, transform, nodes } = useStore(selector, shallow);
+ const elementWidth = (style?.width as number) ?? defaultWidth;
+ const elementHeight = (style?.height as number) ?? defaultHeight;
+ const nodeColorFunc = getAttrFunction(nodeColor);
+ const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
+ const nodeClassNameFunc = getAttrFunction(nodeClassName);
const viewBB: Rect = {
- x: -tX / tScale,
- y: -tY / tScale,
- width: containerWidth / tScale,
- height: containerHeight / tScale,
+ x: -transform[0] / transform[2],
+ y: -transform[1] / transform[2],
+ width: containerWidth / transform[2],
+ height: containerHeight / transform[2],
};
- const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB;
+ const boundingRect = nodes.length > 0 ? getBoundsofRects(getRectOfNodes(nodes), viewBB) : viewBB;
const scaledWidth = boundingRect.width / elementWidth;
const scaledHeight = boundingRect.height / elementHeight;
const viewScale = Math.max(scaledWidth, scaledHeight);
@@ -74,18 +64,16 @@ const MiniMap = ({
height={elementHeight}
viewBox={`${x} ${y} ${width} ${height}`}
style={style}
- className={mapClasses}
+ className={cc(['react-flow__minimap', className])}
>
- {Array.from(nodeInternals)
- .filter(([_, node]) => !node.hidden && node.width && node.height)
- .map(([_, node]) => {
- const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
-
+ {nodes
+ .filter((node) => !node.hidden && node.width && node.height)
+ .map((node) => {
return (
{
if (typeof markerEndId !== 'undefined' && markerEndId) {
@@ -52,3 +55,16 @@ export const getCenter = ({
return [centerX, centerY, xOffset, yOffset];
};
+
+export function getMouseHandler(
+ id: string,
+ getState: GetState,
+ handler?: (event: ReactMouseEvent, edge: Edge) => void
+) {
+ return handler === undefined
+ ? handler
+ : (event: ReactMouseEvent) => {
+ const edge = getState().edges.find((e) => e.id === id)!;
+ handler(event, { ...edge });
+ };
+}
diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx
index 47fb9c85..7f2a66b9 100644
--- a/src/components/Edges/wrapEdge.tsx
+++ b/src/components/Edges/wrapEdge.tsx
@@ -1,12 +1,13 @@
-import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
+import React, { memo, ComponentType, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
-import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
+import { EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { handleMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
+import { getMouseHandler } from './utils';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
@@ -53,10 +54,78 @@ export default (EdgeComponent: ComponentType) => {
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
- const store = useStoreApi();
- const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
-
const [updating, setUpdating] = useState(false);
+ const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
+ const store = useStoreApi();
+
+ const onEdgeClick = (event: React.MouseEvent): void => {
+ const edge = store.getState().edges.find((e) => e.id === id)!;
+
+ if (elementsSelectable) {
+ store.setState({ nodesSelectionActive: false });
+ addSelectedEdges([id]);
+ }
+
+ onClick?.(event, edge);
+ };
+
+ const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
+ const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
+ const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
+ const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
+ const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
+
+ const handleEdgeUpdater = (event: React.MouseEvent, isSourceHandle: boolean) => {
+ const nodeId = isSourceHandle ? target : source;
+ const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
+ const handleType = isSourceHandle ? 'target' : 'source';
+ const isValidConnection = () => true;
+ const isTarget = isSourceHandle;
+ const edge = store.getState().edges.find((e) => e.id === id)!;
+
+ onEdgeUpdateStart?.(event, edge, handleType);
+
+ const _onEdgeUpdate = onEdgeUpdateEnd
+ ? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edge, handleType)
+ : undefined;
+
+ const onConnectEdge = (connection: Connection) => {
+ const { edges } = store.getState();
+ const edge = edges.find((e) => e.id === id);
+
+ if (edge && onEdgeUpdate) {
+ onEdgeUpdate(edge, connection);
+ }
+ };
+
+ handleMouseDown(
+ event,
+ handleId,
+ nodeId,
+ store.setState,
+ onConnectEdge,
+ isTarget,
+ isValidConnection,
+ connectionMode,
+ handleType,
+ _onEdgeUpdate,
+ store.getState
+ );
+ };
+
+ const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent): void =>
+ handleEdgeUpdater(event, true);
+ const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent): void =>
+ handleEdgeUpdater(event, false);
+
+ const onEdgeUpdaterMouseEnter = () => setUpdating(true);
+ const onEdgeUpdaterMouseOut = () => setUpdating(false);
+ const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
+ const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
+
+ if (hidden) {
+ return null;
+ }
const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
@@ -67,139 +136,6 @@ export default (EdgeComponent: ComponentType) => {
{ selected, animated, inactive, updating },
]);
- const edgeElement = useMemo(() => {
- const el: Edge = {
- id,
- source,
- target,
- type,
- };
-
- if (sourceHandleId) {
- el.sourceHandle = sourceHandleId;
- }
-
- if (targetHandleId) {
- el.targetHandle = targetHandleId;
- }
-
- if (typeof data !== 'undefined') {
- el.data = data;
- }
-
- return el;
- }, [id, source, target, type, sourceHandleId, targetHandleId, data]);
-
- const onEdgeClick = useCallback(
- (event: React.MouseEvent): void => {
- if (elementsSelectable) {
- store.setState({ nodesSelectionActive: false });
- addSelectedEdges([edgeElement.id]);
- }
-
- onClick?.(event, edgeElement);
- },
- [elementsSelectable, edgeElement, onClick]
- );
-
- const onEdgeDoubleClickHandler = useCallback(
- (event: React.MouseEvent) => {
- onEdgeDoubleClick?.(event, edgeElement);
- },
- [edgeElement, onEdgeDoubleClick]
- );
-
- const onEdgeContextMenu = useCallback(
- (event: React.MouseEvent): void => {
- onContextMenu?.(event, edgeElement);
- },
- [edgeElement, onContextMenu]
- );
-
- const onEdgeMouseEnter = useCallback(
- (event: React.MouseEvent): void => {
- onMouseEnter?.(event, edgeElement);
- },
- [edgeElement, onContextMenu]
- );
-
- const onEdgeMouseMove = useCallback(
- (event: React.MouseEvent): void => {
- onMouseMove?.(event, edgeElement);
- },
- [edgeElement, onContextMenu]
- );
-
- const onEdgeMouseLeave = useCallback(
- (event: React.MouseEvent): void => {
- onMouseLeave?.(event, edgeElement);
- },
- [edgeElement, onContextMenu]
- );
-
- const handleEdgeUpdater = useCallback(
- (event: React.MouseEvent, isSourceHandle: boolean) => {
- const nodeId = isSourceHandle ? target : source;
- const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
- const handleType = isSourceHandle ? 'target' : 'source';
- const isValidConnection = () => true;
- const isTarget = isSourceHandle;
-
- onEdgeUpdateStart?.(event, edgeElement, handleType);
-
- const _onEdgeUpdate = onEdgeUpdateEnd
- ? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType)
- : undefined;
-
- const onConnectEdge = (connection: Connection) => {
- const { edges } = store.getState();
- const edge = edges.find((e) => e.id === id);
-
- if (edge && onEdgeUpdate) {
- onEdgeUpdate(edge, connection);
- }
- };
-
- handleMouseDown(
- event,
- handleId,
- nodeId,
- store.setState,
- onConnectEdge,
- isTarget,
- isValidConnection,
- connectionMode,
- handleType,
- _onEdgeUpdate,
- store.getState
- );
- },
- [id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
- );
-
- const onEdgeUpdaterSourceMouseDown = useCallback(
- (event: React.MouseEvent): void => {
- handleEdgeUpdater(event, true);
- },
- [id, source, sourceHandleId, handleEdgeUpdater]
- );
-
- const onEdgeUpdaterTargetMouseDown = useCallback(
- (event: React.MouseEvent): void => {
- handleEdgeUpdater(event, false);
- },
- [id, target, targetHandleId, handleEdgeUpdater]
- );
-
- const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
- const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
- const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
- const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
-
- if (hidden) {
- return null;
- }
-
return (
({
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
+ onClickConnectStart: s.onClickConnectStart,
+ onClickConnectStop: s.onClickConnectStop,
+ onClickConnectEnd: s.onClickConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
@@ -47,6 +50,9 @@ const Handle = forwardRef(
onConnectStart,
onConnectStop,
onConnectEnd,
+ onClickConnectStart,
+ onClickConnectStop,
+ onClickConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
@@ -56,99 +62,71 @@ const Handle = forwardRef(
const handleId = id || null;
const isTarget = type === 'target';
- const onConnectExtended = useCallback(
- (params: Connection) => {
- const { defaultEdgeOptions } = store.getState();
+ const onConnectExtended = (params: Connection) => {
+ const { defaultEdgeOptions } = store.getState();
- const edgeParams = {
- ...defaultEdgeOptions,
- ...params,
- };
- if (hasDefaultEdges) {
- const { edges } = store.getState();
- store.setState({ edges: addEdge(edgeParams, edges) });
- }
+ const edgeParams = {
+ ...defaultEdgeOptions,
+ ...params,
+ };
+ if (hasDefaultEdges) {
+ const { edges } = store.getState();
+ store.setState({ edges: addEdge(edgeParams, edges) });
+ }
- onConnectAction?.(edgeParams);
- onConnect?.(edgeParams);
- },
- [hasDefaultEdges, onConnectAction, onConnect]
- );
+ onConnectAction?.(edgeParams);
+ onConnect?.(edgeParams);
+ };
- const onMouseDownHandler = useCallback(
- (event: React.MouseEvent) => {
- if (event.button === 0) {
- handleMouseDown(
- event,
- handleId,
- nodeId,
- store.setState,
- onConnectExtended,
- isTarget,
- isValidConnection,
- connectionMode,
- undefined,
- undefined,
- onConnectStart,
- onConnectStop,
- onConnectEnd
- );
- }
- onMouseDown?.(event);
- },
- [
- handleId,
- nodeId,
- onConnectExtended,
- isTarget,
- isValidConnection,
+ const onMouseDownHandler = (event: React.MouseEvent) => {
+ if (event.button === 0) {
+ handleMouseDown(
+ event,
+ handleId,
+ nodeId,
+ store.setState,
+ onConnectExtended,
+ isTarget,
+ isValidConnection,
+ connectionMode,
+ undefined,
+ undefined,
+ onConnectStart,
+ onConnectStop,
+ onConnectEnd
+ );
+ }
+ onMouseDown?.(event);
+ };
+
+ const onClick = (event: React.MouseEvent) => {
+ if (!connectionStartHandle) {
+ onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
+ store.setState({ connectionStartHandle: { nodeId, type, handleId } });
+ return;
+ }
+
+ const doc = getHostForElement(event.target as HTMLElement);
+ const { connection, isValid } = checkElementBelowIsValid(
+ event as unknown as MouseEvent,
connectionMode,
- onConnectStart,
- onConnectStop,
- onConnectEnd,
- ]
- );
+ connectionStartHandle.type === 'target',
+ connectionStartHandle.nodeId,
+ connectionStartHandle.handleId || null,
+ isValidConnection,
+ doc
+ );
- const onClick = useCallback(
- (event: React.MouseEvent) => {
- if (!connectionStartHandle) {
- onConnectStart?.(event, { nodeId, handleId, handleType: type });
- store.setState({ connectionStartHandle: { nodeId, type, handleId } });
- } else {
- const doc = getHostForElement(event.target as HTMLElement);
- const { connection, isValid } = checkElementBelowIsValid(
- event as unknown as MouseEvent,
- connectionMode,
- connectionStartHandle.type === 'target',
- connectionStartHandle.nodeId,
- connectionStartHandle.handleId || null,
- isValidConnection,
- doc
- );
+ onClickConnectStop?.(event as unknown as MouseEvent);
- onConnectStop?.(event as unknown as MouseEvent);
+ if (isValid) {
+ onConnectExtended(connection);
+ }
- if (isValid) {
- onConnectExtended(connection);
- }
+ onClickConnectEnd?.(event as unknown as MouseEvent);
- onConnectEnd?.(event as unknown as MouseEvent);
-
- store.setState({ connectionStartHandle: null });
- }
- },
- [
- connectionStartHandle,
- onConnectStart,
- onConnectExtended,
- onConnectStop,
- onConnectEnd,
- isTarget,
- nodeId,
- handleId,
- type,
- ]
- );
+ store.setState({ connectionStartHandle: null });
+ };
const handleClasses = cc([
'react-flow__handle',
diff --git a/src/components/Nodes/useMemoizedMouseHandler.ts b/src/components/Nodes/useMemoizedMouseHandler.ts
deleted file mode 100644
index 36334695..00000000
--- a/src/components/Nodes/useMemoizedMouseHandler.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { MouseEvent, useCallback } from 'react';
-import { GetState } from 'zustand';
-
-import { ReactFlowState, Node } from '../../types';
-
-function useMemoizedMouseHandler(
- id: string,
- dragging: boolean,
- getState: GetState,
- handler?: (event: MouseEvent, node: Node) => void
-) {
- const memoizedHandler = useCallback(
- (event: MouseEvent) => {
- if (typeof handler !== 'undefined' && !dragging) {
- const node = getState().nodeInternals.get(id)!;
- handler(event, { ...node });
- }
- },
- [handler, dragging, id]
- );
-
- return memoizedHandler;
-}
-
-export default useMemoizedMouseHandler;
diff --git a/src/components/Nodes/utils.ts b/src/components/Nodes/utils.ts
index 1ed82013..2b9b5137 100644
--- a/src/components/Nodes/utils.ts
+++ b/src/components/Nodes/utils.ts
@@ -1,4 +1,7 @@
-import { HandleElement, Position } from '../../types';
+import { MouseEvent } from 'react';
+import { GetState, SetState } from 'zustand';
+
+import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils';
export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
@@ -39,3 +42,42 @@ export const getHandleBoundsByHandleType = (
};
});
};
+
+export function getMouseHandler(
+ id: string,
+ getState: GetState,
+ handler?: (event: MouseEvent, node: Node) => void
+) {
+ return handler === undefined
+ ? handler
+ : (event: MouseEvent) => {
+ const node = getState().nodeInternals.get(id)!;
+ handler(event, { ...node });
+ };
+}
+
+// this handler is called by
+// 1. the click handler when node is not draggable or selectNodesOnDrag = false
+// or
+// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
+export function handleNodeClick({
+ id,
+ store,
+}: {
+ id: string;
+ store: {
+ getState: GetState;
+ setState: SetState;
+ };
+}) {
+ const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
+ const node = nodeInternals.get(id)!;
+
+ store.setState({ nodesSelectionActive: false });
+
+ if (!node.selected) {
+ addSelectedNodes([id]);
+ } else if (node.selected && multiSelectionActive) {
+ unselectNodesAndEdges({ nodes: [node] });
+ }
+}
diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx
index b3f9bb21..55e4d769 100644
--- a/src/components/Nodes/wrapNode.tsx
+++ b/src/components/Nodes/wrapNode.tsx
@@ -1,26 +1,19 @@
-import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
-import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
+import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
import cc from 'classcat';
-import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
-import useMemoizedMouseHandler from './useMemoizedMouseHandler';
+import useDrag from '../../hooks/useDrag';
+import { getMouseHandler, handleNodeClick } from './utils';
-const selector = (s: ReactFlowState) => ({
- addSelectedNodes: s.addSelectedNodes,
- updateNodePosition: s.updateNodePosition,
- unselectNodesAndEdges: s.unselectNodesAndEdges,
- updateNodeDimensions: s.updateNodeDimensions,
-});
+const selector = (s: ReactFlowState) => s.updateNodeDimensions;
export default (NodeComponent: ComponentType) => {
const NodeWrapper = ({
id,
type,
data,
- scale,
xPos,
yPos,
selected,
@@ -29,10 +22,10 @@ export default (NodeComponent: ComponentType) => {
onMouseMove,
onMouseLeave,
onContextMenu,
- onNodeDoubleClick,
- onNodeDragStart,
- onNodeDrag,
- onNodeDragStop,
+ onDoubleClick,
+ onDragStart,
+ onDrag,
+ onDragStop,
style,
className,
isDraggable,
@@ -42,9 +35,6 @@ export default (NodeComponent: ComponentType) => {
sourcePosition,
targetPosition,
hidden,
- snapToGrid,
- snapGrid,
- dragging,
resizeObserver,
dragHandle,
zIndex,
@@ -53,142 +43,36 @@ export default (NodeComponent: ComponentType) => {
noDragClassName,
}: WrapNodeProps) => {
const store = useStoreApi();
- const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
- selector,
- shallow
- );
- const nodeElement = useRef(null);
+ const updateNodeDimensions = useStore(selector);
+ const nodeRef = useRef(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
- const nodeStyle: CSSProperties = useMemo(
- () => ({
- zIndex,
- transform: `translate(${xPos}px,${yPos}px)`,
- pointerEvents: hasPointerEvents ? 'all' : 'none',
- ...style,
- }),
- [zIndex, xPos, yPos, hasPointerEvents, style]
- );
- const grid = useMemo(
- () => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
- [snapToGrid, snapGrid?.[0], snapGrid?.[1]]
- );
-
- const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
- const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
- const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
- const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
- const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
-
- const onSelectNodeHandler = useCallback(
- (event: MouseEvent) => {
- if (!isDraggable) {
- if (isSelectable) {
- store.setState({ nodesSelectionActive: false });
-
- if (!selected) {
- addSelectedNodes([id]);
- }
- }
-
- if (onClick) {
- const node = store.getState().nodeInternals.get(id)!;
- onClick(event, { ...node });
- }
- }
- },
- [isSelectable, selected, isDraggable, onClick, id]
- );
-
- const onDragStart = useCallback(
- (event: DraggableEvent) => {
- if (selectNodesOnDrag && isSelectable) {
- store.setState({ nodesSelectionActive: false });
-
- if (!selected) {
- addSelectedNodes([id]);
- }
- } else if (!selectNodesOnDrag && !selected && isSelectable) {
- const { multiSelectionActive } = store.getState();
- if (multiSelectionActive) {
- addSelectedNodes([id]);
- } else {
- unselectNodesAndEdges();
- store.setState({ nodesSelectionActive: false });
- }
- }
-
- if (onNodeDragStart) {
- const node = store.getState().nodeInternals.get(id)!;
- onNodeDragStart(event as MouseEvent, { ...node });
- }
- },
- [id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
- );
-
- const onDrag = useCallback(
- (event: DraggableEvent, draggableData: DraggableData) => {
- updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
-
- if (onNodeDrag) {
- const node = store.getState().nodeInternals.get(id)!;
- onNodeDrag(event as MouseEvent, {
- ...node,
- dragging: true,
- position: {
- x: node.position.x + draggableData.deltaX,
- y: node.position.y + draggableData.deltaY,
- },
- positionAbsolute: {
- x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
- y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
- },
- });
- }
- },
- [id, onNodeDrag]
- );
-
- const onDragStop = useCallback(
- (event: DraggableEvent) => {
- // onDragStop also gets called when user just clicks on a node.
- // Because of that we set dragging to true inside the onDrag handler and handle the click here
- let node;
-
- if (onClick || onNodeDragStop) {
- node = store.getState().nodeInternals.get(id)!;
- }
-
- if (!dragging) {
- if (isSelectable && !selectNodesOnDrag && !selected) {
- addSelectedNodes([id]);
- }
-
- if (onClick && node) {
- onClick(event as MouseEvent, { ...node });
- }
-
- return;
- }
-
- updateNodePosition({
+ const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
+ const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
+ const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave);
+ const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
+ const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick);
+ const onSelectNodeHandler = (event: MouseEvent) => {
+ if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
+ // this handler gets called within the drag start event when selectNodesOnDrag=true
+ handleNodeClick({
id,
- dragging: false,
+ store,
});
+ }
- if (onNodeDragStop && node) {
- onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
- }
- },
- [id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
- );
+ if (onClick) {
+ const node = store.getState().nodeInternals.get(id)!;
+ onClick(event, { ...node });
+ }
+ };
useEffect(() => {
- if (nodeElement.current && !hidden) {
- const currNode = nodeElement.current;
+ if (nodeRef.current && !hidden) {
+ const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
@@ -201,7 +85,7 @@ export default (NodeComponent: ComponentType) => {
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
- if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
+ if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
@@ -211,69 +95,72 @@ export default (NodeComponent: ComponentType) => {
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
- updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
+ updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
+ const dragging = useDrag({
+ onStart: onDragStart,
+ onDrag: onDrag,
+ onStop: onDragStop,
+ nodeRef,
+ disabled: !isDraggable,
+ noDragClassName,
+ handleSelector: dragHandle,
+ nodeId: id,
+ isSelectable,
+ selectNodesOnDrag,
+ });
+
if (hidden) {
return null;
}
- const nodeClasses = cc([
- 'react-flow__node',
- `react-flow__node-${type}`,
- noPanClassName,
- className,
- {
- selected,
- selectable: isSelectable,
- parent: isParent,
- },
- ]);
-
return (
-
-
-
+
+
+
+
);
};
diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx
index 72f25f96..b3da5045 100644
--- a/src/components/NodesSelection/index.tsx
+++ b/src/components/NodesSelection/index.tsx
@@ -3,14 +3,14 @@
* made a selection with on or several nodes
*/
-import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
-import { DraggableCore, DraggableData } from 'react-draggable';
+import React, { memo, useCallback, useRef, MouseEvent } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
-import { useStore } from '../../store';
+import { useStore, useStoreApi } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
+import useDrag from '../../hooks/useDrag';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -19,17 +19,21 @@ export interface NodesSelectionProps {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
-// @TODO: work with nodeInternals instead of converting it to an array
+
const selector = (s: ReactFlowState) => ({
transform: s.transform,
- selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
- selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
- snapToGrid: s.snapToGrid,
- snapGrid: s.snapGrid,
- updateNodePosition: s.updateNodePosition,
});
+const bboxSelector = (s: ReactFlowState) => {
+ const selectedNodes = Array.from(s.nodeInternals.values()).filter((n) => n.selected);
+ return getRectOfNodes(selectedNodes);
+};
+
+function useGetMemoizedHandler(handler?: (event: MouseEvent, nodes: Node[]) => void) {
+ return useCallback((event: MouseEvent, _: Node, nodes: Node[]) => handler?.(event, nodes), [handler]);
+}
+
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
@@ -37,96 +41,52 @@ function NodesSelection({
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) {
- const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
- selector,
- shallow
- );
- const [tX, tY, tScale] = transform;
+ const store = useStoreApi();
+ const { transform, userSelectionActive } = useStore(selector, shallow);
+ const { width, height, x: left, y: top } = useStore(bboxSelector, shallow);
const nodeRef = useRef(null);
- const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
+ // it's important that these handlers are memoized to avoid multiple creation of d3 drag handler
+ const onStart = useGetMemoizedHandler(onSelectionDragStart);
+ const onDrag = useGetMemoizedHandler(onSelectionDrag);
+ const onStop = useGetMemoizedHandler(onSelectionDragStop);
- const style = useMemo(
- () => ({
- transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
- }),
- [tX, tY, tScale]
- );
+ useDrag({
+ onStart,
+ onDrag,
+ onStop,
+ nodeRef,
+ });
- const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
-
- const innerStyle = useMemo(
- () => ({
- width: selectedNodesBbox.width,
- height: selectedNodesBbox.height,
- top: selectedNodesBbox.y,
- left: selectedNodesBbox.x,
- }),
- [selectedNodesBbox]
- );
-
- const onStart = useCallback(
- (event: MouseEvent) => {
- onSelectionDragStart?.(event, selectedNodes);
- },
- [onSelectionDragStart, selectedNodes]
- );
-
- const onDrag = useCallback(
- (event: MouseEvent, data: DraggableData) => {
- updateNodePosition({
- diff: {
- x: data.deltaX,
- y: data.deltaY,
- },
- dragging: true,
- });
-
- onSelectionDrag?.(event, selectedNodes);
- },
- [onSelectionDrag, selectedNodes, updateNodePosition]
- );
-
- const onStop = useCallback(
- (event: MouseEvent) => {
- updateNodePosition({
- dragging: false,
- });
-
- onSelectionDragStop?.(event, selectedNodes);
- },
- [selectedNodes, onSelectionDragStop]
- );
-
- const onContextMenu = useCallback(
- (event: MouseEvent) => {
- onSelectionContextMenu?.(event, selectedNodes);
- },
- [onSelectionContextMenu, selectedNodes]
- );
-
- if (!selectedNodes?.length || userSelectionActive) {
+ if (userSelectionActive || !width || !height) {
return null;
}
+ const onContextMenu = onSelectionContextMenu
+ ? (event: MouseEvent) => {
+ const selectedNodes = Array.from(store.getState().nodeInternals.values()).filter((n) => n.selected);
+ onSelectionContextMenu(event, selectedNodes);
+ }
+ : undefined;
+
return (
-
-
onStart(event as MouseEvent)}
- onDrag={(event, data) => onDrag(event as MouseEvent, data)}
- onStop={(event) => onStop(event as MouseEvent)}
- nodeRef={nodeRef}
- enableUserSelectHack={false}
- >
-
-
+
);
}
diff --git a/src/components/StoreUpdater/index.tsx b/src/components/StoreUpdater/index.tsx
index afefe1b0..c693d4af 100644
--- a/src/components/StoreUpdater/index.tsx
+++ b/src/components/StoreUpdater/index.tsx
@@ -19,7 +19,7 @@ import {
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
- OnEdgesDelete
+ OnEdgesDelete,
} from '../../types';
interface StoreUpdaterProps {
@@ -31,6 +31,9 @@ interface StoreUpdaterProps {
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
+ onClickConnectStart?: OnConnectStart;
+ onClickConnectStop?: OnConnectStop;
+ onClickConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
@@ -88,6 +91,9 @@ const StoreUpdater = ({
onConnectStart,
onConnectStop,
onConnectEnd,
+ onClickConnectStart,
+ onClickConnectStop,
+ onClickConnectEnd,
nodesDraggable,
nodesConnectable,
minZoom,
@@ -133,6 +139,9 @@ const StoreUpdater = ({
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
+ useDirectStoreUpdater('onClickConnectStart', onClickConnectStart, store.setState);
+ useDirectStoreUpdater('onClickConnectStop', onClickConnectStop, store.setState);
+ useDirectStoreUpdater('onClickConnectEnd', onClickConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
diff --git a/src/components/UserSelection/index.tsx b/src/components/UserSelection/index.tsx
index 03f4621e..76ee7085 100644
--- a/src/components/UserSelection/index.tsx
+++ b/src/components/UserSelection/index.tsx
@@ -2,7 +2,7 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
-import React, { memo, useState, useRef, useCallback } from 'react';
+import React, { memo, useState, useRef } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
@@ -52,16 +52,20 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
- const resetUserSelection = useCallback(() => {
+ if (!elementsSelectable || !renderUserSelectionPane) {
+ return null;
+ }
+
+ const resetUserSelection = () => {
setUserSelectionRect(initialRect);
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
- }, []);
+ };
- const onMouseDown = useCallback((event: React.MouseEvent): void => {
+ const onMouseDown = (event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
@@ -78,7 +82,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
});
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
- }, []);
+ };
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
@@ -98,7 +102,7 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
- const nodes = Array.from(nodeInternals).map(([_, node]) => node);
+ const nodes = Array.from(nodeInternals.values());
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
@@ -122,21 +126,15 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
setUserSelectionRect(nextUserSelectRect);
};
- const onMouseUp = useCallback(() => {
+ const onMouseUp = () => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
-
resetUserSelection();
- }, []);
+ };
- const onMouseLeave = useCallback(() => {
+ const onMouseLeave = () => {
store.setState({ nodesSelectionActive: false });
-
resetUserSelection();
- }, []);
-
- if (!elementsSelectable || !renderUserSelectionPane) {
- return null;
- }
+ };
return (
void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
defaultMarkerColor: string;
@@ -72,7 +74,13 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
return null;
}
- const { connectionLineType, defaultMarkerColor, connectionLineStyle, connectionLineComponent } = props;
+ const {
+ connectionLineType,
+ defaultMarkerColor,
+ connectionLineStyle,
+ connectionLineComponent,
+ connectionLineContainerStyle,
+ } = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
@@ -179,22 +187,29 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
/>
);
})}
- {renderConnectionLine && isMaxLevel && (
-
- )}
))}
+ {renderConnectionLine && (
+
+ )}
>
);
};
diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx
index 57b0ba11..faae8f49 100644
--- a/src/container/FlowRenderer/index.tsx
+++ b/src/container/FlowRenderer/index.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, memo, ReactNode, WheelEvent, MouseEvent } from 'react';
+import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
@@ -8,6 +8,7 @@ import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
+
import { ReactFlowState } from '../../types';
interface FlowRendererProps
@@ -18,6 +19,7 @@ interface FlowRendererProps
| 'edgeTypes'
| 'snapGrid'
| 'connectionLineType'
+ | 'connectionLineContainerStyle'
| 'arrowHeadColor'
| 'onlyRenderVisibleElements'
| 'selectNodesOnDrag'
@@ -67,18 +69,14 @@ const FlowRenderer = ({
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
- const onClick = useCallback(
- (event: MouseEvent) => {
- onPaneClick?.(event);
- resetSelectedElements();
+ const onClick = (event: MouseEvent) => {
+ onPaneClick?.(event);
+ resetSelectedElements();
- store.setState({ nodesSelectionActive: false });
- },
- [onPaneClick]
- );
-
- const onContextMenu = useCallback((event: MouseEvent) => onPaneContextMenu?.(event), [onPaneContextMenu]);
- const onWheel = useCallback((event: WheelEvent) => onPaneScroll?.(event), [onPaneScroll]);
+ store.setState({ nodesSelectionActive: false });
+ };
+ const onContextMenu = (event: MouseEvent) => onPaneContextMenu?.(event);
+ const onWheel = (event: WheelEvent) => onPaneScroll?.(event);
return (
void;
- onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
- onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
- onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
- onNodeMouseLeave?: (event: MouseEvent, node: Node) => void;
- onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
- onNodeDragStart?: (event: MouseEvent, node: Node) => void;
- onNodeDrag?: (event: MouseEvent, node: Node) => void;
- onNodeDragStop?: (event: MouseEvent, node: Node) => void;
+ onNodeClick?: NodeMouseHandler;
+ onNodeDoubleClick?: NodeMouseHandler;
+ onNodeMouseEnter?: NodeMouseHandler;
+ onNodeMouseMove?: NodeMouseHandler;
+ onNodeMouseLeave?: NodeMouseHandler;
+ onNodeContextMenu?: NodeMouseHandler;
+ onNodeDragStart?: NodeDragHandler;
+ onNodeDrag?: NodeDragHandler;
+ onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
- scale: s.transform[2],
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
- snapGrid: s.snapGrid,
- snapToGrid: s.snapToGrid,
- nodeInternals: s.nodeInternals,
});
const NodeRenderer = (props: NodeRendererProps) => {
- const { scale, nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions, snapGrid, snapToGrid } =
- useStore(selector, shallow);
+ const { nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions } = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef();
@@ -68,15 +70,17 @@ const NodeRenderer = (props: NodeRendererProps) => {
return (
{nodes.map((node) => {
- const nodeType = node.type || 'default';
+ let nodeType = node.type || 'default';
- // @ts-ignore
- if (process.env.NODE_ENV === 'development') {
- if (!props.nodeTypes[nodeType]) {
+ if (!props.nodeTypes[nodeType]) {
+ // @ts-ignore
+ if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
}
+
+ nodeType = 'default';
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType
;
@@ -97,20 +101,16 @@ const NodeRenderer = (props: NodeRendererProps) => {
hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0}
- dragging={!!node.dragging}
- snapGrid={snapGrid}
- snapToGrid={snapToGrid}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu}
- onNodeDoubleClick={props.onNodeDoubleClick}
- onNodeDragStart={props.onNodeDragStart}
- onNodeDrag={props.onNodeDrag}
- onNodeDragStop={props.onNodeDragStop}
- scale={scale}
+ onDoubleClick={props.onNodeDoubleClick}
+ onDragStart={props.onNodeDragStart}
+ onDrag={props.onNodeDrag}
+ onDragStop={props.onNodeDragStop}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx
index f51b9a31..681e8abb 100644
--- a/src/container/ReactFlow/index.tsx
+++ b/src/container/ReactFlow/index.tsx
@@ -68,6 +68,9 @@ const ReactFlow = forwardRef(
onConnectStart,
onConnectStop,
onConnectEnd,
+ onClickConnectStart,
+ onClickConnectStop,
+ onClickConnectEnd,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
@@ -87,6 +90,7 @@ const ReactFlow = forwardRef(
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
connectionLineComponent,
+ connectionLineContainerStyle,
deleteKeyCode = 'Backspace',
selectionKeyCode = 'Shift',
multiSelectionKeyCode = 'Meta',
@@ -169,6 +173,7 @@ const ReactFlow = forwardRef(
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
+ connectionLineContainerStyle={connectionLineContainerStyle}
selectionKeyCode={selectionKeyCode}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
@@ -216,6 +221,9 @@ const ReactFlow = forwardRef(
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
+ onClickConnectStart={onClickConnectStart}
+ onClickConnectStop={onClickConnectStop}
+ onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
diff --git a/src/contexts/NodeIdContext.ts b/src/contexts/NodeIdContext.ts
index ba112d70..c43a1c3b 100644
--- a/src/contexts/NodeIdContext.ts
+++ b/src/contexts/NodeIdContext.ts
@@ -1,8 +1,6 @@
import { createContext } from 'react';
-type ContextProps = string | null;
-
-export const NodeIdContext = createContext>(null);
+export const NodeIdContext = createContext(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
diff --git a/src/hooks/useDrag/index.ts b/src/hooks/useDrag/index.ts
new file mode 100644
index 00000000..82a0e8f5
--- /dev/null
+++ b/src/hooks/useDrag/index.ts
@@ -0,0 +1,176 @@
+import { RefObject, useEffect, useRef, MouseEvent, useState, useCallback } from 'react';
+import { D3DragEvent, drag, SubjectPosition } from 'd3-drag';
+import { select } from 'd3-selection';
+
+import { useStoreApi } from '../../store';
+import { pointToRendererPoint } from '../../utils/graph';
+import { NodeDragItem, NodeDragHandler, XYPosition } from '../../types';
+import {
+ getDragItems,
+ getEventHandlerParams,
+ getParentNodePosition,
+ selectorExistsTargetToNode,
+ updatePosition,
+} from './utils';
+import { handleNodeClick } from '../../components/Nodes/utils';
+
+export type UseDragEvent = D3DragEvent;
+export type UseDragData = { dx: number; dy: number };
+
+type UseDragParams = {
+ nodeRef: RefObject;
+ onStart?: NodeDragHandler;
+ onDrag?: NodeDragHandler;
+ onStop?: NodeDragHandler;
+ disabled?: boolean;
+ noDragClassName?: string;
+ handleSelector?: string;
+ nodeId?: string;
+ isSelectable?: boolean;
+ selectNodesOnDrag?: boolean;
+};
+
+function useDrag({
+ onStart,
+ onDrag,
+ onStop,
+ nodeRef,
+ disabled = false,
+ noDragClassName,
+ handleSelector,
+ nodeId,
+ isSelectable,
+ selectNodesOnDrag,
+}: UseDragParams) {
+ const [dragging, setDragging] = useState(false);
+ const store = useStoreApi();
+ const dragItems = useRef();
+ const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
+ const parentPos = useRef({ x: 0, y: 0 });
+
+ // returns the mouse position projected to the RF coordinate system
+ const getMousePosition = useCallback((event: UseDragEvent) => {
+ const { transform, snapGrid, snapToGrid } = store.getState();
+
+ const mousePos = pointToRendererPoint(
+ {
+ x: event.sourceEvent.clientX,
+ y: event.sourceEvent.clientY,
+ },
+ transform,
+ snapToGrid,
+ snapGrid
+ );
+
+ mousePos.x -= parentPos.current.x;
+ mousePos.y -= parentPos.current.y;
+
+ return mousePos;
+ }, []);
+
+ useEffect(() => {
+ if (nodeRef?.current) {
+ const selection = select(nodeRef.current);
+
+ if (disabled) {
+ selection.on('.drag', null);
+ } else {
+ const dragHandler = drag()
+ .on('start', (event: UseDragEvent) => {
+ const { nodeInternals, multiSelectionActive, unselectNodesAndEdges } = store.getState();
+ parentPos.current = getParentNodePosition(nodeInternals, nodeId);
+
+ if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
+ if (!nodeInternals.get(nodeId)?.selected) {
+ // we need to reset selected nodes when selectNodesOnDrag=false
+ unselectNodesAndEdges();
+ }
+ }
+
+ if (nodeId && isSelectable && selectNodesOnDrag) {
+ handleNodeClick({
+ id: nodeId,
+ store,
+ });
+ }
+
+ const mousePos = getMousePosition(event);
+ dragItems.current = getDragItems(nodeInternals, mousePos, nodeId);
+
+ if (onStart && dragItems.current) {
+ const [currentNode, nodes] = getEventHandlerParams({
+ nodeId,
+ dragItems: dragItems.current,
+ nodeInternals,
+ });
+ onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
+ }
+ })
+ .on('drag', (event: UseDragEvent) => {
+ const { updateNodePositions, nodeInternals, nodeExtent } = store.getState();
+ const mousePos = getMousePosition(event);
+
+ // skip events without movement
+ if ((lastPos.current.x !== mousePos.x || lastPos.current.y !== mousePos.y) && dragItems.current) {
+ lastPos.current = mousePos;
+ dragItems.current = dragItems.current.map((n) => updatePosition(n, mousePos, nodeInternals, nodeExtent));
+
+ updateNodePositions(dragItems.current);
+ setDragging(true);
+
+ if (onDrag) {
+ const [currentNode, nodes] = getEventHandlerParams({
+ nodeId,
+ dragItems: dragItems.current,
+ nodeInternals,
+ });
+ onDrag(event.sourceEvent as MouseEvent, currentNode, nodes);
+ }
+ }
+
+ event.on('end', (event) => {
+ if (onStop && dragItems.current) {
+ const [currentNode, nodes] = getEventHandlerParams({
+ nodeId,
+ dragItems: dragItems.current,
+ nodeInternals,
+ });
+ onStop(event.sourceEvent as MouseEvent, currentNode, nodes);
+ }
+ });
+ })
+ .filter((event: MouseEvent) => {
+ const target = event.target as HTMLDivElement;
+ const filter =
+ !event.ctrlKey && !event.button && (!noDragClassName || !target.classList?.contains?.(noDragClassName));
+ return handleSelector
+ ? selectorExistsTargetToNode(target as HTMLDivElement, handleSelector, nodeRef) && filter
+ : filter;
+ });
+
+ selection.call(dragHandler);
+
+ return () => {
+ selection.on('.drag', null);
+ };
+ }
+ }
+ }, [
+ onStart,
+ onDrag,
+ onStop,
+ nodeRef,
+ disabled,
+ noDragClassName,
+ handleSelector,
+ isSelectable,
+ store,
+ nodeId,
+ selectNodesOnDrag,
+ getMousePosition,
+ ]);
+
+ return dragging;
+}
+
+export default useDrag;
diff --git a/src/hooks/useDrag/utils.ts b/src/hooks/useDrag/utils.ts
new file mode 100644
index 00000000..ec674581
--- /dev/null
+++ b/src/hooks/useDrag/utils.ts
@@ -0,0 +1,133 @@
+import { RefObject } from 'react';
+
+import { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types';
+import { clampPosition } from '../../utils';
+
+export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
+ if (!node.parentNode) {
+ return false;
+ }
+
+ const parentNode = nodeInternals.get(node.parentNode);
+
+ if (!parentNode) {
+ return false;
+ }
+
+ if (parentNode.selected) {
+ return true;
+ }
+
+ return isParentSelected(parentNode, nodeInternals);
+}
+
+export function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): XYPosition {
+ const parentNodeId = nodeId ? nodeInternals.get(nodeId)?.parentNode : null;
+ const parentNode = parentNodeId ? nodeInternals.get(parentNodeId) : null;
+
+ return {
+ x: parentNode?.positionAbsolute?.x || 0,
+ y: parentNode?.positionAbsolute?.y || 0,
+ };
+}
+
+export function selectorExistsTargetToNode(target: Element, selector: string, nodeRef: RefObject): boolean {
+ let current = target;
+
+ do {
+ if (current?.matches(selector)) return true;
+ if (current === nodeRef.current) return false;
+ current = current.parentElement as Element;
+ } while (current);
+
+ return false;
+}
+
+// looks for all selected nodes and created a NodeDragItem for each of them
+export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition, nodeId?: string): NodeDragItem[] {
+ return Array.from(nodeInternals.values())
+ .filter((n) => (n.selected || n.id === nodeId) && (!n.parentNode || !isParentSelected(n, nodeInternals)))
+ .map((n) => ({
+ id: n.id,
+ position: n.position,
+ distance: {
+ x: mousePos.x - n.position.x,
+ y: mousePos.y - n.position.y,
+ },
+ delta: {
+ x: 0,
+ y: 0,
+ },
+ extent: n.extent,
+ parentNode: n.parentNode,
+ width: n.width,
+ height: n.height,
+ }));
+}
+
+export function updatePosition(
+ dragItem: NodeDragItem,
+ mousePos: XYPosition,
+ nodeInternals: NodeInternals,
+ nodeExtent?: CoordinateExtent
+): NodeDragItem {
+ let currentExtent = dragItem.extent || nodeExtent;
+ let nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
+
+ if (dragItem.extent === 'parent') {
+ if (dragItem.parentNode && dragItem.width && dragItem.height) {
+ const parent = nodeInternals.get(dragItem.parentNode);
+ currentExtent =
+ parent?.width && parent?.height
+ ? [
+ [0, 0],
+ [parent.width - dragItem.width, parent.height - dragItem.height],
+ ]
+ : currentExtent;
+ } else {
+ // @ts-ignore
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
+ }
+ currentExtent = nodeExtent;
+ }
+ }
+
+ nextPosition = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
+
+ dragItem.delta = {
+ x: nextPosition.x - dragItem.position.x,
+ y: nextPosition.y - dragItem.position.y,
+ };
+ dragItem.position = nextPosition;
+
+ return dragItem;
+}
+
+// returns two params:
+// 1. the dragged node (or the first of the list, if we are dragging a node selection)
+// 2. array of selected nodes (handy when multi selection is active)
+export function getEventHandlerParams({
+ nodeId,
+ dragItems,
+ nodeInternals,
+}: {
+ nodeId?: string;
+ dragItems: NodeDragItem[];
+ nodeInternals: NodeInternals;
+}): [Node, Node[]] {
+ const extentedDragItems: Node[] = dragItems.map((n) => {
+ const node = nodeInternals.get(n.id)!;
+
+ return {
+ ...node,
+ position: n.position,
+ positionAbsolute: {
+ x: (node.positionAbsolute?.x || 0) + n.delta.x,
+ y: (node.positionAbsolute?.y || 0) + n.delta.y,
+ },
+ };
+ });
+
+ return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems];
+}
diff --git a/src/hooks/useVisibleNodes.ts b/src/hooks/useVisibleNodes.ts
index 8bd2e37d..ebda8953 100644
--- a/src/hooks/useVisibleNodes.ts
+++ b/src/hooks/useVisibleNodes.ts
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { useStore } from '../store';
import { getNodesInside } from '../utils/graph';
+
import { ReactFlowState } from '../types';
function useVisibleNodes(onlyRenderVisible: boolean) {
@@ -10,7 +11,7 @@ function useVisibleNodes(onlyRenderVisible: boolean) {
(s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside(s.nodeInternals, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
- : Array.from(s.nodeInternals).map(([_, node]) => node);
+ : Array.from(s.nodeInternals.values());
},
[onlyRenderVisible]
)
diff --git a/src/store/index.ts b/src/store/index.ts
index 14caeeec..727a6515 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -3,29 +3,22 @@ import createContext from 'zustand/context';
import { clampPosition, getDimensions } from '../utils';
import { applyNodeChanges } from '../utils/changes';
-
import {
ReactFlowState,
Node,
Edge,
NodeDimensionUpdate,
- NodeDiffUpdate,
CoordinateExtent,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
+ NodeDragItem,
+ UnselectNodesAndEdgesParams,
} from '../types';
import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
-import {
- createNodeInternals,
- createPositionChange,
- handleControlledEdgeSelectionChange,
- handleControlledNodeSelectionChange,
- isParentSelected,
- fitView,
-} from './utils';
+import { createNodeInternals, fitView, updateNodesAndEdgesSelections } from './utils';
import initialState from './initialState';
const { Provider, useStore, useStoreApi } = createContext();
@@ -96,20 +89,19 @@ const createStore = () =>
onNodesChange?.(changes);
}
},
- updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => {
- const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes } = get();
+ updateNodePositions: (nodeDragItems: NodeDragItem[]) => {
+ const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) {
const changes: NodePositionChange[] = [];
- nodeInternals.forEach((node) => {
- if (node.selected) {
- if (!node.parentNode || !isParentSelected(node, nodeInternals)) {
- changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
- }
- } else if (node.id === id) {
- changes.push(createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals }));
- }
+ nodeDragItems.forEach((node) => {
+ const change: NodePositionChange = {
+ id: node.id,
+ type: 'position',
+ position: node.position,
+ };
+ changes.push(change);
});
if (changes?.length) {
@@ -123,17 +115,8 @@ const createStore = () =>
}
}
},
- // @TODO: can we unify addSelectedNodes and addSelectedEdges somehow?
addSelectedNodes: (selectedNodeIds: string[]) => {
- const {
- multiSelectionActive,
- onNodesChange,
- nodeInternals,
- hasDefaultNodes,
- onEdgesChange,
- hasDefaultEdges,
- edges,
- } = get();
+ const { multiSelectionActive, nodeInternals, edges } = get();
let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null;
@@ -144,32 +127,15 @@ const createStore = () =>
changedEdges = getSelectionChanges(edges, []);
}
- if (changedNodes.length) {
- if (hasDefaultNodes) {
- set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
- }
-
- onNodesChange?.(changedNodes);
- }
-
- if (changedEdges?.length) {
- if (hasDefaultEdges) {
- set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
- }
-
- onEdgesChange?.(changedEdges);
- }
+ updateNodesAndEdgesSelections({
+ changedNodes,
+ changedEdges,
+ get,
+ set,
+ });
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
- const {
- multiSelectionActive,
- onEdgesChange,
- edges,
- hasDefaultEdges,
- nodeInternals,
- hasDefaultNodes,
- onNodesChange,
- } = get();
+ const { multiSelectionActive, edges, nodeInternals } = get();
let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null;
@@ -180,49 +146,33 @@ const createStore = () =>
changedNodes = getSelectionChanges(Array.from(nodeInternals.values()), []);
}
- if (changedEdges.length) {
- if (hasDefaultEdges) {
- set({
- edges: handleControlledEdgeSelectionChange(changedEdges, edges),
- });
- }
- onEdgesChange?.(changedEdges);
- }
-
- if (changedNodes?.length) {
- if (hasDefaultNodes) {
- set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
- }
-
- onNodesChange?.(changedNodes);
- }
+ updateNodesAndEdgesSelections({
+ changedNodes,
+ changedEdges,
+ get,
+ set,
+ });
},
- unselectNodesAndEdges: () => {
- const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
- const nodes = Array.from(nodeInternals.values());
+ unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
+ const { nodeInternals, edges: storeEdges } = get();
+ const nodesToUnselect = nodes ? nodes : Array.from(nodeInternals.values());
+ const edgesToUnselect = edges ? edges : storeEdges;
- const nodesToUnselect = nodes.map((n) => {
+ const changedNodes = nodesToUnselect.map((n) => {
n.selected = false;
return createSelectionChange(n.id, false);
}) as NodeSelectionChange[];
- const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[];
+ const changedEdges = edgesToUnselect.map((edge) =>
+ createSelectionChange(edge.id, false)
+ ) as EdgeSelectionChange[];
- if (nodesToUnselect.length) {
- if (hasDefaultNodes) {
- set({ nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals) });
- }
- onNodesChange?.(nodesToUnselect);
- }
- if (edgesToUnselect.length) {
- if (hasDefaultEdges) {
- set({
- edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
- });
- }
- onEdgesChange?.(edgesToUnselect);
- }
+ updateNodesAndEdgesSelections({
+ changedNodes,
+ changedEdges,
+ get,
+ set,
+ });
},
-
setMinZoom: (minZoom: number) => {
const { d3Zoom, maxZoom } = get();
d3Zoom?.scaleExtent([minZoom, maxZoom]);
@@ -242,7 +192,7 @@ const createStore = () =>
set({ translateExtent });
},
resetSelectedElements: () => {
- const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
+ const { nodeInternals, edges } = get();
const nodes = Array.from(nodeInternals.values());
const nodesToUnselect = nodes
@@ -252,22 +202,12 @@ const createStore = () =>
.filter((e) => e.selected)
.map((e) => createSelectionChange(e.id, false)) as EdgeSelectionChange[];
- if (nodesToUnselect.length) {
- if (hasDefaultNodes) {
- set({
- nodeInternals: handleControlledNodeSelectionChange(nodesToUnselect, nodeInternals),
- });
- }
- onNodesChange?.(nodesToUnselect);
- }
- if (edgesToUnselect.length) {
- if (hasDefaultEdges) {
- set({
- edges: handleControlledEdgeSelectionChange(edgesToUnselect, edges),
- });
- }
- onEdgesChange?.(edgesToUnselect);
- }
+ updateNodesAndEdgesSelections({
+ changedNodes: nodesToUnselect,
+ changedEdges: edgesToUnselect,
+ get,
+ set,
+ });
},
setNodeExtent: (nodeExtent: CoordinateExtent) => {
const { nodeInternals } = get();
diff --git a/src/store/initialState.ts b/src/store/initialState.ts
index 020c0eab..1ad29aed 100644
--- a/src/store/initialState.ts
+++ b/src/store/initialState.ts
@@ -15,7 +15,6 @@ const initialState: ReactFlowStore = {
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
- selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,
diff --git a/src/store/utils.ts b/src/store/utils.ts
index d30dfd4d..d7c5921e 100644
--- a/src/store/utils.ts
+++ b/src/store/utils.ts
@@ -1,18 +1,15 @@
import { zoomIdentity } from 'd3-zoom';
-import { GetState } from 'zustand';
+import { GetState, SetState } from 'zustand';
-import { clampPosition, isNumeric } from '../utils';
+import { isNumeric } from '../utils';
import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph';
import {
- CoordinateExtent,
Edge,
EdgeSelectionChange,
Node,
NodeInternals,
- NodePositionChange,
NodeSelectionChange,
ReactFlowState,
- XYPosition,
XYZPosition,
FitViewOptions,
} from '../types';
@@ -42,7 +39,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
const parentNodes: ParentNodes = {};
nodes.forEach((node) => {
- const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0;
+ const z = isNumeric(node.zIndex) ? node.zIndex : node.selected ? 1000 : 0;
const currInternals = nodeInternals.get(node.id);
const internals: Node = {
@@ -90,74 +87,6 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals;
}
-export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
- if (!node.parentNode) {
- return false;
- }
-
- const parentNode = nodeInternals.get(node.parentNode);
-
- if (!parentNode) {
- return false;
- }
-
- if (parentNode.selected) {
- return true;
- }
-
- return isParentSelected(parentNode, nodeInternals);
-}
-
-type CreatePostionChangeParams = {
- node: Node;
- nodeExtent: CoordinateExtent;
- nodeInternals: NodeInternals;
- diff?: XYPosition;
- dragging?: boolean;
-};
-
-export function createPositionChange({
- node,
- diff,
- dragging,
- nodeExtent,
- nodeInternals,
-}: CreatePostionChangeParams): NodePositionChange {
- const change: NodePositionChange = {
- id: node.id,
- type: 'position',
- dragging: !!dragging,
- };
-
- if (diff) {
- const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y };
- let currentExtent = node.extent || nodeExtent;
-
- if (node.extent === 'parent') {
- if (node.parentNode && node.width && node.height) {
- const parent = nodeInternals.get(node.parentNode);
- currentExtent =
- parent?.width && parent?.height
- ? [
- [0, 0],
- [parent.width - node.width, parent.height - node.height],
- ]
- : currentExtent;
- } else {
- // @ts-ignore
- if (process.env.NODE_ENV === 'development') {
- console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
- }
- currentExtent = nodeExtent;
- }
- }
-
- change.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
- }
-
- return change;
-}
-
type InternalFitViewOptions = {
initial?: boolean;
} & FitViewOptions;
@@ -223,3 +152,30 @@ export function handleControlledEdgeSelectionChange(edgeChanges: EdgeSelectionCh
return e;
});
}
+
+type UpdateNodesAndEdgesParams = {
+ changedNodes: NodeSelectionChange[] | null;
+ changedEdges: EdgeSelectionChange[] | null;
+ get: GetState;
+ set: SetState;
+};
+
+export function updateNodesAndEdgesSelections({ changedNodes, changedEdges, get, set }: UpdateNodesAndEdgesParams) {
+ const { nodeInternals, edges, onNodesChange, onEdgesChange, hasDefaultNodes, hasDefaultEdges } = get();
+
+ if (changedNodes?.length) {
+ if (hasDefaultNodes) {
+ set({ nodeInternals: handleControlledNodeSelectionChange(changedNodes, nodeInternals) });
+ }
+
+ onNodesChange?.(changedNodes);
+ }
+
+ if (changedEdges?.length) {
+ if (hasDefaultEdges) {
+ set({ edges: handleControlledEdgeSelectionChange(changedEdges, edges) });
+ }
+
+ onEdgesChange?.(changedEdges);
+ }
+}
diff --git a/src/style.css b/src/style.css
index 0a725c6f..88fab950 100644
--- a/src/style.css
+++ b/src/style.css
@@ -36,6 +36,10 @@
overflow: visible;
}
+.react-flow .react-flow__connectionline {
+ z-index: 1001;
+}
+
.react-flow__edge {
pointer-events: visibleStroke;
diff --git a/src/theme-default.css b/src/theme-default.css
index b5bc257a..3df0b997 100644
--- a/src/theme-default.css
+++ b/src/theme-default.css
@@ -54,8 +54,7 @@
background: #fff;
border-color: #1a192b;
- &.selected,
- &.selected:hover {
+ &.selected {
box-shadow: 0 0 0 0.5px #1a192b;
}
@@ -71,16 +70,15 @@
&:hover {
box-shadow: 0 1px 4px 1px rgba(0, 0, 0, 0.08);
}
+
+ &.selected {
+ box-shadow: 0 0 0 0.5px #1a192b;
+ }
}
.react-flow__node-group {
background: rgba(240, 240, 240, 0.25);
border-color: #1a192b;
-
- &.selected,
- &.selected:hover {
- box-shadow: 0 0 0 0.5px #1a192b;
- }
}
.react-flow__nodesselection-rect,
diff --git a/src/types/changes.ts b/src/types/changes.ts
index d896d03e..f833fbf0 100644
--- a/src/types/changes.ts
+++ b/src/types/changes.ts
@@ -13,7 +13,6 @@ export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
- dragging?: boolean;
};
export type NodeSelectionChange = {
diff --git a/src/types/component-props.ts b/src/types/component-props.ts
index e55093a2..309ba580 100644
--- a/src/types/component-props.ts
+++ b/src/types/component-props.ts
@@ -36,6 +36,8 @@ import {
OnMove,
OnMoveStart,
OnMoveEnd,
+ NodeDragHandler,
+ NodeMouseHandler,
} from '.';
import { HandleType } from './handles';
@@ -47,22 +49,25 @@ export interface ReactFlowProps extends HTMLAttributes {
defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
- onNodeClick?: (event: React.MouseEvent, node: Node) => void;
+ onNodeClick?: NodeMouseHandler;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
- onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
- onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
- onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
- onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
- onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void;
- onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
- onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
- onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
+ onNodeDoubleClick?: NodeMouseHandler;
+ onNodeMouseEnter?: NodeMouseHandler;
+ onNodeMouseMove?: NodeMouseHandler;
+ onNodeMouseLeave?: NodeMouseHandler;
+ onNodeContextMenu?: NodeMouseHandler;
+ onNodeDragStart?: NodeDragHandler;
+ onNodeDrag?: NodeDragHandler;
+ onNodeDragStop?: NodeDragHandler;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
+ onClickConnectStart?: OnConnectStart;
+ onClickConnectStop?: OnConnectStop;
+ onClickConnectEnd?: OnConnectEnd;
onInit?: OnInit;
onMove?: OnMove;
onMoveStart?: OnMoveStart;
@@ -81,6 +86,7 @@ export interface ReactFlowProps extends HTMLAttributes {
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
+ connectionLineContainerStyle?: CSSProperties;
deleteKeyCode?: KeyCode | null;
selectionKeyCode?: KeyCode | null;
multiSelectionKeyCode?: KeyCode | null;
diff --git a/src/types/general.ts b/src/types/general.ts
index a579ce33..1a36991f 100644
--- a/src/types/general.ts
+++ b/src/types/general.ts
@@ -3,7 +3,7 @@ import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes';
-import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate, NodeProps, WrapNodeProps } from './nodes';
+import { Node, NodeInternals, NodeDimensionUpdate, NodeProps, WrapNodeProps, NodeDragItem } from './nodes';
import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
@@ -105,6 +105,11 @@ export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number;
};
+export type UnselectNodesAndEdgesParams = {
+ nodes?: Node[];
+ edges?: Edge[];
+};
+
export interface ViewportHelperFunctions {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;
@@ -125,7 +130,6 @@ export type ReactFlowStore = {
transform: Transform;
nodeInternals: NodeInternals;
edges: Edge[];
- selectedNodesBbox: Rect;
onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean;
@@ -166,6 +170,10 @@ export type ReactFlowStore = {
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
+ onClickConnectStart?: OnConnectStart;
+ onClickConnectStop?: OnConnectStop;
+ onClickConnectEnd?: OnConnectEnd;
+
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
@@ -182,9 +190,9 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
- updateNodePosition: (update: NodeDiffUpdate) => void;
+ updateNodePositions: (nodeDragItems: NodeDragItem[]) => void;
resetSelectedElements: () => void;
- unselectNodesAndEdges: () => void;
+ unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void;
diff --git a/src/types/nodes.ts b/src/types/nodes.ts
index b88a82d1..d47507ee 100644
--- a/src/types/nodes.ts
+++ b/src/types/nodes.ts
@@ -1,6 +1,5 @@
import { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
-import { SnapGrid } from './general';
import { XYPosition, Position, CoordinateExtent } from './utils';
import { HandleElement } from './handles';
@@ -16,7 +15,6 @@ export interface Node {
sourcePosition?: Position;
hidden?: boolean;
selected?: boolean;
- dragging?: boolean;
draggable?: boolean;
selectable?: boolean;
connectable?: boolean;
@@ -52,6 +50,7 @@ export interface NodeProps {
}
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
+export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export interface WrapNodeProps {
id: string;
@@ -59,7 +58,6 @@ export interface WrapNodeProps {
data: T;
selected: boolean;
isConnectable: boolean;
- scale: number;
xPos: number;
yPos: number;
width?: number | null;
@@ -68,22 +66,19 @@ export interface WrapNodeProps {
isDraggable: boolean;
selectNodesOnDrag: boolean;
onClick?: NodeMouseHandler;
- onNodeDoubleClick?: NodeMouseHandler;
+ onDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
- onNodeDragStart?: NodeMouseHandler;
- onNodeDrag?: NodeMouseHandler;
- onNodeDragStop?: NodeMouseHandler;
+ onDragStart?: NodeDragHandler;
+ onDrag?: NodeDragHandler;
+ onDragStop?: NodeDragHandler;
style?: CSSProperties;
className?: string;
sourcePosition: Position;
targetPosition: Position;
hidden?: boolean;
- snapToGrid?: boolean;
- snapGrid?: SnapGrid;
- dragging: boolean;
resizeObserver: ResizeObserver | null;
dragHandle?: string;
zIndex: number;
@@ -97,12 +92,6 @@ export type NodeHandleBounds = {
target: HandleElement[] | null;
};
-export type NodeDiffUpdate = {
- id?: string;
- diff?: XYPosition;
- dragging?: boolean;
-};
-
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
@@ -115,3 +104,17 @@ export type NodeBounds = XYPosition & {
width: number | null;
height: number | null;
};
+
+export type NodeDragItem = {
+ id: string;
+ // relative node position
+ position: XYPosition;
+ // distance from the mouse cursor to the node when start dragging
+ distance: XYPosition;
+ // delta to previous position
+ delta: XYPosition;
+ width?: number | null;
+ height?: number | null;
+ extent?: 'parent' | CoordinateExtent;
+ parentNode?: string;
+};
diff --git a/src/utils/changes.ts b/src/utils/changes.ts
index e5278a6f..2e6a1cf0 100644
--- a/src/utils/changes.ts
+++ b/src/utils/changes.ts
@@ -45,8 +45,7 @@ function handleParentExpand(res: any[], updateItem: any) {
}
function applyChanges(changes: any[], elements: any[]): any[] {
- // unfortunately we need this hack to handle the setNodes and setEdges function of the
- // useReactFlow hook.
+ // we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
if (changes.some((c) => c.type === 'reset')) {
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
}
@@ -69,10 +68,6 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updateItem.position = currentChange.position;
}
- if (typeof currentChange.dragging !== 'undefined') {
- updateItem.dragging = currentChange.dragging;
- }
-
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index 222a1e59..37765a01 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -176,7 +176,7 @@ export const getNodesInside = (
const visibleNodes: Node[] = [];
nodeInternals.forEach((node) => {
- const { positionAbsolute, width, height, dragging, selectable = true } = node;
+ const { positionAbsolute, width, height, selectable = true } = node;
if (excludeNonSelectableNodes && !selectable) {
return false;
@@ -187,7 +187,7 @@ export const getNodesInside = (
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
const notInitialized =
- typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging;
+ typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
const partiallyVisible = partially && overlappingArea > 0;
const area = (width || 0) * (height || 0);