diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 33b1ca50..56acd9ac 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -7,7 +7,6 @@ import { ConnectionMode, getBezierPath, getSmoothStepPath, - type ConnectionStatus, type HandleType, } from '@xyflow/system'; @@ -15,13 +14,21 @@ import { useStore } from '../../hooks/useStore'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types'; +function getConnectionStatus(isValid: boolean | null) { + if (isValid === null) { + return null; + } + + return isValid ? 'valid' : 'invalid'; +} + type ConnectionLineProps = { nodeId: string; handleType: HandleType; type: ConnectionLineType; style?: CSSProperties; CustomComponent?: ConnectionLineComponent; - connectionStatus: ConnectionStatus | null; + isValid: boolean | null; }; const oppositePosition = { @@ -37,18 +44,17 @@ const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, - connectionStatus, + isValid, }: ConnectionLineProps) => { - const { fromNode, handleId, toX, toY, connectionMode, endPosition, isValid } = useStore( + const { fromNode, startHandle, endHandle, toX, toY, connectionMode } = useStore( useCallback( (s: ReactFlowStore) => ({ fromNode: s.nodeLookup.get(nodeId), - handleId: s.connectionStartHandle?.handleId, - toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], - toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], + startHandle: s.connection.fromHandle, + endHandle: s.connection.toHandle, + toX: (s.connection.position.x - s.transform[0]) / s.transform[2], + toY: (s.connection.position.y - s.transform[1]) / s.transform[2], connectionMode: s.connectionMode, - endPosition: s.connectionEndHandle?.position, - isValid: s.connectionStatus === 'valid', }), [nodeId] ), @@ -66,13 +72,15 @@ const ConnectionLine = ({ return null; } + const handleId = startHandle?.handleId; const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0]; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0; const fromX = fromNode.internals.positionAbsolute.x + fromHandleX; const fromY = fromNode.internals.positionAbsolute.y + fromHandleY; const fromPosition = fromHandle?.position; - const toPosition = isValid && endPosition ? endPosition : fromPosition ? oppositePosition[fromPosition] : null; + const toPosition = + isValid && endHandle?.position ? endHandle.position : fromPosition ? oppositePosition[fromPosition] : null; if (!fromPosition || !toPosition) { return null; @@ -83,7 +91,7 @@ const ConnectionLine = ({ ); } @@ -136,19 +144,19 @@ type ConnectionLineWrapperProps = { }; const selector = (s: ReactFlowState) => ({ - nodeId: s.connectionStartHandle?.nodeId, - handleType: s.connectionStartHandle?.type, + nodeId: s.connection.fromHandle?.nodeId, + handleType: s.connection.fromHandle?.type, nodesConnectable: s.nodesConnectable, - connectionStatus: s.connectionStatus, + isValid: s.connection.isValid, width: s.width, height: s.height, }); export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { - const { nodeId, handleType, nodesConnectable, width, height, connectionStatus } = useStore(selector, shallow); - const isValid = !!(nodeId && handleType && width && nodesConnectable); + const { nodeId, handleType, nodesConnectable, width, height, isValid } = useStore(selector, shallow); + const renderConnectionLine = !!(nodeId && handleType && width && nodesConnectable); - if (!isValid) { + if (!renderConnectionLine) { return null; } @@ -159,14 +167,14 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component } height={height} className="react-flow__connectionline react-flow__container" > - + diff --git a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx index d1cfd138..236592ee 100644 --- a/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx +++ b/packages/react/src/components/EdgeWrapper/EdgeUpdateAnchors.tsx @@ -96,7 +96,7 @@ export function EdgeUpdateAnchors({ onReconnectEnd: _onReconnectEnd, updateConnection, getTransform: () => store.getState().transform, - getConnectionStartHandle: () => store.getState().connectionStartHandle, + getFromHandle: () => store.getState().connection.fromHandle, }); }; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 760733a0..4fe59a96 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -39,28 +39,23 @@ const selector = (s: ReactFlowState) => ({ const connectingSelector = (nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => { - const { - connectionStartHandle: startHandle, - connectionEndHandle: endHandle, - connectionClickStartHandle: clickHandle, - connectionMode, - connectionStatus, - } = state; + const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state; - const connectingTo = endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type; + const { fromHandle, toHandle, isValid } = connection; + + const connectingTo = toHandle?.nodeId === nodeId && toHandle?.handleId === handleId && toHandle?.type === type; return { - connectingFrom: - startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type, + connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.handleId === handleId && fromHandle?.type === type, connectingTo, clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type, isPossibleEndHandle: connectionMode === ConnectionMode.Strict - ? startHandle?.type !== type - : nodeId !== startHandle?.nodeId || handleId !== startHandle?.handleId, - connectionInProcess: !!startHandle, - valid: connectingTo && connectionStatus === 'valid', + ? fromHandle?.type !== type + : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.handleId, + connectionInProcess: !!fromHandle, + valid: connectingTo && isValid, }; }; @@ -144,7 +139,7 @@ function HandleComponent( onConnect: onConnectExtended, isValidConnection: isValidConnection || currentStore.isValidConnection, getTransform: () => store.getState().transform, - getConnectionStartHandle: () => store.getState().connectionStartHandle, + getFromHandle: () => store.getState().connection.fromHandle, }); } diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts index 9b458e87..d8f1bae4 100644 --- a/packages/react/src/hooks/useConnection.ts +++ b/packages/react/src/hooks/useConnection.ts @@ -2,33 +2,19 @@ import { shallow } from 'zustand/shallow'; import { useStore } from './useStore'; import type { ReactFlowStore } from '../types/store'; +import { ConnectionState } from '@xyflow/system'; const selector = (s: ReactFlowStore) => ({ - startHandle: s.connectionStartHandle, - endHandle: s.connectionEndHandle, - status: s.connectionStatus, - position: s.connectionStartHandle ? s.connectionPosition : null, + ...s.connection, + inProgress: s.connection.fromHandle !== null, }); -type UseConnectionResult = { - /** The start handle where the user interaction started or null */ - startHandle: ReactFlowStore['connectionStartHandle']; - /** The target handle that's inside the connection radius or null */ - endHandle: ReactFlowStore['connectionEndHandle']; - /** The current connection status 'valid', 'invalid' or null*/ - status: ReactFlowStore['connectionStatus']; - /** The current connection position or null */ - position: ReactFlowStore['connectionPosition'] | null; -}; - /** * Hook for accessing the ongoing connection. * * @public * @returns ongoing connection */ -export function useConnection(): UseConnectionResult { - const ongoingConnection = useStore(selector, shallow); - - return ongoingConnection; +export function useConnection(): ConnectionState & { inProgress: boolean } { + return useStore(selector, shallow); } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index e00cd0d7..e15d9ca2 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -49,7 +49,6 @@ export { type OnMoveStart, type OnMoveEnd, type Connection, - type ConnectionStatus, ConnectionMode, type OnConnectStartParams, type OnConnectStart, diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index ccc522d4..5ccd7070 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -309,18 +309,25 @@ const createStore = ({ options ); }, - cancelConnection: () => + cancelConnection: () => { + const { connection } = get(); set({ - connectionStatus: null, - connectionStartHandle: null, - connectionEndHandle: null, - }), + connection: { + position: connection.position, + fromHandle: null, + toHandle: null, + isValid: null, + }, + }); + }, updateConnection: (params) => { - const { connectionPosition } = get(); + const { connection } = get(); const currentConnection = { - ...params, - connectionPosition: params.connectionPosition ?? connectionPosition, + connection: { + ...params, + position: params.position ?? connection.position, + }, }; set(currentConnection); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 1b98ee68..495afd3d 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -79,8 +79,6 @@ const getInitialState = ({ nodesSelectionActive: false, userSelectionActive: false, userSelectionRect: null, - connectionPosition: { x: 0, y: 0 }, - connectionStatus: null, connectionMode: ConnectionMode.Strict, domNode: null, paneDragging: false, @@ -106,8 +104,12 @@ const getInitialState = ({ multiSelectionActive: false, - connectionStartHandle: null, - connectionEndHandle: null, + connection: { + fromHandle: null, + toHandle: null, + position: { x: 0, y: 0 }, + isValid: null, + }, connectionClickStartHandle: null, connectOnClick: true, diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index 23bec358..63a61adf 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -11,7 +11,6 @@ import type { Connection, ConnectionLineType, HandleElement, - ConnectionStatus, EdgePosition, StepPathOptions, OnError, @@ -202,7 +201,7 @@ export type ConnectionLineComponentProps = { toY: number; fromPosition: Position; toPosition: Position; - connectionStatus: ConnectionStatus | null; + connectionStatus: 'valid' | 'invalid' | null; }; export type ConnectionLineComponent = ComponentType; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 89e33748..ab99ad45 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -1,6 +1,6 @@ import { ConnectionMode, - type ConnectionStatus, + type ConnectionState, type CoordinateExtent, type InternalNodeUpdate, type UpdateNodePositions, @@ -12,7 +12,6 @@ import { type SnapGrid, type ConnectingHandle, type Transform, - type XYPosition, type PanZoomInstance, type PanBy, type OnConnectStart, @@ -25,9 +24,9 @@ import { type EdgeLookup, type ConnectionLookup, type NodeLookup, - NodeChange, - EdgeChange, - ParentLookup, + type NodeChange, + type EdgeChange, + type ParentLookup, } from '@xyflow/system'; import type { @@ -79,9 +78,9 @@ export type ReactFlowStore; onNodeDrag?: OnNodeDrag; onNodeDragStop?: OnNodeDrag; diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 472ca2c2..7ebfeb78 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -103,7 +103,7 @@ $onConnectEndAction?.(event); }, getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], - getConnectionStartHandle: () => $connection.startHandle + getFromHandle: () => $connection.startHandle }); } } diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 9bc4ac94..b13a7d90 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -70,7 +70,6 @@ export { type OnMoveStart, type OnMoveEnd, type Connection, - type ConnectionStatus, ConnectionMode, type OnConnectStartParams, type OnConnectStart, diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 606475dc..21d5cc0e 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -15,7 +15,8 @@ import { type CoordinateExtent, type UpdateConnection, errorMessages, - type NodeOrigin + type NodeOrigin, + type ConnectionState } from '@xyflow/system'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; @@ -350,12 +351,17 @@ export function createStore({ // by creating an internal, unexposed store and using a derived store // we prevent using slow get() calls const currentConnection = writable(initConnectionUpdateData); - const updateConnection: UpdateConnection = (newConnection: ConnectionData) => { - currentConnection.set(newConnection); + const updateConnection: UpdateConnection = (newConnection: ConnectionState) => { + currentConnection.set({ + connectionStartHandle: newConnection.fromHandle, + connectionEndHandle: newConnection.toHandle, + connectionPosition: newConnection.position, + connectionStatus: newConnection.isValid ? 'valid' : 'invalid' + }); }; function cancelConnection() { - updateConnection(initConnectionUpdateData); + currentConnection.set(initConnectionUpdateData); } function reset() { diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 5d2bb390..fed3a318 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -37,8 +37,6 @@ export type HandleConnection = Connection & { edgeId: string; }; -export type ConnectionStatus = 'valid' | 'invalid'; - export enum ConnectionMode { Strict = 'strict', Loose = 'loose', @@ -134,12 +132,23 @@ export type OnError = (id: string, message: string) => void; export type UpdateNodePositions = (dragItems: Map, dragging?: boolean) => void; export type PanBy = (delta: XYPosition) => boolean; -export type UpdateConnection = (params: { - connectionPosition: XYPosition | null; - connectionStatus: ConnectionStatus | null; - connectionStartHandle: ConnectingHandle | null; - connectionEndHandle: ConnectingHandle | null; -}) => void; +export type NoConnectionInProgress = { + position: XYPosition; + isValid: null; + fromHandle: null; + toHandle: null; +}; + +export type ConnectionInProgress = { + position: XYPosition; + isValid: boolean | null; + fromHandle: ConnectingHandle; + toHandle: ConnectingHandle | null; +}; + +export type ConnectionState = ConnectionInProgress | NoConnectionInProgress; + +export type UpdateConnection = (params: ConnectionState) => void; export type ColorModeClass = 'light' | 'dark'; export type ColorMode = ColorModeClass | 'system'; diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index c2dca7fe..bd4a866e 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -16,7 +16,7 @@ import { Position, } from '../types'; -import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; +import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils'; export type OnPointerDownParams = { autoPanOnConnect: boolean; @@ -39,7 +39,7 @@ export type OnPointerDownParams = { isValidConnection?: IsValidConnection; onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void; getTransform: () => Transform; - getConnectionStartHandle: () => ConnectingHandle | null; + getFromHandle: () => ConnectingHandle | null; }; export type IsValidParams = { @@ -63,12 +63,12 @@ type Result = { handleDomNode: Element | null; isValid: boolean; connection: Connection | null; - endHandle: ConnectingHandle | null; + toHandle: ConnectingHandle | null; }; const alwaysValid = () => true; -let connectionStartHandle: ConnectingHandle | null = null; +let fromHandle: ConnectingHandle | null = null; function onPointerDown( event: MouseEvent | TouchEvent, @@ -93,7 +93,7 @@ function onPointerDown( onReconnectEnd, updateConnection, getTransform, - getConnectionStartHandle, + getFromHandle, }: OnPointerDownParams ) { // when xyflow is used inside a shadow root we can't use document @@ -110,7 +110,7 @@ function onPointerDown( return; } - let connectionPosition = getEventPosition(event, containerBounds); + let position = getEventPosition(event, containerBounds); let autoPanStarted = false; let connection: Connection | null = null; let isValid = false; @@ -128,38 +128,39 @@ function onPointerDown( if (!autoPanOnConnect || !containerBounds) { return; } - const [x, y] = calcAutoPan(connectionPosition, containerBounds); + const [x, y] = calcAutoPan(position, containerBounds); panBy({ x, y }); autoPanId = requestAnimationFrame(autoPan); } // Stays the same for all consecutive pointermove events - connectionStartHandle = { + fromHandle = { nodeId, handleId, type: handleType, - position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top, + position: (clickedHandle?.getAttribute('data-handlepos') as Position) ?? Position.Top, }; updateConnection({ - connectionPosition, - connectionStatus: null, - connectionStartHandle, - connectionEndHandle: null, + position, + isValid: null, + fromHandle, + toHandle: null, }); onConnectStart?.(event, { nodeId, handleId, handleType }); function onPointerMove(event: MouseEvent | TouchEvent) { - if (!getConnectionStartHandle()) { + if (!getFromHandle() || !fromHandle) { onPointerUp(event); + return; } const transform = getTransform(); - connectionPosition = getEventPosition(event, containerBounds); + position = getEventPosition(event, containerBounds); closestHandle = getClosestHandle( - pointToRendererPoint(connectionPosition, transform, false, [1, 1]), + pointToRendererPoint(position, transform, false, [1, 1]), connectionRadius, handleLookup ); @@ -186,8 +187,8 @@ function onPointerDown( isValid = result.isValid; updateConnection({ - connectionStartHandle, - connectionPosition: + fromHandle, + position: closestHandle && isValid ? rendererPointToPoint( { @@ -196,9 +197,9 @@ function onPointerDown( }, transform ) - : connectionPosition, - connectionStatus: getConnectionStatus(!!closestHandle, isValid), - connectionEndHandle: result.endHandle, + : position, + isValid: isConnectionValid(!!closestHandle, isValid), + toHandle: result.toHandle, }); } @@ -221,7 +222,7 @@ function onPointerDown( isValid = false; connection = null; handleDomNode = null; - connectionStartHandle = null; + fromHandle = null; doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener); @@ -267,7 +268,7 @@ function isValidHandle( handleDomNode: handleToCheck, isValid: false, connection: null, - endHandle: null, + toHandle: null, }; if (handleToCheck) { @@ -300,7 +301,7 @@ function isValidHandle( result.isValid = isValid && isValidConnection(connection); - result.endHandle = { + result.toHandle = { nodeId: handleNodeId as string, handleId, type: handleType as HandleType, diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index 31dbfe61..b1b868ad 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -1,6 +1,5 @@ import { getHandlePosition } from '../utils'; import { - ConnectionStatus, type HandleType, type NodeHandleBounds, type XYPosition, @@ -105,14 +104,14 @@ export function getHandleType( return null; } -export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { - let connectionStatus = null; +export function isConnectionValid(isInsideConnectionRadius: boolean, isHandleValid: boolean) { + let isValid: boolean | null = null; if (isHandleValid) { - connectionStatus = 'valid'; + isValid = true; } else if (isInsideConnectionRadius && !isHandleValid) { - connectionStatus = 'invalid'; + isValid = false; } - return connectionStatus as ConnectionStatus; + return isValid; }