diff --git a/examples/vite-app/src/App/index.tsx b/examples/vite-app/src/App/index.tsx
index 33347757..02eaac1e 100644
--- a/examples/vite-app/src/App/index.tsx
+++ b/examples/vite-app/src/App/index.tsx
@@ -9,6 +9,7 @@ import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
+import EasyConnect from '../examples/EasyConnect';
import Edges from '../examples/Edges';
import EdgeRenderer from '../examples/EdgeRenderer';
import EdgeTypes from '../examples/EdgeTypes';
@@ -96,6 +97,11 @@ const routes: IRoute[] = [
path: '/dragndrop',
component: DragNDrop,
},
+ {
+ name: 'EasyConnect',
+ path: '/easy-connect',
+ component: EasyConnect,
+ },
{
name: 'Edges',
path: '/edges',
diff --git a/examples/vite-app/src/examples/EasyConnect/CustomConnectionLine.tsx b/examples/vite-app/src/examples/EasyConnect/CustomConnectionLine.tsx
new file mode 100644
index 00000000..ea671570
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/CustomConnectionLine.tsx
@@ -0,0 +1,19 @@
+import { ConnectionLineComponentProps, getStraightPath } from 'reactflow';
+
+function CustomConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle }: ConnectionLineComponentProps) {
+ const [edgePath] = getStraightPath({
+ sourceX: fromX,
+ sourceY: fromY,
+ targetX: toX,
+ targetY: toY,
+ });
+
+ return (
+
+
+
+
+ );
+}
+
+export default CustomConnectionLine;
diff --git a/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx
new file mode 100644
index 00000000..49c1adb9
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/CustomNode.tsx
@@ -0,0 +1,27 @@
+import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow';
+
+const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId;
+
+export default function CustomNode({ id }: NodeProps) {
+ const connectionNodeId = useStore(connectionNodeIdSelector);
+ const isTarget = connectionNodeId && connectionNodeId !== id;
+
+ const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
+ const label = isTarget ? 'Drop here' : 'Drag to connect';
+
+ return (
+
+ );
+}
diff --git a/examples/vite-app/src/examples/EasyConnect/FloatingEdge.tsx b/examples/vite-app/src/examples/EasyConnect/FloatingEdge.tsx
new file mode 100644
index 00000000..4b1038f0
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/FloatingEdge.tsx
@@ -0,0 +1,26 @@
+import { useCallback } from 'react';
+import { useStore, getStraightPath, EdgeProps } from 'reactflow';
+
+import { getEdgeParams } from './utils.js';
+
+function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
+ const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source]));
+ const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target]));
+
+ if (!sourceNode || !targetNode) {
+ return null;
+ }
+
+ const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
+
+ const [edgePath] = getStraightPath({
+ sourceX: sx,
+ sourceY: sy,
+ targetX: tx,
+ targetY: ty,
+ });
+
+ return ;
+}
+
+export default FloatingEdge;
diff --git a/examples/vite-app/src/examples/EasyConnect/index.tsx b/examples/vite-app/src/examples/EasyConnect/index.tsx
new file mode 100644
index 00000000..1e26ae3d
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/index.tsx
@@ -0,0 +1,85 @@
+import { useCallback } from 'react';
+import ReactFlow, { Node, Edge, addEdge, useNodesState, useEdgesState, MarkerType, OnConnect } from 'reactflow';
+
+import CustomNode from './CustomNode';
+import FloatingEdge from './FloatingEdge';
+import CustomConnectionLine from './CustomConnectionLine';
+
+import 'reactflow/dist/style.css';
+import './style.css';
+
+const initialNodes: Node[] = [
+ {
+ id: '1',
+ type: 'custom',
+ position: { x: 0, y: 0 },
+ data: {},
+ },
+ {
+ id: '2',
+ type: 'custom',
+ position: { x: 250, y: 320 },
+ data: {},
+ },
+ {
+ id: '3',
+ type: 'custom',
+ position: { x: 40, y: 300 },
+ data: {},
+ },
+ {
+ id: '4',
+ type: 'custom',
+ position: { x: 300, y: 0 },
+ data: {},
+ },
+];
+
+const initialEdges: Edge[] = [];
+
+const connectionLineStyle = {
+ strokeWidth: 3,
+ stroke: 'black',
+};
+
+const nodeTypes = {
+ custom: CustomNode,
+};
+
+const edgeTypes = {
+ floating: FloatingEdge,
+};
+
+const defaultEdgeOptions = {
+ style: { strokeWidth: 3, stroke: 'black' },
+ type: 'floating',
+ markerEnd: {
+ type: MarkerType.ArrowClosed,
+ color: 'black',
+ },
+};
+
+const EasyConnectExample = () => {
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
+
+ return (
+
+ );
+};
+
+export default EasyConnectExample;
diff --git a/examples/vite-app/src/examples/EasyConnect/style.css b/examples/vite-app/src/examples/EasyConnect/style.css
new file mode 100644
index 00000000..c2ebc0a2
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/style.css
@@ -0,0 +1,54 @@
+.customNodeBody {
+ width: 150px;
+ height: 80px;
+ border: 3px solid black;
+ position: relative;
+ overflow: hidden;
+ border-radius: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-weight: bold;
+}
+
+.customNode:before {
+ content: '';
+ position: absolute;
+ top: -10px;
+ left: 50%;
+ height: 20px;
+ width: 40px;
+ transform: translate(-50%, 0);
+ background: #d6d5e6;
+ z-index: 1000;
+ line-height: 1;
+ border-radius: 4px;
+ color: #fff;
+ font-size: 9px;
+ border: 2px solid #222138;
+}
+
+div.sourceHandle {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-radius: 0;
+ transform: none;
+ border: none;
+ opacity: 0;
+}
+
+div.targetHandle {
+ width: 100%;
+ height: 100%;
+ background: blue;
+ position: absolute;
+ top: 0;
+ left: 0;
+ border-radius: 0;
+ transform: none;
+ border: none;
+ opacity: 0;
+}
diff --git a/examples/vite-app/src/examples/EasyConnect/utils.tsx b/examples/vite-app/src/examples/EasyConnect/utils.tsx
new file mode 100644
index 00000000..02bf2eaf
--- /dev/null
+++ b/examples/vite-app/src/examples/EasyConnect/utils.tsx
@@ -0,0 +1,102 @@
+import { Node, Position, MarkerType, XYPosition } from 'reactflow';
+
+// this helper function returns the intersection point
+// of the line between the center of the intersectionNode and the target node
+function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
+ // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
+ const {
+ width: intersectionNodeWidth,
+ height: intersectionNodeHeight,
+ positionAbsolute: intersectionNodePosition,
+ } = intersectionNode;
+ const targetPosition = targetNode.positionAbsolute!;
+
+ const w = intersectionNodeWidth! / 2;
+ const h = intersectionNodeHeight! / 2;
+
+ const x2 = intersectionNodePosition!.x + w;
+ const y2 = intersectionNodePosition!.y + h;
+ const x1 = targetPosition.x + w;
+ const y1 = targetPosition.y + h;
+
+ const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
+ const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
+ const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
+ const xx3 = a * xx1;
+ const yy3 = a * yy1;
+ const x = w * (xx3 + yy3) + x2;
+ const y = h * (-xx3 + yy3) + y2;
+
+ return { x, y };
+}
+
+// returns the position (top,right,bottom or right) passed node compared to the intersection point
+function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
+ const n = { ...node.positionAbsolute, ...node };
+ const nx = Math.round(n.x!);
+ const ny = Math.round(n.y!);
+ const px = Math.round(intersectionPoint.x);
+ const py = Math.round(intersectionPoint.y);
+
+ if (px <= nx + 1) {
+ return Position.Left;
+ }
+ if (px >= nx + n.width! - 1) {
+ return Position.Right;
+ }
+ if (py <= ny + 1) {
+ return Position.Top;
+ }
+ if (py >= n.y! + n.height! - 1) {
+ return Position.Bottom;
+ }
+
+ return Position.Top;
+}
+
+// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
+export function getEdgeParams(source: Node, target: Node) {
+ const sourceIntersectionPoint = getNodeIntersection(source, target);
+ const targetIntersectionPoint = getNodeIntersection(target, source);
+
+ const sourcePos = getEdgePosition(source, sourceIntersectionPoint);
+ const targetPos = getEdgePosition(target, targetIntersectionPoint);
+
+ return {
+ sx: sourceIntersectionPoint.x,
+ sy: sourceIntersectionPoint.y,
+ tx: targetIntersectionPoint.x,
+ ty: targetIntersectionPoint.y,
+ sourcePos,
+ targetPos,
+ };
+}
+
+export function createNodesAndEdges() {
+ const nodes = [];
+ const edges = [];
+ const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
+
+ nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
+
+ for (let i = 0; i < 8; i++) {
+ const degrees = i * (360 / 8);
+ const radians = degrees * (Math.PI / 180);
+ const x = 250 * Math.cos(radians) + center.x;
+ const y = 250 * Math.sin(radians) + center.y;
+
+ nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
+
+ edges.push({
+ id: `edge-${i}`,
+ target: 'target',
+ source: `${i}`,
+ type: 'floating',
+ markerEnd: {
+ type: MarkerType.Arrow,
+ },
+ });
+ }
+
+ return { nodes, edges };
+}
diff --git a/examples/vite-app/src/examples/Validation/index.tsx b/examples/vite-app/src/examples/Validation/index.tsx
index b11c1f92..1488b48c 100644
--- a/examples/vite-app/src/examples/Validation/index.tsx
+++ b/examples/vite-app/src/examples/Validation/index.tsx
@@ -1,16 +1,17 @@
-import React, { FC, useCallback, useState } from 'react';
+import { FC, useCallback, useState } from 'react';
import ReactFlow, {
addEdge,
Handle,
Connection,
Position,
Node,
- Edge,
NodeProps,
NodeTypes,
useNodesState,
useEdgesState,
- OnConnectStartParams,
+ OnConnectStart,
+ OnConnectEnd,
+ OnConnect,
} from 'reactflow';
import styles from './validation.module.css';
@@ -49,24 +50,24 @@ const ValidationFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
- const onConnectStart = useCallback(
- (event: React.MouseEvent, params: OnConnectStartParams) => {
+ const onConnectStart: OnConnectStart = useCallback(
+ (event, params) => {
console.log('on connect start', params, event, value);
setValue(1);
},
[value]
);
- const onConnect = useCallback(
- (params: Connection | Edge) => {
+ const onConnect: OnConnect = useCallback(
+ (params) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
- const onConnectEnd = useCallback(
- (event: MouseEvent) => {
+ const onConnectEnd: OnConnectEnd = useCallback(
+ (event) => {
console.log('on connect end', event, value);
setValue(0);
},
diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts
index 180b6027..03bf698c 100644
--- a/packages/core/src/components/Handle/handler.ts
+++ b/packages/core/src/components/Handle/handler.ts
@@ -125,6 +125,7 @@ export function handlePointerDown({
}
const { connection, handleDomNode, isValid } = isValidHandle(
+ event,
prevClosestHandle,
connectionMode,
nodeId,
@@ -148,6 +149,7 @@ export function handlePointerDown({
if (prevClosestHandle) {
const { connection, isValid } = isValidHandle(
+ event,
prevClosestHandle,
connectionMode,
nodeId,
diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx
index 59d2bd7f..dab16653 100644
--- a/packages/core/src/components/Handle/index.tsx
+++ b/packages/core/src/components/Handle/index.tsx
@@ -102,6 +102,7 @@ const Handle = forwardRef(
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = isValidHandle(
+ event,
{
nodeId,
id: handleId,
diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts
index 439ccc7a..0b391e37 100644
--- a/packages/core/src/components/Handle/utils.ts
+++ b/packages/core/src/components/Handle/utils.ts
@@ -1,5 +1,7 @@
+import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
+
import { ConnectionMode } from '../../types';
-import { internalsSymbol } from '../../utils';
+import { getEventPosition, internalsSymbol } from '../../utils';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types';
export type ConnectionHandle = {
@@ -61,6 +63,7 @@ type Result = {
// checks if and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle(
+ event: MouseEvent | TouchEvent | ReactMouseEvent | ReactTouchEvent,
handle: Pick,
connectionMode: ConnectionMode,
fromNodeId: string,
@@ -73,23 +76,26 @@ export function isValidHandle(
const handleDomNode = doc.querySelector(
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
+ const { x, y } = getEventPosition(event);
+ const handleBelow = doc.elementFromPoint(x, y);
+ const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode;
+
const result: Result = {
- handleDomNode,
+ handleDomNode: handleToCheck,
isValid: false,
connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
};
- if (handleDomNode) {
- const handleIsTarget = handle.type === 'target';
- const handleIsSource = handle.type === 'source';
- const handleNodeId = handleDomNode.getAttribute('data-nodeid');
- const handleId = handleDomNode.getAttribute('data-handleid');
+ if (handleToCheck) {
+ const handleType = getHandleType(undefined, handleToCheck);
+ const handleNodeId = handleToCheck.getAttribute('data-nodeid');
+ const handleId = handleToCheck.getAttribute('data-handleid');
const connection: Connection = {
source: isTarget ? handle.nodeId : fromNodeId,
- sourceHandle: isTarget ? handle.id : fromHandleId,
+ sourceHandle: isTarget ? handleId : fromHandleId,
target: isTarget ? fromNodeId : handle.nodeId,
- targetHandle: isTarget ? fromHandleId : handle.id,
+ targetHandle: isTarget ? fromHandleId : handleId,
};
result.connection = connection;
@@ -97,7 +103,7 @@ export function isValidHandle(
// in strict mode we don't allow target to target or source to source connections
const isValid =
connectionMode === ConnectionMode.Strict
- ? (isTarget && handleIsSource) || (!isTarget && handleIsTarget)
+ ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) {