diff --git a/examples/react/src/examples/TouchDevice/index.tsx b/examples/react/src/examples/TouchDevice/index.tsx index 636a36a4..e412bee5 100644 --- a/examples/react/src/examples/TouchDevice/index.tsx +++ b/examples/react/src/examples/TouchDevice/index.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import { ReactFlow, Node, Edge, useNodesState, useEdgesState, Position, Connection, addEdge } from '@xyflow/react'; -import styles from './touch-device.module.css'; +import './touch-device.css'; const initialNodes: Node[] = [ { @@ -42,7 +42,7 @@ const TouchDeviceFlow = () => { onConnectEnd={onConnectEnd} onClickConnectStart={onClickConnectStart} onClickConnectEnd={onClickConnectEnd} - className={styles.flow} + className="touch-flow" /> ); }; diff --git a/examples/react/src/examples/TouchDevice/touch-device.css b/examples/react/src/examples/TouchDevice/touch-device.css new file mode 100644 index 00000000..92f85c27 --- /dev/null +++ b/examples/react/src/examples/TouchDevice/touch-device.css @@ -0,0 +1,27 @@ +.react-flow.touch-flow .react-flow__handle { + width: 20px; + height: 20px; + border-radius: 3px; + background-color: #9f7aea; +} + +.touch-flow .react-flow__handle-right { + --translate: translate(50%, -50%); +} + +.touch-flow .react-flow__handle-left { + --translate: translate(-50%, -50%); +} + +@keyframes bounce { + 0% { + transform: var(--translate) scale(1); + } + 50% { + transform: var(--translate) scale(1.1); + } +} + +.react-flow.touch-flow .react-flow__handle.clickconnecting { + animation: bounce 1600ms infinite ease-in; +} diff --git a/examples/react/src/examples/TouchDevice/touch-device.module.css b/examples/react/src/examples/TouchDevice/touch-device.module.css deleted file mode 100644 index b5f65209..00000000 --- a/examples/react/src/examples/TouchDevice/touch-device.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.flow :global .react-flow__handle { - width: 20px; - height: 20px; - border-radius: 3px; - background-color: #9f7aea; -} - -.flow :global .react-flow__handle.connecting { - animation: bounce 1600ms infinite ease-out; -} - -@keyframes bounce { - 0% { - transform: translate(0, -50%) scale(1); - } - 50% { - transform: translate(0, -50%) scale(1.1); - } -} diff --git a/examples/react/src/examples/Validation/validation.module.css b/examples/react/src/examples/Validation/validation.module.css index c19a8723..3fbe2f94 100644 --- a/examples/react/src/examples/Validation/validation.module.css +++ b/examples/react/src/examples/Validation/validation.module.css @@ -24,10 +24,14 @@ background: #fff; } -.validationflow :global .connecting { +.validationflow :global .connectingto { background: #ff6060; } +.validationflow :global .react-flow__node-custominput .connectingfrom { + background: #55dd99; +} + .validationflow :global .valid { background: #55dd99; } diff --git a/examples/svelte/src/routes/examples/validation/style.css b/examples/svelte/src/routes/examples/validation/style.css index 6839e129..a6bbf75a 100644 --- a/examples/svelte/src/routes/examples/validation/style.css +++ b/examples/svelte/src/routes/examples/validation/style.css @@ -1,7 +1,11 @@ -.svelte-flow__handle.connecting { +.svelte-flow__handle.connectingto { background: #ff6060; } +.svelte-flow__handle.connectingfrom { + background: #55dd99; +} + .svelte-flow__handle.valid { background: #55dd99; } diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index 69b51bf7..0f5f3b92 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -5,6 +5,7 @@ ## ⚠️ Breaking changes - `useNodesData` not only returns data objects but also the type and the id of the node +- status class names for Handle components are slightly different. It's now "connectingfrom" and "connectingto" instead of "connecting" ## Patch changes diff --git a/packages/react/src/components/Handle/index.tsx b/packages/react/src/components/Handle/index.tsx index 36778039..75c36b8c 100644 --- a/packages/react/src/components/Handle/index.tsx +++ b/packages/react/src/components/Handle/index.tsx @@ -16,6 +16,7 @@ import { type HandleProps, type Connection, type HandleType, + ConnectionMode, } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; @@ -36,14 +37,24 @@ const connectingSelector = connectionStartHandle: startHandle, connectionEndHandle: endHandle, connectionClickStartHandle: clickHandle, + connectionMode, + connectionStatus, } = state; + const connectingTo = endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type; + return { - connecting: - (startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type) || - (endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type), + connectingFrom: + startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.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', }; }; @@ -71,7 +82,10 @@ const HandleComponent = forwardRef( const store = useStoreApi(); const nodeId = useNodeId(); const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow); - const { connecting, clickConnecting } = useStore(connectingSelector(nodeId, handleId, type), shallow); + const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore( + connectingSelector(nodeId, handleId, type), + shallow + ); if (!nodeId) { store.getState().onError?.('010', errorMessages['error010']()); @@ -201,10 +215,16 @@ const HandleComponent = forwardRef( connectable: isConnectable, connectablestart: isConnectableStart, connectableend: isConnectableEnd, - connecting: clickConnecting, - // this class is used to style the handle when the user is connecting + clickconnecting: clickConnecting, + connectingfrom: connectingFrom, + connectingto: connectingTo, + valid, + // shows where you can start a connection from + // and where you can end it while connecting connectionindicator: - isConnectable && ((isConnectableStart && !connecting) || (isConnectableEnd && connecting)), + isConnectable && + (!connectionInProcess || isPossibleEndHandle) && + (connectionInProcess ? isConnectableEnd : isConnectableStart), }, ])} onMouseDown={onPointerDown} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 823d5022..a2b98c1a 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -303,13 +303,11 @@ const createRFStore = ({ connectionEndHandle: null, }), updateConnection: (params) => { - const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get(); + const { connectionPosition } = get(); const currentConnection = { + ...params, connectionPosition: params.connectionPosition ?? connectionPosition, - connectionStatus: params.connectionStatus ?? connectionStatus, - connectionStartHandle: params.connectionStartHandle ?? connectionStartHandle, - connectionEndHandle: params.connectionEndHandle ?? connectionEndHandle, }; set(currentConnection); diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 948e69eb..ea7f4181 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -5,6 +5,7 @@ ## ⚠️ Breaking changes - `useNodesData` not only returns data objects but also the type and the id of the node +- status class names for Handle components are slightly different. It's now "connectingfrom" and "connectingto" instead of "connecting" ## Patch changes diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index f8a43dd4..e35804fc 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -56,7 +56,8 @@ onconnect: onConnectAction, onconnectstart: onConnectStartAction, onconnectend: onConnectEndAction, - flowId + flowId, + connection } = store; function onPointerDown(event: MouseEvent | TouchEvent) { @@ -123,6 +124,16 @@ prevConnections = connections ?? new Map(); } + $: connectingFrom = + $connection.startHandle?.nodeId === nodeId && + $connection.startHandle?.type === type && + $connection.startHandle?.handleId === handleId; + $: connectingTo = + $connection.endHandle?.nodeId === nodeId && + $connection.endHandle?.type === type && + $connection.endHandle?.handleId === handleId; + $: valid = connectingTo && $connection.status === 'valid'; + // @todo implement connectablestart, connectableend @@ -141,6 +152,11 @@ The Handle component is the part of a node that can be used to connect nodes. 'nodrag', 'nopan', position, + { + valid, + connectingto: connectingTo, + connectingfrom: connectingFrom + }, className ])} class:source={!isTarget} diff --git a/packages/system/src/styles/init.css b/packages/system/src/styles/init.css index 46628842..72f0b2e8 100644 --- a/packages/system/src/styles/init.css +++ b/packages/system/src/styles/init.css @@ -223,6 +223,10 @@ svg.xy-flow__connectionline { min-width: 5px; min-height: 5px; + &.connectingfrom { + pointer-events: all; + } + &.connectionindicator { pointer-events: all; cursor: crosshair; diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 4ddb5313..4f99a97c 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -15,7 +15,7 @@ import { type ConnectionHandle, } from '../types'; -import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType, resetRecentHandle } from './utils'; +import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; export type OnPointerDownParams = { autoPanOnConnect: boolean; @@ -107,7 +107,6 @@ function onPointerDown( return; } - let prevActiveHandle: Element; let connectionPosition = getEventPosition(event, containerBounds); let autoPanStarted = false; let connection: Connection | null = null; @@ -194,18 +193,6 @@ function onPointerDown( connectionStatus: getConnectionStatus(!!closestHandle, isValid), connectionEndHandle: result.endHandle, }); - - if (!closestHandle && !isValid && !handleDomNode) { - return resetRecentHandle(prevActiveHandle, lib); - } - - if (connection?.source !== connection?.target && handleDomNode) { - resetRecentHandle(prevActiveHandle, lib); - prevActiveHandle = handleDomNode; - handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`); - handleDomNode.classList.toggle('valid', isValid); - handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid); - } } function onPointerUp(event: MouseEvent | TouchEvent) { @@ -221,7 +208,6 @@ function onPointerDown( onEdgeUpdateEnd?.(event); } - resetRecentHandle(prevActiveHandle, lib); cancelConnection(); cancelAnimationFrame(autoPanId); autoPanStarted = false; diff --git a/packages/system/src/xyhandle/utils.ts b/packages/system/src/xyhandle/utils.ts index b829f691..8c82f6e3 100644 --- a/packages/system/src/xyhandle/utils.ts +++ b/packages/system/src/xyhandle/utils.ts @@ -38,7 +38,7 @@ export function getClosestHandle( let closestHandles: ConnectionHandle[] = []; let minDistance = Infinity; - handles.forEach((handle) => { + for (const handle of handles) { const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2)); if (distance <= connectionRadius) { if (distance < minDistance) { @@ -49,7 +49,7 @@ export function getClosestHandle( } minDistance = distance; } - }); + } if (!closestHandles.length) { return null; @@ -101,10 +101,6 @@ export function getHandleType( return null; } -export function resetRecentHandle(handleDomNode: Element, lib: string): void { - handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`); -} - export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { let connectionStatus = null;