streamlined connections

This commit is contained in:
peterkogo
2024-06-26 15:03:12 +02:00
parent 44254648c8
commit 5493f045ff
25 changed files with 355 additions and 474 deletions
@@ -1,140 +1,17 @@
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 (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode.internals.userNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
/>
);
}
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 <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
type ConnectionLineWrapperProps = {
type: ConnectionLineType;
@@ -144,19 +21,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 isIncorrect = !(nodeId && handleType && width && nodesConnectable);
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress);
if (isIncorrect) {
if (!renderConnection) {
return null;
}
@@ -168,15 +44,81 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container"
>
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
isValid={isValid}
/>
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
</g>
</svg>
);
}
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 } = useStore(
(s: ReactFlowState) => s.connection,
shallow
);
if (!inProgress) {
return;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={from.x}
fromY={from.y}
toX={to.x}
toY={to.y}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
toNode={toNode}
toHandle={toHandle}
/>
);
}
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 <path d={path} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
@@ -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,
+1 -2
View File
@@ -6,7 +6,6 @@ import { ConnectionState } from '@xyflow/system';
const selector = (s: ReactFlowStore) => ({
...s.connection,
inProgress: s.connection.fromHandle !== null,
});
/**
@@ -15,6 +14,6 @@ const selector = (s: ReactFlowStore) => ({
* @public
* @returns ongoing connection
*/
export function useConnection(): ConnectionState & { inProgress: boolean } {
export function useConnection(): ConnectionState {
return useStore(selector, shallow);
}
+4 -18
View File
@@ -12,6 +12,7 @@ import {
EdgeSelectionChange,
NodeSelectionChange,
ParentExpandChild,
initialConnection,
} from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -308,27 +309,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() }),
+2 -6
View File
@@ -7,6 +7,7 @@ import {
updateConnectionLookup,
devWarn,
getInternalNodesBounds,
initialConnection,
} from '@xyflow/system';
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
@@ -101,12 +102,7 @@ const getInitialState = ({
multiSelectionActive: false,
connection: {
fromHandle: null,
toHandle: null,
position: { x: 0, y: 0 },
isValid: null,
},
connection: { ...initialConnection },
connectionClickStartHandle: null,
connectOnClick: true,
+5 -3
View File
@@ -10,7 +10,7 @@ import type {
HandleType,
Connection,
ConnectionLineType,
HandleElement,
Handle,
EdgePosition,
StepPathOptions,
OnError,
@@ -193,8 +193,8 @@ export type OnReconnect<EdgeType extends Edge = Edge> = (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<ConnectionLineComponentProps>;
+2 -2
View File
@@ -10,7 +10,7 @@ import {
type OnViewportChange,
type SelectionRect,
type SnapGrid,
type ConnectingHandle,
type Handle,
type Transform,
type PanZoomInstance,
type PanBy,
@@ -79,7 +79,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
connection: ConnectionState;
connectionMode: ConnectionMode;
connectionClickStartHandle: ConnectingHandle | null;
connectionClickStartHandle: (Pick<Handle, 'nodeId' | 'id'> & Required<Pick<Handle, 'type'>>) | null;
snapToGrid: boolean;
snapGrid: SnapGrid;