Merge pull request #2958 from wbkd/feat/handle-start-connection

feat(handle): add isConnectableStart and isConnectableEnd
This commit is contained in:
Moritz Klack
2023-03-27 12:59:09 +02:00
committed by GitHub
11 changed files with 118 additions and 56 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@reactflow/background': minor
'@reactflow/core': minor
'@reactflow/minimap': minor
---
Handles: add isConnectableStart and isConnectableEnd props
@@ -6,15 +6,21 @@ import styles from './validation.module.css';
const selector = (state: ReactFlowState) => ({ const selector = (state: ReactFlowState) => ({
connectionPosition: state.connectionPosition, connectionPosition: state.connectionPosition,
connectionStatus: state.connectionStatus, connectionStatus: state.connectionStatus,
connectionNodeId: state.connectionNodeId, connectionStartNodeId: state.connectionStartHandle?.nodeId,
connectionTargetNodeId: state.connectionTargetNodeId, connectionStartHandleType: state.connectionStartHandle?.type,
connectionEndNodeId: state.connectionEndHandle?.nodeId,
connectionEndHandleType: state.connectionEndHandle?.type,
}); });
function ConnectionStatus() { function ConnectionStatus() {
const { connectionPosition, connectionStatus, connectionNodeId, connectionTargetNodeId } = useStore( const {
selector, connectionPosition,
shallow connectionStatus,
); connectionStartNodeId,
connectionStartHandleType,
connectionEndNodeId,
connectionEndHandleType,
} = useStore(selector, shallow);
if (!connectionPosition) { if (!connectionPosition) {
return null; return null;
@@ -22,15 +28,17 @@ function ConnectionStatus() {
return ( return (
<div className={styles.connectionstatus}> <div className={styles.connectionstatus}>
{connectionNodeId ? ( {connectionStartNodeId ? (
<> <>
<div> <div>
<strong>connection info</strong> <strong>connection info</strong>
</div> </div>
<div>position: {JSON.stringify(connectionPosition)}</div> <div>position: {JSON.stringify(connectionPosition)}</div>
<div>status: {JSON.stringify(connectionStatus)}</div> <div>status: {connectionStatus}</div>
<div>source node id: {JSON.stringify(connectionNodeId)}</div> <div>from node id: {connectionStartNodeId}</div>
<div>target node id: {JSON.stringify(connectionTargetNodeId)}</div> <div>from handle type: {connectionStartHandleType}</div>
<div>to node id: {connectionEndNodeId}</div>
<div>to handle type: {connectionEndHandleType}</div>
</> </>
) : ( ) : (
'no connection data' 'no connection data'
@@ -38,7 +38,7 @@ const CustomInput: FC<NodeProps> = () => (
const CustomNode: FC<NodeProps> = ({ id }) => ( const CustomNode: FC<NodeProps> = ({ id }) => (
<> <>
<Handle type="target" position={Position.Left} /> <Handle type="target" position={Position.Left} isConnectableStart={false} />
<div>{id}</div> <div>{id}</div>
<Handle type="source" position={Position.Right} /> <Handle type="source" position={Position.Right} />
</> </>
@@ -89,12 +89,17 @@ export function handlePointerDown({
setState({ setState({
connectionPosition, connectionPosition,
connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionNodeId: nodeId, connectionNodeId: nodeId,
connectionHandleId: handleId, connectionHandleId: handleId,
connectionHandleType: handleType, connectionHandleType: handleType,
connectionStatus: null, connectionStartHandle: {
connectionTargetNodeId: null, nodeId,
connectionTargetHandleId: null, handleId,
type: handleType,
},
connectionEndHandle: null,
}); });
onConnectStart?.(event, { nodeId, handleId, handleType }); onConnectStart?.(event, { nodeId, handleId, handleType });
@@ -141,8 +146,7 @@ export function handlePointerDown({
) )
: connectionPosition, : connectionPosition,
connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid), connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid),
connectionTargetNodeId: connection.target, connectionEndHandle: result.endHandle,
connectionTargetHandleId: connection.targetHandle,
}); });
if (!prevClosestHandle && !isValid && !handleDomNode) { if (!prevClosestHandle && !isValid && !handleDomNode) {
+47 -20
View File
@@ -7,8 +7,7 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler'; import { handlePointerDown } from './handler';
import { getHostForElement, isMouseEvent } from '../../utils'; import { getHostForElement, isMouseEvent } from '../../utils';
import { addEdge } from '../../utils/graph'; import { addEdge } from '../../utils/graph';
import { Position } from '../../types'; import { type HandleProps, type Connection, type ReactFlowState, HandleType, Position } from '../../types';
import type { HandleProps, Connection, ReactFlowState } from '../../types';
import { isValidHandle } from './utils'; import { isValidHandle } from './utils';
import { errorMessages } from '../../contants'; import { errorMessages } from '../../contants';
@@ -22,6 +21,23 @@ const selector = (s: ReactFlowState) => ({
noPanClassName: s.noPanClassName, noPanClassName: s.noPanClassName,
}); });
const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const {
connectionStartHandle: startHandle,
connectionEndHandle: endHandle,
connectionClickStartHandle: clickHandle,
} = state;
return {
connecting:
(startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type) ||
(endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type),
clickConnecting:
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
};
};
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>( const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
( (
{ {
@@ -29,6 +45,8 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
position = Position.Top, position = Position.Top,
isValidConnection, isValidConnection,
isConnectable = true, isConnectable = true,
isConnectableStart = true,
isConnectableEnd = true,
id, id,
onConnect, onConnect,
children, children,
@@ -39,20 +57,17 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}, },
ref ref
) => { ) => {
const handleId = id || null;
const isTarget = type === 'target';
const store = useStoreApi(); const store = useStoreApi();
const nodeId = useNodeId(); const nodeId = useNodeId();
const { connectOnClick, noPanClassName } = useStore(selector, shallow);
const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type));
if (!nodeId) { if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']()); store.getState().onError?.('010', errorMessages['error010']());
return null;
} }
const { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = (params: Connection) => { const onConnectExtended = (params: Connection) => {
const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState(); const { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState();
@@ -70,9 +85,13 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}; };
const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => { const onPointerDown = (event: ReactMouseEvent<HTMLDivElement> | ReactTouchEvent<HTMLDivElement>) => {
if (!nodeId) {
return;
}
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { if (isConnectableStart && ((isMouseTriggered && event.button === 0) || !isMouseTriggered)) {
handlePointerDown({ handlePointerDown({
event, event,
handleId, handleId,
@@ -96,12 +115,18 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
const { const {
onClickConnectStart, onClickConnectStart,
onClickConnectEnd, onClickConnectEnd,
connectionClickStartHandle,
connectionMode, connectionMode,
isValidConnection: isValidConnectionStore, isValidConnection: isValidConnectionStore,
} = store.getState(); } = store.getState();
if (!connectionStartHandle) {
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
return;
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } }); store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return; return;
} }
@@ -115,9 +140,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
type, type,
}, },
connectionMode, connectionMode,
connectionStartHandle.nodeId, connectionClickStartHandle.nodeId,
connectionStartHandle.handleId || null, connectionClickStartHandle.handleId || null,
connectionStartHandle.type, connectionClickStartHandle.type,
isValidConnectionHandler, isValidConnectionHandler,
doc doc
); );
@@ -128,7 +153,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
onClickConnectEnd?.(event as unknown as MouseEvent); onClickConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null }); store.setState({ connectionClickStartHandle: null });
}; };
return ( return (
@@ -147,10 +172,12 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
source: !isTarget, source: !isTarget,
target: isTarget, target: isTarget,
connectable: isConnectable, connectable: isConnectable,
connecting: connectablestart: isConnectableStart,
connectionStartHandle?.nodeId === nodeId && connectableend: isConnectableEnd,
connectionStartHandle?.handleId === handleId && connecting: clickConnecting,
connectionStartHandle?.type === type, // this class is used to style the handle when the user is connecting
connectionindicator:
isConnectable && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)),
}, },
])} ])}
onMouseDown={onPointerDown} onMouseDown={onPointerDown}
+14 -3
View File
@@ -1,6 +1,6 @@
import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react'; import { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import { ConnectionMode, ConnectionStatus } from '../../types'; import { ConnectingHandle, ConnectionMode, ConnectionStatus } from '../../types';
import { getEventPosition, internalsSymbol } from '../../utils'; import { getEventPosition, internalsSymbol } from '../../utils';
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types'; import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types';
@@ -59,6 +59,7 @@ type Result = {
handleDomNode: Element | null; handleDomNode: Element | null;
isValid: boolean; isValid: boolean;
connection: Connection; connection: Connection;
endHandle: ConnectingHandle | null;
}; };
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null }; const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
@@ -70,7 +71,7 @@ export function isValidHandle(
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
fromNodeId: string, fromNodeId: string,
fromHandleId: string | null, fromHandleId: string | null,
fromType: string, fromType: HandleType,
isValidConnection: ValidConnectionFunc, isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot doc: Document | ShadowRoot
) { ) {
@@ -86,12 +87,15 @@ export function isValidHandle(
handleDomNode: handleToCheck, handleDomNode: handleToCheck,
isValid: false, isValid: false,
connection: nullConnection, connection: nullConnection,
endHandle: null,
}; };
if (handleToCheck) { if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck); const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid'); const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid'); const handleId = handleToCheck.getAttribute('data-handleid');
const connectable = handleToCheck.classList.contains('connectable');
const connectableEnd = handleToCheck.classList.contains('connectableend');
const connection: Connection = { const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId, source: isTarget ? handleNodeId : fromNodeId,
@@ -102,14 +106,21 @@ export function isValidHandle(
result.connection = connection; result.connection = connection;
const isConnectable = connectable && connectableEnd;
// in strict mode we don't allow target to target or source to source connections // in strict mode we don't allow target to target or source to source connections
const isValid = const isValid =
handleToCheck.classList.contains('connectable') && isConnectable &&
(connectionMode === ConnectionMode.Strict (connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId); : handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) { if (isValid) {
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection); result.isValid = isValidConnection(connection);
} }
} }
+2 -2
View File
@@ -275,8 +275,8 @@ const createRFStore = () =>
connectionHandleId: initialState.connectionHandleId, connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType, connectionHandleType: initialState.connectionHandleType,
connectionStatus: initialState.connectionStatus, connectionStatus: initialState.connectionStatus,
connectionTargetNodeId: initialState.connectionTargetNodeId, connectionStartHandle: initialState.connectionStartHandle,
connectionTargetHandleId: initialState.connectionTargetHandleId, connectionEndHandle: initialState.connectionEndHandle,
}), }),
reset: () => set({ ...initialState }), reset: () => set({ ...initialState }),
})); }));
+2 -2
View File
@@ -31,8 +31,6 @@ const initialState: ReactFlowStore = {
connectionNodeId: null, connectionNodeId: null,
connectionHandleId: null, connectionHandleId: null,
connectionHandleType: 'source', connectionHandleType: 'source',
connectionTargetNodeId: null,
connectionTargetHandleId: null,
connectionPosition: { x: 0, y: 0 }, connectionPosition: { x: 0, y: 0 },
connectionStatus: null, connectionStatus: null,
connectionMode: ConnectionMode.Strict, connectionMode: ConnectionMode.Strict,
@@ -57,6 +55,8 @@ const initialState: ReactFlowStore = {
multiSelectionActive: false, multiSelectionActive: false,
connectionStartHandle: null, connectionStartHandle: null,
connectionEndHandle: null,
connectionClickStartHandle: null,
connectOnClick: true, connectOnClick: true,
ariaLiveMessage: '', ariaLiveMessage: '',
+1 -1
View File
@@ -144,7 +144,7 @@
min-width: 5px; min-width: 5px;
min-height: 5px; min-height: 5px;
&.connectable { &.connectionindicator {
pointer-events: all; pointer-events: all;
cursor: crosshair; cursor: crosshair;
} }
+6 -4
View File
@@ -21,7 +21,7 @@ import type {
NodeOrigin, NodeOrigin,
} from './nodes'; } from './nodes';
import type { Edge, EdgeProps, WrapEdgeProps } from './edges'; import type { Edge, EdgeProps, WrapEdgeProps } from './edges';
import type { HandleType, StartHandle } from './handles'; import type { HandleType, ConnectingHandle } from './handles';
import type { DefaultEdgeOptions } from '.'; import type { DefaultEdgeOptions } from '.';
import type { ReactFlowInstance } from './instance'; import type { ReactFlowInstance } from './instance';
@@ -167,10 +167,9 @@ export type ReactFlowStore = {
userSelectionActive: boolean; userSelectionActive: boolean;
userSelectionRect: SelectionRect | null; userSelectionRect: SelectionRect | null;
// @todo remove this in next major version in favor of connectionStartHandle
connectionNodeId: string | null; connectionNodeId: string | null;
connectionHandleId: string | null; connectionHandleId: string | null;
connectionTargetNodeId: string | null;
connectionTargetHandleId: string | null;
connectionHandleType: HandleType | null; connectionHandleType: HandleType | null;
connectionPosition: XYPosition; connectionPosition: XYPosition;
connectionStatus: ConnectionStatus | null; connectionStatus: ConnectionStatus | null;
@@ -188,7 +187,10 @@ export type ReactFlowStore = {
multiSelectionActive: boolean; multiSelectionActive: boolean;
connectionStartHandle: StartHandle | null; connectionStartHandle: ConnectingHandle | null;
connectionEndHandle: ConnectingHandle | null;
// @todo this is only used for the click connection - we might remove this in the next major version
connectionClickStartHandle: ConnectingHandle | null;
onNodeDragStart?: NodeDragHandler; onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler; onNodeDrag?: NodeDragHandler;
+11 -8
View File
@@ -2,22 +2,25 @@ import type { XYPosition, Position, Dimensions, OnConnect, Connection } from '.'
export type HandleType = 'source' | 'target'; export type HandleType = 'source' | 'target';
export interface HandleElement extends XYPosition, Dimensions { export type HandleElement = XYPosition &
id?: string | null; Dimensions & {
position: Position; id?: string | null;
} position: Position;
};
export interface StartHandle { export type ConnectingHandle = {
nodeId: string; nodeId: string;
type: HandleType; type: HandleType;
handleId?: string | null; handleId?: string | null;
} };
export interface HandleProps { export type HandleProps = {
type: HandleType; type: HandleType;
position: Position; position: Position;
isConnectable?: boolean; isConnectable?: boolean;
isConnectableStart?: boolean;
isConnectableEnd?: boolean;
onConnect?: OnConnect; onConnect?: OnConnect;
isValidConnection?: (connection: Connection) => boolean; isValidConnection?: (connection: Connection) => boolean;
id?: string; id?: string;
} };