import { ConnectionMode } from '../types' import type { Actions, Connection, ConnectionStatus, Dimensions, GraphEdge, GraphNode, HandleType, NodeHandleBounds, ValidConnectionFunc, ValidHandleResult, XYPosition, } from '../types' import { getEventPosition } from '.' export interface ConnectionHandle extends XYPosition, Dimensions { id: string | null type: HandleType | null nodeId: string } function defaultValidHandleResult(): ValidHandleResult { return { handleDomNode: null, isValid: false, connection: { source: '', target: '', sourceHandle: null, targetHandle: null }, endHandle: null, } } export function resetRecentHandle(handleDomNode: Element): void { handleDomNode?.classList.remove('valid', 'connecting', 'vue-flow__handle-valid', 'vue-flow__handle-connecting') } // this functions collects all handles and adds an absolute position // so that we can later find the closest handle to the mouse position export function getHandles( node: GraphNode, handleBounds: NodeHandleBounds, type: HandleType, currentHandle: string, ): ConnectionHandle[] { return (handleBounds[type] || []).reduce((res, h) => { if (`${node.id}-${h.id}-${type}` !== currentHandle) { res.push({ id: h.id || null, type, nodeId: node.id, x: (node.computedPosition?.x ?? 0) + h.x + h.width / 2, y: (node.computedPosition?.y ?? 0) + h.y + h.height / 2, width: h.width, height: h.height, }) } return res }, []) } export function getClosestHandle( event: MouseEvent | TouchEvent, doc: Document | ShadowRoot, pos: XYPosition, connectionRadius: number, handles: ConnectionHandle[], validator: (handle: Pick) => ValidHandleResult, ) { // we always want to prioritize the handle below the mouse cursor over the closest distance handle, // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor const { x, y } = getEventPosition(event) const domNodes = doc.elementsFromPoint(x, y) const handleBelow = domNodes.find((el) => el.classList.contains('vue-flow__handle')) if (handleBelow) { const handleNodeId = handleBelow.getAttribute('data-nodeid') if (handleNodeId) { const handleType = getHandleType(undefined, handleBelow) const handleId = handleBelow.getAttribute('data-handleid') const validHandleResult = validator({ nodeId: handleNodeId, id: handleId, type: handleType }) if (validHandleResult) { const handle = handles.find((h) => h.nodeId === handleNodeId && h.type === handleType && h.id === handleId) return { handle: { id: handleId, type: handleType, nodeId: handleNodeId, x: handle?.x || pos.x, y: handle?.y || pos.y, }, validHandleResult, } } } } // if we couldn't find a handle below the mouse cursor we look for the closest distance based on the connectionRadius let closestHandles: { handle: ConnectionHandle; validHandleResult: ValidHandleResult }[] = [] let minDistance = Number.POSITIVE_INFINITY for (const handle of handles) { const distance = Math.sqrt((handle.x - pos.x) ** 2 + (handle.y - pos.y) ** 2) if (distance <= connectionRadius) { const validHandleResult = validator(handle) if (distance <= minDistance) { if (distance < minDistance) { closestHandles = [{ handle, validHandleResult }] } else if (distance === minDistance) { // when multiple handles are on the same distance we collect all of them closestHandles.push({ handle, validHandleResult, }) } minDistance = distance } } } if (!closestHandles.length) { return { handle: null, validHandleResult: defaultValidHandleResult() } } if (closestHandles.length === 1) { return closestHandles[0] } const hasValidHandle = closestHandles.some(({ validHandleResult }) => validHandleResult.isValid) const hasTargetHandle = closestHandles.some(({ handle }) => handle.type === 'target') // if multiple handles are layout on top of each other we prefer the one with type = target and the one that is valid return ( closestHandles.find(({ handle, validHandleResult }) => hasTargetHandle ? handle.type === 'target' : hasValidHandle ? validHandleResult.isValid : true, ) || closestHandles[0] ) } // checks if and returns connection in fom of an object { source: 123, target: 312 } export function isValidHandle( event: MouseEvent | TouchEvent, handle: Pick | null, connectionMode: ConnectionMode, fromNodeId: string, fromHandleId: string | null, fromType: HandleType, isValidConnection: ValidConnectionFunc, doc: Document | ShadowRoot, edges: GraphEdge[], nodes: GraphNode[], findNode: Actions['findNode'], ) { const isTarget = fromType === 'target' const handleDomNode = doc.querySelector(`.vue-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`) const { x, y } = getEventPosition(event) const elementBelow = doc.elementFromPoint(x, y) // we always want to prioritize the handle below the mouse cursor over the closest distance handle, // because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor const handleToCheck = elementBelow?.classList.contains('vue-flow__handle') ? elementBelow : handleDomNode const result = defaultValidHandleResult() if (handleToCheck) { result.handleDomNode = handleToCheck const handleType = getHandleType(undefined, handleToCheck) const handleNodeId = handleToCheck.getAttribute('data-nodeid')! const handleId = handleToCheck.getAttribute('data-handleid') const connectable = handleToCheck.classList.contains('connectable') const connectableEnd = handleToCheck.classList.contains('connectableend') const connection: Connection = { source: isTarget ? handleNodeId : fromNodeId, sourceHandle: isTarget ? handleId : fromHandleId, target: isTarget ? fromNodeId : handleNodeId, targetHandle: isTarget ? fromHandleId : handleId, } result.connection = connection const isConnectable = connectable && connectableEnd // in strict mode we don't allow target to target or source to source connections const isValid = isConnectable && (connectionMode === ConnectionMode.Strict ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') : handleNodeId !== fromNodeId || handleId !== fromHandleId) result.endHandle = { nodeId: handleNodeId, handleId, type: handleType as HandleType, } if (isValid) { result.isValid = isValidConnection(connection, { edges, nodes, sourceNode: findNode(connection.source)!, targetNode: findNode(connection.target)!, }) } } return result } interface GetHandleLookupParams { nodes: GraphNode[] nodeId: string handleId: string | null handleType: string } export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) { return nodes.reduce((res, node) => { const { handleBounds } = node let sourceHandles: ConnectionHandle[] = [] let targetHandles: ConnectionHandle[] = [] if (handleBounds) { sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`) targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`) } res.push(...sourceHandles, ...targetHandles) return res }, []) } export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDomNode: Element | null): HandleType | null { if (edgeUpdaterType) { return edgeUpdaterType } else if (handleDomNode?.classList.contains('target')) { return 'target' } else if (handleDomNode?.classList.contains('source')) { return 'source' } return null } export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { let connectionStatus: ConnectionStatus | null = null if (isHandleValid) { connectionStatus = 'valid' } else if (isInsideConnectionRadius && !isHandleValid) { connectionStatus = 'invalid' } return connectionStatus }