import { FC } from 'react';import { getBezierPath, ConnectionLineComponentProps, Node } from 'react-flow-renderer';import { getEdgeParams } from './utils';const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({targetX,targetY,sourcePosition,targetPosition,sourceNode,}) => {if (!sourceNode) {return null;}const targetNode = {id: 'connection-target',__rf: { width: 1, height: 1, position: { x: targetX, y: targetY } },} as Node;const { sx, sy } = getEdgeParams(sourceNode, targetNode);const d = getBezierPath({sourceX: sx,sourceY: sy,sourcePosition,targetPosition,targetX,targetY,});return (<g><path fill="none" stroke="#222" strokeWidth={1.5} className="animated" d={d} /><circle cx={targetX} cy={targetY} fill="#fff" r={3} stroke="#222" strokeWidth={1.5} /></g>);};export default FloatingConnectionLine;
import { FC, useMemo, CSSProperties } from 'react';import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer';import { getEdgeParams } from './utils';const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, markerEndId, style }) => {const nodes = useStoreState((state) => state.nodes);const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]);const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]);if (!sourceNode || !targetNode) {return null;}const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);const d = getBezierPath({sourceX: sx,sourceY: sy,sourcePosition: sourcePos,targetPosition: targetPos,targetX: tx,targetY: ty,});return (<g className="react-flow__connection"><path id={id} className="react-flow__edge-path" d={d} markerEnd={markerEnd} style={style as CSSProperties} /></g>);};export default FloatingEdge;
import React, { useState } from 'react';import ReactFlow, {removeElements,addEdge,Background,OnLoadParams,EdgeTypesType,Elements,Connection,Edge,ArrowHeadType,} from 'react-flow-renderer';import './style.css';import FloatingEdge from './FloatingEdge';import FloatingConnectionLine from './FloatingConnectionLine';import { createElements } from './utils';const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();const initialElements: Elements = createElements();const edgeTypes: EdgeTypesType = {floating: FloatingEdge,};const NodeAsHandleFlow = () => {const [elements, setElements] = useState<Elements>(initialElements);const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));const onConnect = (params: Connection | Edge) =>setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els));return (<div className="floatingedges"><ReactFlowelements={elements}onElementsRemove={onElementsRemove}onConnect={onConnect}onLoad={onLoad}edgeTypes={edgeTypes}connectionLineComponent={FloatingConnectionLine}><Background /></ReactFlow></div>);};export default NodeAsHandleFlow;
import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer';// this helper function returns the intersection point// of the line between the center of the intersectionNode and the target nodefunction getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-aconst {width: intersectionNodeWidth,height: intersectionNodeHeight,position: intersectionNodePosition,} = intersectionNode.__rf;const targetPosition = targetNode.__rf.position;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 pointfunction getEdgePosition(node: Node, intersectionPoint: XYPosition) {const n = { ...node.__rf.position, ...node.__rf };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 edgeexport 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 createElements() {const elements = [];const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };elements.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;elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });elements.push({id: `edge-${i}`,target: 'target',source: `${i}`,type: 'floating',arrowHeadType: ArrowHeadType.Arrow,});}return elements;}