diff --git a/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte b/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte index 0a7f87b5..d315f572 100644 --- a/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte +++ b/examples/svelte/src/routes/examples/custom-connection-line/ConnectionLine.svelte @@ -1,9 +1,24 @@ -{#if $connection.path} - +{#if $connection.inProgress} + {/if} diff --git a/packages/react/src/components/ConnectionLine/index.tsx b/packages/react/src/components/ConnectionLine/index.tsx index 56acd9ac..a5f7e1e2 100644 --- a/packages/react/src/components/ConnectionLine/index.tsx +++ b/packages/react/src/components/ConnectionLine/index.tsx @@ -1,140 +1,18 @@ -import { CSSProperties, useCallback } from 'react'; +import { CSSProperties } from 'react'; import { shallow } from 'zustand/shallow'; import cc from 'classcat'; import { - Position, ConnectionLineType, - ConnectionMode, getBezierPath, getSmoothStepPath, - type HandleType, + getConnectionStatus, + getStraightPath, } from '@xyflow/system'; 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; - isValid: boolean | null; -}; - -const oppositePosition = { - [Position.Left]: Position.Right, - [Position.Right]: Position.Left, - [Position.Top]: Position.Bottom, - [Position.Bottom]: Position.Top, -}; - -const ConnectionLine = ({ - nodeId, - handleType, - style, - type = ConnectionLineType.Bezier, - CustomComponent, - isValid, -}: ConnectionLineProps) => { - const { fromNode, startHandle, endHandle, toX, toY, connectionMode } = useStore( - useCallback( - (s: ReactFlowStore) => ({ - fromNode: s.nodeLookup.get(nodeId), - 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, - }), - [nodeId] - ), - shallow - ); - - const fromHandleBounds = fromNode?.internals.handleBounds; - let handleBounds = fromHandleBounds?.[handleType]; - - if (connectionMode === ConnectionMode.Loose) { - handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source']; - } - - if (!fromNode || !handleBounds) { - 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 && endHandle?.position ? endHandle.position : fromPosition ? oppositePosition[fromPosition] : null; - - if (!fromPosition || !toPosition) { - return null; - } - - if (CustomComponent) { - return ( - - ); - } - - let dAttr = ''; - - const pathParams = { - sourceX: fromX, - sourceY: fromY, - sourcePosition: fromPosition, - targetX: toX, - targetY: toY, - targetPosition: toPosition, - }; - - if (type === ConnectionLineType.Bezier) { - // we assume the destination position is opposite to the source position - [dAttr] = getBezierPath(pathParams); - } else if (type === ConnectionLineType.Step) { - [dAttr] = getSmoothStepPath({ - ...pathParams, - borderRadius: 0, - }); - } else if (type === ConnectionLineType.SmoothStep) { - [dAttr] = getSmoothStepPath(pathParams); - } else if (type === ConnectionLineType.SimpleBezier) { - [dAttr] = getSimpleBezierPath(pathParams); - } else { - dAttr = `M${fromX},${fromY} ${toX},${toY}`; - } - - return ; -}; - -ConnectionLine.displayName = 'ConnectionLine'; +import type { ConnectionLineComponent, ReactFlowState } from '../../types'; +import { useConnection } from '../../hooks/useConnection'; type ConnectionLineWrapperProps = { type: ConnectionLineType; @@ -144,19 +22,18 @@ type ConnectionLineWrapperProps = { }; const selector = (s: ReactFlowState) => ({ - nodeId: s.connection.fromHandle?.nodeId, - handleType: s.connection.fromHandle?.type, nodesConnectable: s.nodesConnectable, isValid: s.connection.isValid, + inProgress: s.connection.inProgress, width: s.width, height: s.height, }); export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { - const { nodeId, handleType, nodesConnectable, width, height, isValid } = useStore(selector, shallow); - const renderConnectionLine = !!(nodeId && handleType && width && nodesConnectable); + const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow); + const renderConnection = !!(width && nodesConnectable && inProgress); - if (!renderConnectionLine) { + if (!renderConnection) { return null; } @@ -168,15 +45,78 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component } className="react-flow__connectionline react-flow__container" > - + ); } + +type ConnectionLineProps = { + type: ConnectionLineType; + style?: CSSProperties; + CustomComponent?: ConnectionLineComponent; + isValid: boolean | null; +}; + +const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => { + const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection(); + + if (!inProgress) { + return; + } + + if (CustomComponent) { + return ( + + ); + } + + let path = ''; + + const pathParams = { + sourceX: from.x, + sourceY: from.y, + sourcePosition: fromPosition, + targetX: to.x, + targetY: to.y, + targetPosition: toPosition, + }; + + switch (type) { + case ConnectionLineType.Bezier: + [path] = getBezierPath(pathParams); + break; + case ConnectionLineType.SimpleBezier: + [path] = getSimpleBezierPath(pathParams); + break; + case ConnectionLineType.Step: + [path] = getSmoothStepPath({ + ...pathParams, + borderRadius: 0, + }); + break; + case ConnectionLineType.SmoothStep: + [path] = getSmoothStepPath(pathParams); + break; + default: + [path] = getStraightPath(pathParams); + } + + return ; +}; + +ConnectionLine.displayName = 'ConnectionLine'; diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 4fe59a96..02ba9ced 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -43,17 +43,16 @@ const connectingSelector = const { fromHandle, toHandle, isValid } = connection; - const connectingTo = toHandle?.nodeId === nodeId && toHandle?.handleId === handleId && toHandle?.type === type; + const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type; return { - connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.handleId === handleId && fromHandle?.type === type, + connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.id === handleId && fromHandle?.type === type, connectingTo, - clickConnecting: - clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type, + clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.id === handleId && clickHandle?.type === type, isPossibleEndHandle: connectionMode === ConnectionMode.Strict ? fromHandle?.type !== type - : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.handleId, + : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id, connectionInProcess: !!fromHandle, valid: connectingTo && isValid, }; @@ -167,7 +166,7 @@ function HandleComponent( if (!connectionClickStartHandle) { onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type }); - store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); + store.setState({ connectionClickStartHandle: { nodeId, type, id: handleId } }); return; } @@ -181,7 +180,7 @@ function HandleComponent( }, connectionMode, fromNodeId: connectionClickStartHandle.nodeId, - fromHandleId: connectionClickStartHandle.handleId || null, + fromHandleId: connectionClickStartHandle.id || null, fromType: connectionClickStartHandle.type, isValidConnection: isValidConnectionHandler, flowId, diff --git a/packages/react/src/hooks/useConnection.ts b/packages/react/src/hooks/useConnection.ts index d8f1bae4..8a4b377b 100644 --- a/packages/react/src/hooks/useConnection.ts +++ b/packages/react/src/hooks/useConnection.ts @@ -2,19 +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) => ({ - ...s.connection, - inProgress: s.connection.fromHandle !== null, -}); +import { ConnectionState, pointToRendererPoint } from '@xyflow/system'; +const selector = (s: ReactFlowStore): ConnectionState => { + return s.connection.inProgress + ? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) } + : { ...s.connection }; +}; /** - * Hook for accessing the ongoing connection. + * Hook for accessing the connection state. * * @public - * @returns ongoing connection + * @returns ConnectionState */ -export function useConnection(): ConnectionState & { inProgress: boolean } { +export function useConnection(): ConnectionState { return useStore(selector, shallow); } diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 5ccd7070..57beb3f9 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -12,6 +12,7 @@ import { EdgeSelectionChange, NodeSelectionChange, ParentExpandChild, + initialConnection, NodeOrigin, } from '@xyflow/system'; @@ -310,27 +311,12 @@ const createStore = ({ ); }, cancelConnection: () => { - const { connection } = get(); set({ - connection: { - position: connection.position, - fromHandle: null, - toHandle: null, - isValid: null, - }, + connection: { ...initialConnection }, }); }, - updateConnection: (params) => { - const { connection } = get(); - - const currentConnection = { - connection: { - ...params, - position: params.position ?? connection.position, - }, - }; - - set(currentConnection); + updateConnection: (connection) => { + set({ connection }); }, reset: () => set({ ...getInitialState() }), diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 495afd3d..7d2789a2 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -8,6 +8,7 @@ import { devWarn, getInternalNodesBounds, NodeOrigin, + initialConnection, } from '@xyflow/system'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; @@ -104,12 +105,7 @@ const getInitialState = ({ multiSelectionActive: false, - connection: { - fromHandle: null, - toHandle: null, - position: { x: 0, y: 0 }, - isValid: null, - }, + connection: { ...initialConnection }, connectionClickStartHandle: null, connectOnClick: true, diff --git a/packages/react/src/types/edges.ts b/packages/react/src/types/edges.ts index 63a61adf..640366d7 100644 --- a/packages/react/src/types/edges.ts +++ b/packages/react/src/types/edges.ts @@ -10,7 +10,7 @@ import type { HandleType, Connection, ConnectionLineType, - HandleElement, + Handle, EdgePosition, StepPathOptions, OnError, @@ -193,8 +193,8 @@ export type OnReconnect = (oldEdge: EdgeType, newC export type ConnectionLineComponentProps = { connectionLineStyle?: CSSProperties; connectionLineType: ConnectionLineType; - fromNode?: Node; - fromHandle?: HandleElement; + fromNode: Node; + fromHandle: Handle; fromX: number; fromY: number; toX: number; @@ -202,6 +202,8 @@ export type ConnectionLineComponentProps = { fromPosition: Position; toPosition: Position; connectionStatus: 'valid' | 'invalid' | null; + toNode: Node | null; + toHandle: Handle | null; }; export type ConnectionLineComponent = ComponentType; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index ab99ad45..7ce5fabf 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -10,7 +10,7 @@ import { type OnViewportChange, type SelectionRect, type SnapGrid, - type ConnectingHandle, + type Handle, type Transform, type PanZoomInstance, type PanBy, @@ -80,7 +80,7 @@ export type ReactFlowStore & Required>) | null; snapToGrid: boolean; snapGrid: SnapGrid; diff --git a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte index 904a45d3..71e3f2c9 100644 --- a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte +++ b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte @@ -2,21 +2,59 @@ import cc from 'classcat'; import { useStore } from '$lib/store'; + import { + ConnectionLineType, + getBezierPath, + getConnectionStatus, + getSmoothStepPath, + getStraightPath + } from '@xyflow/system'; export let containerStyle: string = ''; export let style: string = ''; export let isCustomComponent: boolean = false; - const { width, height, connection } = useStore(); + const { width, height, connection, connectionLineType } = useStore(); + + let path: string | null = null; + + $: if ($connection.inProgress && !isCustomComponent) { + const { from, to, fromPosition, toPosition } = $connection; + const pathParams = { + sourceX: from.x, + sourceY: from.y, + sourcePosition: fromPosition, + targetX: to.x, + targetY: to.y, + targetPosition: toPosition + }; + + switch ($connectionLineType) { + case ConnectionLineType.Bezier: + [path] = getBezierPath(pathParams); + break; + case ConnectionLineType.Step: + [path] = getSmoothStepPath({ + ...pathParams, + borderRadius: 0 + }); + break; + case ConnectionLineType.SmoothStep: + [path] = getSmoothStepPath(pathParams); + break; + default: + [path] = getStraightPath(pathParams); + } + } -{#if $connection.path} +{#if $connection.inProgress} - + {#if !isCustomComponent} - + {/if} diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 7ebfeb78..90482aa1 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], - getFromHandle: () => $connection.startHandle + getFromHandle: () => $connection.fromHandle }); } } @@ -128,21 +128,20 @@ prevConnections = connections ?? new Map(); } - $: connectionInProcess = !!$connection.startHandle; + $: connectionInProcess = !!$connection.fromHandle; $: connectingFrom = - $connection.startHandle?.nodeId === nodeId && - $connection.startHandle?.type === type && - $connection.startHandle?.handleId === handleId; + $connection.fromHandle?.nodeId === nodeId && + $connection.fromHandle?.type === type && + $connection.fromHandle?.id === handleId; $: connectingTo = - $connection.endHandle?.nodeId === nodeId && - $connection.endHandle?.type === type && - $connection.endHandle?.handleId === handleId; + $connection.toHandle?.nodeId === nodeId && + $connection.toHandle?.type === type && + $connection.toHandle?.id === handleId; $: isPossibleEndHandle = $connectionMode === ConnectionMode.Strict - ? $connection.startHandle?.type !== type - : nodeId !== $connection.startHandle?.nodeId || - handleId !== $connection.startHandle?.handleId; - $: valid = connectingTo && $connection.status === 'valid'; + ? $connection.fromHandle?.type !== type + : nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id; + $: valid = connectingTo && $connection.isValid;