import React, { memo, MouseEvent as ReactMouseEvent, CSSProperties } from 'react'; import cc from 'classcat'; import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc, OnConnectEndFunc, Connection, SetConnectionId, } from '../../types'; type ValidConnectionFunc = (connection: Connection) => boolean; export type SetSourceIdFunc = (params: SetConnectionId) => void; interface BaseHandleProps { type: HandleType; nodeId: ElementId; onConnect: OnConnectFunc; onConnectStart?: OnConnectStartFunc; onConnectStop?: OnConnectStopFunc; onConnectEnd?: OnConnectEndFunc; position: Position; setConnectionNodeId: SetSourceIdFunc; setPosition: (pos: XYPosition) => void; isValidConnection: ValidConnectionFunc; id?: ElementId | null; className?: string; style?: CSSProperties; } type Result = { elementBelow: Element | null; isValid: boolean; connection: Connection; isHoveringHandle: boolean; }; export function onMouseDown( event: ReactMouseEvent, handleId: ElementId | null, nodeId: ElementId, setConnectionNodeId: SetSourceIdFunc, setPosition: (pos: XYPosition) => void, onConnect: OnConnectFunc, isTarget: boolean, isValidConnection: ValidConnectionFunc, onConnectStart?: OnConnectStartFunc, onConnectStop?: OnConnectStopFunc, onConnectEnd?: OnConnectEndFunc ): void { const reactFlowNode = (event.target as Element).closest('.react-flow'); if (!reactFlowNode) { return; } const handleType = isTarget ? 'target' : 'source'; const containerBounds = reactFlowNode.getBoundingClientRect(); let recentHoveredHandle: Element; setPosition({ x: event.clientX - containerBounds.left, y: event.clientY - containerBounds.top, }); setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType }); if (onConnectStart) { onConnectStart(event, { nodeId, handleId, handleType }); } function resetRecentHandle(): void { if (!recentHoveredHandle) { return; } recentHoveredHandle.classList.remove('react-flow__handle-valid'); recentHoveredHandle.classList.remove('react-flow__handle-connecting'); } // checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 } function checkElementBelowIsValid(event: MouseEvent) { const elementBelow = document.elementFromPoint(event.clientX, event.clientY); const result: Result = { elementBelow, isValid: false, connection: { source: null, target: null, sourceHandle: null, targetHandle: null }, isHoveringHandle: false, }; if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) { result.isHoveringHandle = true; if ( (isTarget && elementBelow.classList.contains('source')) || (!isTarget && elementBelow.classList.contains('target')) ) { let connection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null }; if (isTarget) { const sourceId = elementBelow.getAttribute('data-nodeid'); const sourcehandleId = elementBelow.getAttribute('data-handleid'); connection = { source: sourceId, sourceHandle: sourcehandleId, target: nodeId, targetHandle: handleId }; } else { const targetId = elementBelow.getAttribute('data-nodeid'); const targetHandleId = elementBelow.getAttribute('data-handleid'); connection = { source: nodeId, sourceHandle: handleId, target: targetId, targetHandle: targetHandleId }; } const isValid = isValidConnection(connection); result.connection = connection; result.isValid = isValid; } } return result; } function onMouseMove(event: MouseEvent) { setPosition({ x: event.clientX - containerBounds.left, y: event.clientY - containerBounds.top, }); const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(event); if (!isHoveringHandle) { return resetRecentHandle(); } const isOwnHandle = connection.source === connection.target; if (!isOwnHandle && elementBelow) { recentHoveredHandle = elementBelow; elementBelow.classList.add('react-flow__handle-connecting'); elementBelow.classList.toggle('react-flow__handle-valid', isValid); } } function onMouseUp(event: MouseEvent) { const { connection, isValid } = checkElementBelowIsValid(event); if (onConnectStop) { onConnectStop(event); } if (isValid && onConnect) { onConnect(connection); } if (onConnectEnd) { onConnectEnd(event); } resetRecentHandle(); setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null }); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); } const BaseHandle = ({ type, nodeId, onConnect, onConnectStart, onConnectStop, onConnectEnd, position, setConnectionNodeId, setPosition, className, id = null, isValidConnection, ...rest }: BaseHandleProps) => { const isTarget = type === 'target'; const handleClasses = cc([ 'react-flow__handle', `react-flow__handle-${position}`, 'nodrag', className, { source: !isTarget, target: isTarget, }, ]); const handleId = id || null; return (