Merge pull request #5344 from xyflow/feat/connection-drag-threshold
Add connectionDragThreshold
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/react': minor
|
||||||
|
'@xyflow/svelte': minor
|
||||||
|
'@xyflow/system': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Add connectionDragThreshold prop
|
||||||
@@ -36,6 +36,7 @@ const ConnectionLineFlow = () => {
|
|||||||
onEdgesChange={onEdgesChange}
|
onEdgesChange={onEdgesChange}
|
||||||
connectionLineComponent={ConnectionLine}
|
connectionLineComponent={ConnectionLine}
|
||||||
onConnect={onConnect}
|
onConnect={onConnect}
|
||||||
|
connectionDragThreshold={25}
|
||||||
>
|
>
|
||||||
<Background variant={BackgroundVariant.Lines} />
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
|
|||||||
@@ -24,7 +24,14 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div style="height:100vh;">
|
<div style="height:100vh;">
|
||||||
<SvelteFlow bind:nodes bind:edges {nodeTypes} fitView connectionLineComponent={ConnectionLine}>
|
<SvelteFlow
|
||||||
|
bind:nodes
|
||||||
|
bind:edges
|
||||||
|
{nodeTypes}
|
||||||
|
fitView
|
||||||
|
connectionLineComponent={ConnectionLine}
|
||||||
|
connectionDragThreshold={100}
|
||||||
|
>
|
||||||
<Background variant={BackgroundVariant.Lines} />
|
<Background variant={BackgroundVariant.Lines} />
|
||||||
</SvelteFlow>
|
</SvelteFlow>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
||||||
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
|
import {
|
||||||
|
XYHandle,
|
||||||
|
type Connection,
|
||||||
|
EdgePosition,
|
||||||
|
FinalConnectionState,
|
||||||
|
HandleType,
|
||||||
|
OnConnectStart,
|
||||||
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
import { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||||
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
||||||
@@ -60,15 +67,17 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
} = store.getState();
|
} = store.getState();
|
||||||
const isTarget = oppositeHandle.type === 'target';
|
const isTarget = oppositeHandle.type === 'target';
|
||||||
|
|
||||||
setReconnecting(true);
|
|
||||||
onReconnectStart?.(event, edge, oppositeHandle.type);
|
|
||||||
|
|
||||||
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
|
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
|
||||||
setReconnecting(false);
|
setReconnecting(false);
|
||||||
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
||||||
|
const _onConnectStart: OnConnectStart = (_event, params) => {
|
||||||
|
setReconnecting(true);
|
||||||
|
onReconnectStart?.(event, edge, oppositeHandle.type);
|
||||||
|
onConnectStart?.(_event, params);
|
||||||
|
};
|
||||||
|
|
||||||
XYHandle.onPointerDown(event.nativeEvent, {
|
XYHandle.onPointerDown(event.nativeEvent, {
|
||||||
autoPanOnConnect,
|
autoPanOnConnect,
|
||||||
@@ -86,12 +95,13 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
panBy,
|
panBy,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
onConnect: onConnectEdge,
|
onConnect: onConnectEdge,
|
||||||
onConnectStart,
|
onConnectStart: _onConnectStart,
|
||||||
onConnectEnd,
|
onConnectEnd,
|
||||||
onReconnectEnd: _onReconnectEnd,
|
onReconnectEnd: _onReconnectEnd,
|
||||||
updateConnection,
|
updateConnection,
|
||||||
getTransform: () => store.getState().transform,
|
getTransform: () => store.getState().transform,
|
||||||
getFromHandle: () => store.getState().connection.fromHandle,
|
getFromHandle: () => store.getState().connection.fromHandle,
|
||||||
|
dragThreshold: store.getState().connectionDragThreshold,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ function HandleComponent(
|
|||||||
getTransform: () => store.getState().transform,
|
getTransform: () => store.getState().transform,
|
||||||
getFromHandle: () => store.getState().connection.fromHandle,
|
getFromHandle: () => store.getState().connection.fromHandle,
|
||||||
autoPanSpeed: currentStore.autoPanSpeed,
|
autoPanSpeed: currentStore.autoPanSpeed,
|
||||||
|
dragThreshold: currentStore.connectionDragThreshold,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const reactFlowFieldsToTrack = [
|
|||||||
'isValidConnection',
|
'isValidConnection',
|
||||||
'selectNodesOnDrag',
|
'selectNodesOnDrag',
|
||||||
'nodeDragThreshold',
|
'nodeDragThreshold',
|
||||||
|
'connectionDragThreshold',
|
||||||
'onBeforeDelete',
|
'onBeforeDelete',
|
||||||
'debug',
|
'debug',
|
||||||
'autoPanSpeed',
|
'autoPanSpeed',
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
style,
|
style,
|
||||||
id,
|
id,
|
||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
|
connectionDragThreshold,
|
||||||
viewport,
|
viewport,
|
||||||
onViewportChange,
|
onViewportChange,
|
||||||
width,
|
width,
|
||||||
@@ -306,6 +307,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
isValidConnection={isValidConnection}
|
isValidConnection={isValidConnection}
|
||||||
selectNodesOnDrag={selectNodesOnDrag}
|
selectNodesOnDrag={selectNodesOnDrag}
|
||||||
nodeDragThreshold={nodeDragThreshold}
|
nodeDragThreshold={nodeDragThreshold}
|
||||||
|
connectionDragThreshold={connectionDragThreshold}
|
||||||
onBeforeDelete={onBeforeDelete}
|
onBeforeDelete={onBeforeDelete}
|
||||||
paneClickDistance={paneClickDistance}
|
paneClickDistance={paneClickDistance}
|
||||||
debug={debug}
|
debug={debug}
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const getInitialState = ({
|
|||||||
noPanClassName: 'nopan',
|
noPanClassName: 'nopan',
|
||||||
nodeOrigin: storeNodeOrigin,
|
nodeOrigin: storeNodeOrigin,
|
||||||
nodeDragThreshold: 1,
|
nodeDragThreshold: 1,
|
||||||
|
connectionDragThreshold: 1,
|
||||||
|
|
||||||
snapGrid: [15, 15],
|
snapGrid: [15, 15],
|
||||||
snapToGrid: false,
|
snapToGrid: false,
|
||||||
|
|||||||
@@ -658,6 +658,12 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
|
|||||||
* @default 1
|
* @default 1
|
||||||
*/
|
*/
|
||||||
nodeDragThreshold?: number;
|
nodeDragThreshold?: number;
|
||||||
|
/**
|
||||||
|
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||||
|
* This is useful to prevent accidental connections when clicking on a handle.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
connectionDragThreshold?: number;
|
||||||
/** Sets a fixed width for the flow. */
|
/** Sets a fixed width for the flow. */
|
||||||
width?: number;
|
width?: number;
|
||||||
/** Sets a fixed height for the flow. */
|
/** Sets a fixed height for the flow. */
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
|||||||
nodeExtent: CoordinateExtent;
|
nodeExtent: CoordinateExtent;
|
||||||
nodeOrigin: NodeOrigin;
|
nodeOrigin: NodeOrigin;
|
||||||
nodeDragThreshold: number;
|
nodeDragThreshold: number;
|
||||||
|
connectionDragThreshold: number;
|
||||||
|
|
||||||
nodesSelectionActive: boolean;
|
nodesSelectionActive: boolean;
|
||||||
userSelectionActive: boolean;
|
userSelectionActive: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
import type { Edge } from '$lib/types';
|
import type { Edge } from '$lib/types';
|
||||||
import { XYHandle, type HandleType } from '@xyflow/system';
|
import { XYHandle, type HandleType, type OnConnectStart } from '@xyflow/system';
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import { EdgeLabel } from '../EdgeLabel';
|
import { EdgeLabel } from '../EdgeLabel';
|
||||||
import type { EdgeReconnectAnchorProps } from './types';
|
import type { EdgeReconnectAnchorProps } from './types';
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
position,
|
position,
|
||||||
class: className,
|
class: className,
|
||||||
size = 25,
|
size = 25,
|
||||||
|
dragThreshold = 1,
|
||||||
children,
|
children,
|
||||||
...rest
|
...rest
|
||||||
}: EdgeReconnectAnchorProps = $props();
|
}: EdgeReconnectAnchorProps = $props();
|
||||||
@@ -52,8 +53,11 @@
|
|||||||
let newEdge: Edge | undefined;
|
let newEdge: Edge | undefined;
|
||||||
let edge = edgeLookup.get(edgeId)!;
|
let edge = edgeLookup.get(edgeId)!;
|
||||||
|
|
||||||
reconnecting = true;
|
const _onConnectStart: OnConnectStart = (evt, params) => {
|
||||||
onreconnectstart?.(event, edge, type);
|
reconnecting = true;
|
||||||
|
onreconnectstart?.(event, edge, type);
|
||||||
|
onconnectstart?.(evt, params);
|
||||||
|
};
|
||||||
|
|
||||||
const opposite =
|
const opposite =
|
||||||
type === 'target'
|
type === 'target'
|
||||||
@@ -79,7 +83,7 @@
|
|||||||
cancelConnection,
|
cancelConnection,
|
||||||
panBy,
|
panBy,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
onConnectStart: onconnectstart,
|
onConnectStart: _onConnectStart,
|
||||||
onConnectEnd: onconnectend,
|
onConnectEnd: onconnectend,
|
||||||
onConnect: (connection) => {
|
onConnect: (connection) => {
|
||||||
newEdge = { ...edge, ...connection };
|
newEdge = { ...edge, ...connection };
|
||||||
@@ -97,7 +101,8 @@
|
|||||||
},
|
},
|
||||||
updateConnection,
|
updateConnection,
|
||||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||||
getFromHandle: () => store.connection.fromHandle
|
getFromHandle: () => store.connection.fromHandle,
|
||||||
|
dragThreshold: dragThreshold ?? store.connectionDragThreshold
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export type EdgeReconnectAnchorProps = {
|
|||||||
position?: XYPosition;
|
position?: XYPosition;
|
||||||
size?: number;
|
size?: number;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
|
dragThreshold?: number;
|
||||||
} & HTMLAttributes<HTMLDivElement>;
|
} & HTMLAttributes<HTMLDivElement>;
|
||||||
|
|||||||
@@ -139,7 +139,8 @@
|
|||||||
store.onconnectend?.(event, connectionState);
|
store.onconnectend?.(event, connectionState);
|
||||||
},
|
},
|
||||||
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
|
||||||
getFromHandle: () => store.connection.fromHandle
|
getFromHandle: () => store.connection.fromHandle,
|
||||||
|
dragThreshold: store.connectionDragThreshold
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@
|
|||||||
fitViewOptions,
|
fitViewOptions,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
nodeDragThreshold,
|
nodeDragThreshold,
|
||||||
|
connectionDragThreshold,
|
||||||
minZoom,
|
minZoom,
|
||||||
maxZoom,
|
maxZoom,
|
||||||
initialViewport,
|
initialViewport,
|
||||||
|
|||||||
@@ -170,6 +170,12 @@ export type SvelteFlowProps<
|
|||||||
* @default 0
|
* @default 0
|
||||||
*/
|
*/
|
||||||
nodeClickDistance?: number;
|
nodeClickDistance?: number;
|
||||||
|
/**
|
||||||
|
* The threshold in pixels that the mouse must move before a connection line starts to drag.
|
||||||
|
* This is useful to prevent accidental connections when clicking on a handle.
|
||||||
|
* @default 1
|
||||||
|
*/
|
||||||
|
connectionDragThreshold?: number;
|
||||||
/** Minimum zoom level
|
/** Minimum zoom level
|
||||||
* @default 0.5
|
* @default 0.5
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ export function getInitialStore<NodeType extends Node = Node, EdgeType extends E
|
|||||||
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
|
||||||
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
|
||||||
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
|
autoPanOnNodeFocus: boolean = $derived(signals.props.autoPanOnNodeFocus ?? true);
|
||||||
|
connectionDragThreshold: number = $derived(signals.props.connectionDragThreshold ?? 1);
|
||||||
|
|
||||||
fitViewQueued: boolean = signals.props.fitView ?? false;
|
fitViewQueued: boolean = signals.props.fitView ?? false;
|
||||||
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
|
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function onPointerDown(
|
|||||||
getTransform,
|
getTransform,
|
||||||
getFromHandle,
|
getFromHandle,
|
||||||
autoPanSpeed,
|
autoPanSpeed,
|
||||||
|
dragThreshold = 1,
|
||||||
}: OnPointerDownParams
|
}: OnPointerDownParams
|
||||||
) {
|
) {
|
||||||
// when xyflow is used inside a shadow root we can't use document
|
// when xyflow is used inside a shadow root we can't use document
|
||||||
@@ -56,6 +57,7 @@ function onPointerDown(
|
|||||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||||
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
||||||
const containerBounds = domNode?.getBoundingClientRect();
|
const containerBounds = domNode?.getBoundingClientRect();
|
||||||
|
let connectionStarted = false;
|
||||||
|
|
||||||
if (!containerBounds || !handleType) {
|
if (!containerBounds || !handleType) {
|
||||||
return;
|
return;
|
||||||
@@ -92,10 +94,9 @@ function onPointerDown(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||||
|
|
||||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||||
|
|
||||||
const newConnection: ConnectionInProgress = {
|
let previousConnection: ConnectionInProgress = {
|
||||||
inProgress: true,
|
inProgress: true,
|
||||||
isValid: null,
|
isValid: null,
|
||||||
|
|
||||||
@@ -110,12 +111,31 @@ function onPointerDown(
|
|||||||
toNode: null,
|
toNode: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
updateConnection(newConnection);
|
function startConnection() {
|
||||||
let previousConnection: ConnectionInProgress = newConnection;
|
updateConnection(previousConnection);
|
||||||
|
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||||
|
}
|
||||||
|
|
||||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
if (dragThreshold === 0) {
|
||||||
|
startConnection();
|
||||||
|
}
|
||||||
|
|
||||||
function onPointerMove(event: MouseEvent | TouchEvent) {
|
function onPointerMove(event: MouseEvent | TouchEvent) {
|
||||||
|
if (!connectionStarted) {
|
||||||
|
const { x: evtX, y: evtY } = getEventPosition(event);
|
||||||
|
const dx = evtX - x;
|
||||||
|
const dy = evtY - y;
|
||||||
|
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
|
||||||
|
|
||||||
|
if (!nextConnectionStarted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startConnection();
|
||||||
|
|
||||||
|
connectionStarted = nextConnectionStarted;
|
||||||
|
}
|
||||||
|
|
||||||
if (!getFromHandle() || !fromHandle) {
|
if (!getFromHandle() || !fromHandle) {
|
||||||
onPointerUp(event);
|
onPointerUp(event);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export type OnPointerDownParams = {
|
|||||||
getTransform: () => Transform;
|
getTransform: () => Transform;
|
||||||
getFromHandle: () => Handle | null;
|
getFromHandle: () => Handle | null;
|
||||||
autoPanSpeed?: number;
|
autoPanSpeed?: number;
|
||||||
|
dragThreshold?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IsValidParams = {
|
export type IsValidParams = {
|
||||||
|
|||||||
Reference in New Issue
Block a user