From 2fd5319cf2f3878d7d2c2ea1f660f7c0d01f1ee0 Mon Sep 17 00:00:00 2001 From: moklick Date: Fri, 24 Mar 2023 17:36:14 +0100 Subject: [PATCH 1/3] faet(handle): add isConnectableStart and isConnectableEnd --- packages/core/src/components/Handle/index.tsx | 23 +++++++++++++++---- packages/core/src/components/Handle/utils.ts | 5 +++- packages/core/src/styles/init.css | 2 +- packages/core/src/types/handles.ts | 2 ++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index c66f5272..1dc6df1c 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -29,6 +29,8 @@ const Handle = forwardRef( position = Position.Top, isValidConnection, isConnectable = true, + isConnectableStart = true, + isConnectableEnd = true, id, onConnect, children, @@ -72,7 +74,7 @@ const Handle = forwardRef( const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => { const isMouseTriggered = isMouseEvent(event); - if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { + if (isConnectableStart && ((isMouseTriggered && event.button === 0) || !isMouseTriggered)) { handlePointerDown({ event, handleId, @@ -99,6 +101,11 @@ const Handle = forwardRef( connectionMode, isValidConnection: isValidConnectionStore, } = store.getState(); + + if (!connectionStartHandle && !isConnectableStart) { + return; + } + if (!connectionStartHandle) { onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); store.setState({ connectionStartHandle: { nodeId, type, handleId } }); @@ -131,6 +138,11 @@ const Handle = forwardRef( store.setState({ connectionStartHandle: null }); }; + const connecting = + connectionStartHandle?.nodeId === nodeId && + connectionStartHandle?.handleId === handleId && + connectionStartHandle?.type === type; + return (
( source: !isTarget, target: isTarget, connectable: isConnectable, - connecting: - connectionStartHandle?.nodeId === nodeId && - connectionStartHandle?.handleId === handleId && - connectionStartHandle?.type === type, + connectablestart: isConnectableStart, + connectableend: isConnectableEnd, + connecting, + connectionindicator: + (isConnectable && isConnectableStart && !connecting) || (isConnectableEnd && connecting), }, ])} onMouseDown={onPointerDown} diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index bfd181e1..2825bb0e 100644 --- a/packages/core/src/components/Handle/utils.ts +++ b/packages/core/src/components/Handle/utils.ts @@ -92,6 +92,8 @@ export function isValidHandle( 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, @@ -102,9 +104,10 @@ export function isValidHandle( result.connection = connection; + const isConnectable = connectable && connectableEnd; // in strict mode we don't allow target to target or source to source connections const isValid = - handleToCheck.classList.contains('connectable') && + isConnectable && (connectionMode === ConnectionMode.Strict ? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target') : handleNodeId !== fromNodeId || handleId !== fromHandleId); diff --git a/packages/core/src/styles/init.css b/packages/core/src/styles/init.css index 790dcaff..f676e445 100644 --- a/packages/core/src/styles/init.css +++ b/packages/core/src/styles/init.css @@ -144,7 +144,7 @@ min-width: 5px; min-height: 5px; - &.connectable { + &.connectionindicator { pointer-events: all; cursor: crosshair; } diff --git a/packages/core/src/types/handles.ts b/packages/core/src/types/handles.ts index 0850526c..aff196f6 100644 --- a/packages/core/src/types/handles.ts +++ b/packages/core/src/types/handles.ts @@ -17,6 +17,8 @@ export interface HandleProps { type: HandleType; position: Position; isConnectable?: boolean; + isConnectableStart?: boolean; + isConnectableEnd?: boolean; onConnect?: OnConnect; isValidConnection?: (connection: Connection) => boolean; id?: string; From 395a1586c3a268bfacd4a0be58e0126674d17644 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 27 Mar 2023 12:56:57 +0200 Subject: [PATCH 2/3] refactor(handles): add connectionStart and connectionEnd handle to store, cleanup --- .../examples/Validation/ConnectionStatus.tsx | 28 +++++---- .../src/examples/Validation/index.tsx | 2 +- .../core/src/components/Handle/handler.ts | 14 +++-- packages/core/src/components/Handle/index.tsx | 60 ++++++++++++------- packages/core/src/components/Handle/utils.ts | 12 +++- packages/core/src/store/index.ts | 4 +- packages/core/src/store/initialState.ts | 4 +- packages/core/src/types/general.ts | 10 ++-- packages/core/src/types/handles.ts | 17 +++--- 9 files changed, 94 insertions(+), 57 deletions(-) diff --git a/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx b/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx index 4102b9a7..43cdb5bf 100644 --- a/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx +++ b/examples/vite-app/src/examples/Validation/ConnectionStatus.tsx @@ -6,15 +6,21 @@ import styles from './validation.module.css'; const selector = (state: ReactFlowState) => ({ connectionPosition: state.connectionPosition, connectionStatus: state.connectionStatus, - connectionNodeId: state.connectionNodeId, - connectionTargetNodeId: state.connectionTargetNodeId, + connectionStartNodeId: state.connectionStartHandle?.nodeId, + connectionStartHandleType: state.connectionStartHandle?.type, + connectionEndNodeId: state.connectionEndHandle?.nodeId, + connectionEndHandleType: state.connectionEndHandle?.type, }); function ConnectionStatus() { - const { connectionPosition, connectionStatus, connectionNodeId, connectionTargetNodeId } = useStore( - selector, - shallow - ); + const { + connectionPosition, + connectionStatus, + connectionStartNodeId, + connectionStartHandleType, + connectionEndNodeId, + connectionEndHandleType, + } = useStore(selector, shallow); if (!connectionPosition) { return null; @@ -22,15 +28,17 @@ function ConnectionStatus() { return (
- {connectionNodeId ? ( + {connectionStartNodeId ? ( <>
connection info
position: {JSON.stringify(connectionPosition)}
-
status: {JSON.stringify(connectionStatus)}
-
source node id: {JSON.stringify(connectionNodeId)}
-
target node id: {JSON.stringify(connectionTargetNodeId)}
+
status: {connectionStatus}
+
from node id: {connectionStartNodeId}
+
from handle type: {connectionStartHandleType}
+
to node id: {connectionEndNodeId}
+
to handle type: {connectionEndHandleType}
) : ( 'no connection data' diff --git a/examples/vite-app/src/examples/Validation/index.tsx b/examples/vite-app/src/examples/Validation/index.tsx index ed2b5419..f385083b 100644 --- a/examples/vite-app/src/examples/Validation/index.tsx +++ b/examples/vite-app/src/examples/Validation/index.tsx @@ -38,7 +38,7 @@ const CustomInput: FC = () => ( const CustomNode: FC = ({ id }) => ( <> - +
{id}
diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index 3ad191b8..9825295b 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -89,12 +89,17 @@ export function handlePointerDown({ setState({ connectionPosition, + connectionStatus: null, + // connectionNodeId etc will be removed in the next major in favor of connectionStartHandle connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType, - connectionStatus: null, - connectionTargetNodeId: null, - connectionTargetHandleId: null, + connectionStartHandle: { + nodeId, + handleId, + type: handleType, + }, + connectionEndHandle: null, }); onConnectStart?.(event, { nodeId, handleId, handleType }); @@ -141,8 +146,7 @@ export function handlePointerDown({ ) : connectionPosition, connectionStatus: getConnectionStatus(!!prevClosestHandle, isValid), - connectionTargetNodeId: connection.target, - connectionTargetHandleId: connection.targetHandle, + connectionEndHandle: result.endHandle, }); if (!prevClosestHandle && !isValid && !handleDomNode) { diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index 1dc6df1c..dcd95d7c 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -7,8 +7,7 @@ import { useNodeId } from '../../contexts/NodeIdContext'; import { handlePointerDown } from './handler'; import { getHostForElement, isMouseEvent } from '../../utils'; import { addEdge } from '../../utils/graph'; -import { Position } from '../../types'; -import type { HandleProps, Connection, ReactFlowState } from '../../types'; +import { type HandleProps, type Connection, type ReactFlowState, HandleType, Position } from '../../types'; import { isValidHandle } from './utils'; import { errorMessages } from '../../contants'; @@ -22,6 +21,23 @@ const selector = (s: ReactFlowState) => ({ 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( ( { @@ -41,20 +57,17 @@ const Handle = forwardRef( }, ref ) => { + const handleId = id || null; + const isTarget = type === 'target'; const store = useStoreApi(); const nodeId = useNodeId(); + const { connectOnClick, noPanClassName } = useStore(selector, shallow); + const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type)); if (!nodeId) { 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 { defaultEdgeOptions, onConnect: onConnectAction, hasDefaultEdges } = store.getState(); @@ -72,6 +85,10 @@ const Handle = forwardRef( }; const onPointerDown = (event: ReactMouseEvent | ReactTouchEvent) => { + if (!nodeId) { + return; + } + const isMouseTriggered = isMouseEvent(event); if (isConnectableStart && ((isMouseTriggered && event.button === 0) || !isMouseTriggered)) { @@ -98,17 +115,18 @@ const Handle = forwardRef( const { onClickConnectStart, onClickConnectEnd, + connectionClickStartHandle, connectionMode, isValidConnection: isValidConnectionStore, } = store.getState(); - if (!connectionStartHandle && !isConnectableStart) { + if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) { return; } - if (!connectionStartHandle) { + if (!connectionClickStartHandle) { onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); - store.setState({ connectionStartHandle: { nodeId, type, handleId } }); + store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); return; } @@ -122,9 +140,9 @@ const Handle = forwardRef( type, }, connectionMode, - connectionStartHandle.nodeId, - connectionStartHandle.handleId || null, - connectionStartHandle.type, + connectionClickStartHandle.nodeId, + connectionClickStartHandle.handleId || null, + connectionClickStartHandle.type, isValidConnectionHandler, doc ); @@ -135,14 +153,9 @@ const Handle = forwardRef( onClickConnectEnd?.(event as unknown as MouseEvent); - store.setState({ connectionStartHandle: null }); + store.setState({ connectionClickStartHandle: null }); }; - const connecting = - connectionStartHandle?.nodeId === nodeId && - connectionStartHandle?.handleId === handleId && - connectionStartHandle?.type === type; - return (
( connectable: isConnectable, connectablestart: isConnectableStart, connectableend: isConnectableEnd, - connecting, + connecting: clickConnecting, + // this class is used to style the handle when the user is connecting connectionindicator: - (isConnectable && isConnectableStart && !connecting) || (isConnectableEnd && connecting), + isConnectable && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)), }, ])} onMouseDown={onPointerDown} diff --git a/packages/core/src/components/Handle/utils.ts b/packages/core/src/components/Handle/utils.ts index 2825bb0e..b8e8cf6a 100644 --- a/packages/core/src/components/Handle/utils.ts +++ b/packages/core/src/components/Handle/utils.ts @@ -1,6 +1,6 @@ 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 type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '../../types'; @@ -59,6 +59,7 @@ type Result = { handleDomNode: Element | null; isValid: boolean; connection: Connection; + endHandle: ConnectingHandle | null; }; const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null }; @@ -70,7 +71,7 @@ export function isValidHandle( connectionMode: ConnectionMode, fromNodeId: string, fromHandleId: string | null, - fromType: string, + fromType: HandleType, isValidConnection: ValidConnectionFunc, doc: Document | ShadowRoot ) { @@ -86,6 +87,7 @@ export function isValidHandle( handleDomNode: handleToCheck, isValid: false, connection: nullConnection, + endHandle: null, }; if (handleToCheck) { @@ -113,6 +115,12 @@ export function isValidHandle( : handleNodeId !== fromNodeId || handleId !== fromHandleId); if (isValid) { + result.endHandle = { + nodeId: handleNodeId as string, + handleId, + type: handleType as HandleType, + }; + result.isValid = isValidConnection(connection); } } diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 58cc5080..f4365d45 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -275,8 +275,8 @@ const createRFStore = () => connectionHandleId: initialState.connectionHandleId, connectionHandleType: initialState.connectionHandleType, connectionStatus: initialState.connectionStatus, - connectionTargetNodeId: initialState.connectionTargetNodeId, - connectionTargetHandleId: initialState.connectionTargetHandleId, + connectionStartHandle: initialState.connectionStartHandle, + connectionEndHandle: initialState.connectionEndHandle, }), reset: () => set({ ...initialState }), })); diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts index 2722038d..8c4a5d76 100644 --- a/packages/core/src/store/initialState.ts +++ b/packages/core/src/store/initialState.ts @@ -31,8 +31,6 @@ const initialState: ReactFlowStore = { connectionNodeId: null, connectionHandleId: null, connectionHandleType: 'source', - connectionTargetNodeId: null, - connectionTargetHandleId: null, connectionPosition: { x: 0, y: 0 }, connectionStatus: null, connectionMode: ConnectionMode.Strict, @@ -57,6 +55,8 @@ const initialState: ReactFlowStore = { multiSelectionActive: false, connectionStartHandle: null, + connectionEndHandle: null, + connectionClickStartHandle: null, connectOnClick: true, ariaLiveMessage: '', diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index fab391d0..aeb33632 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -21,7 +21,7 @@ import type { NodeOrigin, } from './nodes'; 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 { ReactFlowInstance } from './instance'; @@ -167,10 +167,9 @@ export type ReactFlowStore = { userSelectionActive: boolean; userSelectionRect: SelectionRect | null; + // @todo remove this in next major version in favor of connectionStartHandle connectionNodeId: string | null; connectionHandleId: string | null; - connectionTargetNodeId: string | null; - connectionTargetHandleId: string | null; connectionHandleType: HandleType | null; connectionPosition: XYPosition; connectionStatus: ConnectionStatus | null; @@ -188,7 +187,10 @@ export type ReactFlowStore = { 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; onNodeDrag?: NodeDragHandler; diff --git a/packages/core/src/types/handles.ts b/packages/core/src/types/handles.ts index aff196f6..7e40d1e5 100644 --- a/packages/core/src/types/handles.ts +++ b/packages/core/src/types/handles.ts @@ -2,18 +2,19 @@ import type { XYPosition, Position, Dimensions, OnConnect, Connection } from '.' export type HandleType = 'source' | 'target'; -export interface HandleElement extends XYPosition, Dimensions { - id?: string | null; - position: Position; -} +export type HandleElement = XYPosition & + Dimensions & { + id?: string | null; + position: Position; + }; -export interface StartHandle { +export type ConnectingHandle = { nodeId: string; type: HandleType; handleId?: string | null; -} +}; -export interface HandleProps { +export type HandleProps = { type: HandleType; position: Position; isConnectable?: boolean; @@ -22,4 +23,4 @@ export interface HandleProps { onConnect?: OnConnect; isValidConnection?: (connection: Connection) => boolean; id?: string; -} +}; From 4d97a0ed168ce643fc0c99fa6b47cf1296d66065 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 27 Mar 2023 12:58:42 +0200 Subject: [PATCH 3/3] chore(changeset): add --- .changeset/thick-needles-march.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/thick-needles-march.md diff --git a/.changeset/thick-needles-march.md b/.changeset/thick-needles-march.md new file mode 100644 index 00000000..26d0890e --- /dev/null +++ b/.changeset/thick-needles-march.md @@ -0,0 +1,7 @@ +--- +'@reactflow/background': minor +'@reactflow/core': minor +'@reactflow/minimap': minor +--- + +Handles: add isConnectableStart and isConnectableEnd props