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,9 +1,24 @@
<script lang="ts"> <script lang="ts">
import { useConnection } from '@xyflow/svelte'; import { getBezierPath, useConnection } from '@xyflow/svelte';
const connection = useConnection(); const connection = useConnection();
let path: string | null = null;
$: if ($connection.inProgress) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
[path] = getBezierPath(pathParams);
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<path d={$connection.path} fill="none" stroke={$connection.startHandle?.handleId} /> <path d={path} fill="none" stroke={$connection.fromHandle.id} />
{/if} {/if}
@@ -1,140 +1,17 @@
import { CSSProperties, useCallback } from 'react'; import { CSSProperties } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { import {
Position,
ConnectionLineType, ConnectionLineType,
ConnectionMode,
getBezierPath, getBezierPath,
getSmoothStepPath, getSmoothStepPath,
type HandleType, getConnectionStatus,
getStraightPath,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types'; import type { ConnectionLineComponent, ReactFlowState } 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';
type ConnectionLineWrapperProps = { type ConnectionLineWrapperProps = {
type: ConnectionLineType; type: ConnectionLineType;
@@ -144,19 +21,18 @@ type ConnectionLineWrapperProps = {
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodeId: s.connection.fromHandle?.nodeId,
handleType: s.connection.fromHandle?.type,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
isValid: s.connection.isValid, isValid: s.connection.isValid,
inProgress: s.connection.inProgress,
width: s.width, width: s.width,
height: s.height, height: s.height,
}); });
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height, isValid } = useStore(selector, shallow); const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const isIncorrect = !(nodeId && handleType && width && nodesConnectable); const renderConnection = !!(width && nodesConnectable && inProgress);
if (isIncorrect) { if (!renderConnection) {
return null; return null;
} }
@@ -168,15 +44,81 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container" className="react-flow__connectionline react-flow__container"
> >
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}> <g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine <ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
isValid={isValid}
/>
</g> </g>
</svg> </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 { 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 { return {
connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.handleId === handleId && fromHandle?.type === type, connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.id === handleId && fromHandle?.type === type,
connectingTo, connectingTo,
clickConnecting: clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.id === handleId && clickHandle?.type === type,
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
isPossibleEndHandle: isPossibleEndHandle:
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type ? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.handleId, : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!fromHandle, connectionInProcess: !!fromHandle,
valid: connectingTo && isValid, valid: connectingTo && isValid,
}; };
@@ -167,7 +166,7 @@ function HandleComponent(
if (!connectionClickStartHandle) { if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); store.setState({ connectionClickStartHandle: { nodeId, type, id: handleId } });
return; return;
} }
@@ -181,7 +180,7 @@ function HandleComponent(
}, },
connectionMode, connectionMode,
fromNodeId: connectionClickStartHandle.nodeId, fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null, fromHandleId: connectionClickStartHandle.id || null,
fromType: connectionClickStartHandle.type, fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler, isValidConnection: isValidConnectionHandler,
flowId, flowId,
+1 -2
View File
@@ -6,7 +6,6 @@ import { ConnectionState } from '@xyflow/system';
const selector = (s: ReactFlowStore) => ({ const selector = (s: ReactFlowStore) => ({
...s.connection, ...s.connection,
inProgress: s.connection.fromHandle !== null,
}); });
/** /**
@@ -15,6 +14,6 @@ const selector = (s: ReactFlowStore) => ({
* @public * @public
* @returns ongoing connection * @returns ongoing connection
*/ */
export function useConnection(): ConnectionState & { inProgress: boolean } { export function useConnection(): ConnectionState {
return useStore(selector, shallow); return useStore(selector, shallow);
} }
+4 -18
View File
@@ -12,6 +12,7 @@ import {
EdgeSelectionChange, EdgeSelectionChange,
NodeSelectionChange, NodeSelectionChange,
ParentExpandChild, ParentExpandChild,
initialConnection,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -308,27 +309,12 @@ const createStore = ({
); );
}, },
cancelConnection: () => { cancelConnection: () => {
const { connection } = get();
set({ set({
connection: { connection: { ...initialConnection },
position: connection.position,
fromHandle: null,
toHandle: null,
isValid: null,
},
}); });
}, },
updateConnection: (params) => { updateConnection: (connection) => {
const { connection } = get(); set({ connection });
const currentConnection = {
connection: {
...params,
position: params.position ?? connection.position,
},
};
set(currentConnection);
}, },
reset: () => set({ ...getInitialState() }), reset: () => set({ ...getInitialState() }),
+2 -6
View File
@@ -7,6 +7,7 @@ import {
updateConnectionLookup, updateConnectionLookup,
devWarn, devWarn,
getInternalNodesBounds, getInternalNodesBounds,
initialConnection,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
@@ -101,12 +102,7 @@ const getInitialState = ({
multiSelectionActive: false, multiSelectionActive: false,
connection: { connection: { ...initialConnection },
fromHandle: null,
toHandle: null,
position: { x: 0, y: 0 },
isValid: null,
},
connectionClickStartHandle: null, connectionClickStartHandle: null,
connectOnClick: true, connectOnClick: true,
+5 -3
View File
@@ -10,7 +10,7 @@ import type {
HandleType, HandleType,
Connection, Connection,
ConnectionLineType, ConnectionLineType,
HandleElement, Handle,
EdgePosition, EdgePosition,
StepPathOptions, StepPathOptions,
OnError, OnError,
@@ -193,8 +193,8 @@ export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newC
export type ConnectionLineComponentProps = { export type ConnectionLineComponentProps = {
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
fromNode?: Node; fromNode: Node;
fromHandle?: HandleElement; fromHandle: Handle;
fromX: number; fromX: number;
fromY: number; fromY: number;
toX: number; toX: number;
@@ -202,6 +202,8 @@ export type ConnectionLineComponentProps = {
fromPosition: Position; fromPosition: Position;
toPosition: Position; toPosition: Position;
connectionStatus: 'valid' | 'invalid' | null; connectionStatus: 'valid' | 'invalid' | null;
toNode: Node | null;
toHandle: Handle | null;
}; };
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
+2 -2
View File
@@ -10,7 +10,7 @@ import {
type OnViewportChange, type OnViewportChange,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type ConnectingHandle, type Handle,
type Transform, type Transform,
type PanZoomInstance, type PanZoomInstance,
type PanBy, type PanBy,
@@ -79,7 +79,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
connection: ConnectionState; connection: ConnectionState;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
connectionClickStartHandle: ConnectingHandle | null; connectionClickStartHandle: (Pick<Handle, 'nodeId' | 'id'> & Required<Pick<Handle, 'type'>>) | null;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: SnapGrid; snapGrid: SnapGrid;
@@ -2,21 +2,59 @@
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import {
ConnectionLineType,
getBezierPath,
getConnectionStatus,
getSmoothStepPath,
getStraightPath
} from '@xyflow/system';
export let containerStyle: string = ''; export let containerStyle: string = '';
export let style: string = ''; export let style: string = '';
export let isCustomComponent: boolean = false; export let isCustomComponent: boolean = false;
const { width, height, connection } = useStore(); const { width, height, connection, connectionLineType } = useStore();
let path: string | null = null;
$: if ($connection.inProgress && !isCustomComponent) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
switch ($connectionLineType) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', $connection.status])}> <g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
<slot name="connectionLine" /> <slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent --> <!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent} {#if !isCustomComponent}
<path d={$connection.path} {style} fill="none" class="svelte-flow__connection-path" /> <path d={path} {style} fill="none" class="svelte-flow__connection-path" />
{/if} {/if}
</g> </g>
</svg> </svg>
@@ -103,7 +103,7 @@
$onConnectEndAction?.(event); $onConnectEndAction?.(event);
}, },
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getFromHandle: () => $connection.startHandle getFromHandle: () => $connection.fromHandle
}); });
} }
} }
@@ -128,21 +128,20 @@
prevConnections = connections ?? new Map(); prevConnections = connections ?? new Map();
} }
$: connectionInProcess = !!$connection.startHandle; $: connectionInProcess = !!$connection.fromHandle;
$: connectingFrom = $: connectingFrom =
$connection.startHandle?.nodeId === nodeId && $connection.fromHandle?.nodeId === nodeId &&
$connection.startHandle?.type === type && $connection.fromHandle?.type === type &&
$connection.startHandle?.handleId === handleId; $connection.fromHandle?.id === handleId;
$: connectingTo = $: connectingTo =
$connection.endHandle?.nodeId === nodeId && $connection.toHandle?.nodeId === nodeId &&
$connection.endHandle?.type === type && $connection.toHandle?.type === type &&
$connection.endHandle?.handleId === handleId; $connection.toHandle?.id === handleId;
$: isPossibleEndHandle = $: isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict $connectionMode === ConnectionMode.Strict
? $connection.startHandle?.type !== type ? $connection.fromHandle?.type !== type
: nodeId !== $connection.startHandle?.nodeId || : nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
handleId !== $connection.startHandle?.handleId; $: valid = connectingTo && $connection.isValid;
$: valid = connectingTo && $connection.status === 'valid';
</script> </script>
<!-- <!--
@@ -1,7 +1,7 @@
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { ConnectionProps } from '$lib/store/derived-connection-props';
import type { ConnectionState } from '@xyflow/system';
/** /**
* Hook for receiving the current connection. * Hook for receiving the current connection.
@@ -9,7 +9,7 @@ import type { ConnectionProps } from '$lib/store/derived-connection-props';
* @public * @public
* @returns current connection as a readable store * @returns current connection as a readable store
*/ */
export function useConnection(): Readable<ConnectionProps> { export function useConnection(): Readable<ConnectionState> {
const { connection } = useStore(); const { connection } = useStore();
return connection; return connection;
@@ -1,130 +0,0 @@
import { derived, type Writable } from 'svelte/store';
import {
getBezierPath,
getSmoothStepPath,
getStraightPath,
ConnectionLineType,
ConnectionMode,
Position,
type HandleElement
} from '@xyflow/system';
import type { SvelteFlowStoreState } from './types';
import type { ConnectionData } from '$lib/types';
export type ConnectionProps = {
path: string | null;
sourceX: number | null;
sourceY: number | null;
sourcePosition: Position | undefined | null;
targetX: number | null;
targetY: number | null;
targetPosition: Position | undefined | null;
pointerPosition: ConnectionData['connectionPosition'] | null;
startHandle: ConnectionData['connectionStartHandle'] | null;
endHandle: ConnectionData['connectionEndHandle'] | null;
status: ConnectionData['connectionStatus'] | null;
};
export const initConnectionProps = {
path: null,
sourceX: null,
sourceY: null,
sourcePosition: null,
targetX: null,
targetY: null,
targetPosition: null,
pointerPosition: null,
startHandle: null,
endHandle: null,
status: null
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top
};
export function getDerivedConnectionProps(
store: SvelteFlowStoreState,
currentConnection: Writable<ConnectionData>
) {
return derived(
[
currentConnection,
store.connectionLineType,
store.connectionMode,
store.nodeLookup,
store.viewport
],
([connection, connectionLineType, connectionMode, nodeLookup, viewport]) => {
if (!connection.connectionStartHandle?.nodeId) {
return initConnectionProps;
}
// TODO: it should bail out if the node is not found
const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId);
const fromHandleBounds = fromNode?.internals.handleBounds;
const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[
connection?.connectionStartHandle?.type === 'source' ? 'target' : 'source'
];
const handleBounds =
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.connectionStartHandle?.handleId
? handleBounds?.find((d) => d.id === connection.connectionStartHandle?.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 ?? 0) + fromHandleX;
const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition =
connection.connectionEndHandle?.position ??
(fromPosition ? oppositePosition[fromPosition] : undefined);
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: ((connection.connectionPosition?.x ?? 0) - viewport.x) / viewport.zoom,
targetY: ((connection.connectionPosition?.y ?? 0) - viewport.y) / viewport.zoom,
targetPosition: toPosition
};
let path = '';
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
}
return {
path,
...pathParams,
pointerPosition: connection.connectionPosition,
startHandle: connection.connectionStartHandle,
endHandle: connection.connectionEndHandle,
status: connection.connectionStatus
};
}
);
}
+6 -22
View File
@@ -1,5 +1,5 @@
import { getContext, setContext } from 'svelte'; import { getContext, setContext } from 'svelte';
import { derived, get, writable } from 'svelte/store'; import { derived, get } from 'svelte/store';
import { import {
createMarkerIds, createMarkerIds,
fitView as fitViewSystem, fitView as fitViewSystem,
@@ -7,6 +7,8 @@ import {
panBy as panBySystem, panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem, updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil, addEdge as addEdgeUtil,
initialConnection,
errorMessages,
type UpdateNodePositions, type UpdateNodePositions,
type InternalNodeUpdate, type InternalNodeUpdate,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
@@ -14,17 +16,15 @@ import {
type XYPosition, type XYPosition,
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
errorMessages,
type ConnectionState type ConnectionState
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getVisibleEdges } from './visible-edges'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props';
export const key = Symbol(); export const key = Symbol();
@@ -331,27 +331,12 @@ export function createStore({
}); });
} }
const initConnectionUpdateData = {
connectionStartHandle: null,
connectionEndHandle: null,
connectionPosition: null,
connectionStatus: null
};
// by creating an internal, unexposed store and using a derived store
// we prevent using slow get() calls
const currentConnection = writable<ConnectionData>(initConnectionUpdateData);
const updateConnection: UpdateConnection = (newConnection: ConnectionState) => { const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
currentConnection.set({ store.connection.set({ ...newConnection });
connectionStartHandle: newConnection.fromHandle,
connectionEndHandle: newConnection.toHandle,
connectionPosition: newConnection.position,
connectionStatus: newConnection.isValid ? 'valid' : 'invalid'
});
}; };
function cancelConnection() { function cancelConnection() {
currentConnection.set(initConnectionUpdateData); store.connection.set(initialConnection);
} }
function reset() { function reset() {
@@ -370,7 +355,6 @@ export function createStore({
...store, ...store,
// derived state // derived state
connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store), visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
markers: derived( markers: derived(
@@ -9,6 +9,7 @@ import {
getNodesBounds, getNodesBounds,
getViewportForBounds, getViewportForBounds,
updateConnectionLookup, updateConnectionLookup,
initialConnection,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
@@ -22,7 +23,8 @@ import {
type OnConnectStart, type OnConnectStart,
type OnConnectEnd, type OnConnectEnd,
type NodeLookup, type NodeLookup,
type EdgeLookup type EdgeLookup,
type ConnectionState
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +53,6 @@ import type {
InternalNode InternalNode
} from '$lib/types'; } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils'; import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
export const initialNodeTypes = { export const initialNodeTypes = {
input: InputNode, input: InputNode,
@@ -142,7 +143,7 @@ export const getInitialStore = ({
viewport: writable<Viewport>(viewport), viewport: writable<Viewport>(viewport),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict), connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null), domNode: writable<HTMLDivElement | null>(null),
connection: readable<ConnectionProps>(initConnectionProps), connection: writable<ConnectionState>(initialConnection),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier), connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
connectionRadius: writable<number>(20), connectionRadius: writable<number>(20),
isValidConnection: writable<IsValidConnection>(() => true), isValidConnection: writable<IsValidConnection>(() => true),
+3 -3
View File
@@ -2,7 +2,7 @@ import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { import type {
FitViewOptionsBase, FitViewOptionsBase,
XYPosition, XYPosition,
ConnectingHandle, Handle,
Connection, Connection,
OnBeforeDeleteBase, OnBeforeDeleteBase,
HandleProps as HandlePropsSystem HandleProps as HandlePropsSystem
@@ -17,8 +17,8 @@ export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = { export type ConnectionData = {
connectionPosition: XYPosition | null; connectionPosition: XYPosition | null;
connectionStartHandle: ConnectingHandle | null; connectionStartHandle: Handle | null;
connectionEndHandle: ConnectingHandle | null; connectionEndHandle: Handle | null;
connectionStatus: string | null; connectionStatus: string | null;
}; };
+37 -8
View File
@@ -6,9 +6,9 @@ import type { ZoomBehavior } from 'd3-zoom';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Transition } from 'd3-transition'; import type { Transition } from 'd3-transition';
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect, Position } from './utils';
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
import type { ConnectingHandle, HandleType } from './handles'; import type { Handle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..'; import { EdgeBase } from '..';
@@ -133,21 +133,50 @@ export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void; export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean; export type PanBy = (delta: XYPosition) => boolean;
export type NoConnectionInProgress = { export const initialConnection: NoConnection = {
position: XYPosition; inProgress: false,
isValid: null,
from: null,
fromHandle: null,
fromPosition: null,
fromNode: null,
to: null,
toHandle: null,
toPosition: null,
toNode: null,
};
export type NoConnection = {
inProgress: false;
isValid: null; isValid: null;
from: null;
fromHandle: null; fromHandle: null;
fromPosition: null;
fromNode: null;
to: null;
toHandle: null; toHandle: null;
toPosition: null;
toNode: null;
}; };
export type ConnectionInProgress = { export type ConnectionInProgress = {
position: XYPosition; inProgress: true;
isValid: boolean | null; isValid: boolean | null;
fromHandle: ConnectingHandle;
toHandle: ConnectingHandle | null; from: XYPosition;
fromHandle: Handle;
fromPosition: Position;
fromNode: NodeBase;
to: XYPosition;
toHandle: Handle | null;
toPosition: Position;
toNode: NodeBase | null;
}; };
export type ConnectionState = ConnectionInProgress | NoConnectionInProgress; export type ConnectionState = ConnectionInProgress | NoConnection;
export type UpdateConnection = (params: ConnectionState) => void; export type UpdateConnection = (params: ConnectionState) => void;
+4 -18
View File
@@ -2,29 +2,15 @@ import type { Position, IsValidConnection } from '.';
export type HandleType = 'source' | 'target'; export type HandleType = 'source' | 'target';
export type HandleElement = { export type Handle = {
id?: string | null; id?: string | null;
nodeId: string;
x: number; x: number;
y: number; y: number;
position: Position;
type: HandleType;
width: number; width: number;
height: number; height: number;
position: Position;
type?: HandleType;
};
export type ConnectingHandle = {
nodeId: string;
type: HandleType;
handleId?: string | null;
position?: Position | null;
};
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
}; };
export type HandleProps = { export type HandleProps = {
+4 -4
View File
@@ -1,4 +1,4 @@
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
import { Optional } from '../utils/types'; import { Optional } from '../utils/types';
/** /**
@@ -110,8 +110,8 @@ export type NodeProps<NodeType extends NodeBase> = Pick<
}; };
export type NodeHandleBounds = { export type NodeHandleBounds = {
source: HandleElement[] | null; source: Handle[] | null;
target: HandleElement[] | null; target: Handle[] | null;
}; };
export type InternalNodeUpdate = { export type InternalNodeUpdate = {
@@ -148,7 +148,7 @@ export type NodeOrigin = [number, number];
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
+7
View File
@@ -5,6 +5,13 @@ export enum Position {
Bottom = 'bottom', Bottom = 'bottom',
} }
export const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
export type XYPosition = { export type XYPosition = {
x: number; x: number;
y: number; y: number;
+4
View File
@@ -51,3 +51,7 @@ export function handleConnectionChange(
cb(diff); cb(diff);
} }
} }
export function getConnectionStatus(isValid: boolean | null) {
return isValid === null ? null : isValid ? 'valid' : 'invalid';
}
+8 -5
View File
@@ -1,4 +1,4 @@
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types'; import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, Handle, Position } from '../types';
import { snapPosition, pointToRendererPoint } from './general'; import { snapPosition, pointToRendererPoint } from './general';
export type GetPointerPositionParams = { export type GetPointerPositionParams = {
@@ -59,13 +59,14 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
// We store them in the internals object of the node in order to avoid // We store them in the internals object of the node in order to avoid
// unnecessary recalculations. // unnecessary recalculations.
export const getHandleBounds = ( export const getHandleBounds = (
selector: string, type: 'source' | 'target',
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
nodeBounds: DOMRect, nodeBounds: DOMRect,
zoom: number, zoom: number,
nodeId: string,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin = [0, 0]
): HandleElement[] | null => { ): Handle[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(`.${type}`);
if (!handles || !handles.length) { if (!handles || !handles.length) {
return null; return null;
@@ -78,11 +79,13 @@ export const getHandleBounds = (
y: nodeBounds.top + nodeBounds.height * nodeOrigin[1], y: nodeBounds.top + nodeBounds.height * nodeOrigin[1],
}; };
return handlesArray.map((handle): HandleElement => { return handlesArray.map((handle): Handle => {
const handleBounds = handle.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect();
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
type,
nodeId,
position: handle.getAttribute('data-handlepos') as unknown as Position, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeOffset.x) / zoom, x: (handleBounds.left - nodeOffset.x) / zoom,
y: (handleBounds.top - nodeOffset.y) / zoom, y: (handleBounds.top - nodeOffset.y) / zoom,
+24 -18
View File
@@ -1,9 +1,9 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { InternalNodeBase, NodeHandle } from '../../types/nodes'; import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils'; import { Position, XYPosition } from '../../types/utils';
import { errorMessages } from '../../constants'; import { errorMessages } from '../../constants';
import { HandleElement } from '../../types'; import { Handle } from '../../types';
import { getNodeDimensions } from '../general'; import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = { export type GetEdgePositionParams = {
@@ -58,14 +58,14 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
const [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition); const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition); const target = getHandlePosition(targetNode, targetHandle, targetPosition);
return { return {
sourceX, sourceX: source.x,
sourceY, sourceY: source.y,
targetX, targetX: target.x,
targetY, targetY: target.y,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
}; };
@@ -84,9 +84,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
handle.height = handle.height ?? 1; handle.height = handle.height ?? 1;
if (handle.type === 'source') { if (handle.type === 'source') {
source.push(handle as HandleElement); source.push(handle as Handle);
} else if (handle.type === 'target') { } else if (handle.type === 'target') {
target.push(handle as HandleElement); target.push(handle as Handle);
} }
} }
@@ -98,27 +98,33 @@ function toHandleBounds(handles?: NodeHandle[]) {
export function getHandlePosition( export function getHandlePosition(
node: InternalNodeBase, node: InternalNodeBase,
handle: HandleElement | null, handle: Handle | null,
fallbackPosition: Position = Position.Left fallbackPosition: Position = Position.Left,
): number[] { center = false
): XYPosition {
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x; const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y; const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
if (center) {
return { x: x + width / 2, y: y + height / 2 };
}
const position = handle?.position ?? fallbackPosition; const position = handle?.position ?? fallbackPosition;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return [x + width / 2, y]; return { x: x + width / 2, y };
case Position.Right: case Position.Right:
return [x + width, y + height / 2]; return { x: x + width, y: y + height / 2 };
case Position.Bottom: case Position.Bottom:
return [x + width / 2, y + height]; return { x: x + width / 2, y: y + height };
case Position.Left: case Position.Left:
return [x, y + height / 2]; return { x, y: y + height / 2 };
} }
} }
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null { function getHandle(bounds: Handle[], handleId?: string | null): Handle | null {
if (!bounds) { if (!bounds) {
return null; return null;
} }
+2 -2
View File
@@ -274,8 +274,8 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
node.internals = { node.internals = {
...node.internals, ...node.internals,
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin), source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id, node.origin || nodeOrigin),
target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin), target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id, node.origin || nodeOrigin),
}, },
}; };
+52 -33
View File
@@ -1,4 +1,4 @@
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils'; import { pointToRendererPoint, getHostForElement, calcAutoPan, getEventPosition, getHandlePosition } from '../utils';
import { import {
ConnectionMode, ConnectionMode,
type OnConnect, type OnConnect,
@@ -7,13 +7,13 @@ import {
type Connection, type Connection,
type PanBy, type PanBy,
type Transform, type Transform,
type ConnectingHandle, type Handle,
type OnConnectEnd, type OnConnectEnd,
type UpdateConnection, type UpdateConnection,
type IsValidConnection, type IsValidConnection,
type ConnectionHandle,
NodeLookup, NodeLookup,
Position, Position,
oppositePosition,
} from '../types'; } from '../types';
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
@@ -39,11 +39,11 @@ export type OnPointerDownParams = {
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void; onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform; getTransform: () => Transform;
getFromHandle: () => ConnectingHandle | null; getFromHandle: () => Handle | null;
}; };
export type IsValidParams = { export type IsValidParams = {
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null; handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
fromNodeId: string; fromNodeId: string;
fromHandleId: string | null; fromHandleId: string | null;
@@ -52,6 +52,7 @@ export type IsValidParams = {
doc: Document | ShadowRoot; doc: Document | ShadowRoot;
lib: string; lib: string;
flowId: string | null; flowId: string | null;
handleLookup?: Handle[];
}; };
export type XYHandleInstance = { export type XYHandleInstance = {
@@ -63,13 +64,11 @@ type Result = {
handleDomNode: Element | null; handleDomNode: Element | null;
isValid: boolean; isValid: boolean;
connection: Connection | null; connection: Connection | null;
toHandle: ConnectingHandle | null; toHandle: Handle | null;
}; };
const alwaysValid = () => true; const alwaysValid = () => true;
let fromHandle: ConnectingHandle | null = null;
function onPointerDown( function onPointerDown(
event: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent,
{ {
@@ -99,7 +98,7 @@ function onPointerDown(
// 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
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0; let autoPanId = 0;
let closestHandle: ConnectionHandle | null; let closestHandle: Handle | null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y); const clickedHandle = doc?.elementFromPoint(x, y);
@@ -113,10 +112,10 @@ function onPointerDown(
let position = getEventPosition(event, containerBounds); let position = getEventPosition(event, containerBounds);
let autoPanStarted = false; let autoPanStarted = false;
let connection: Connection | null = null; let connection: Connection | null = null;
let isValid = false; let isValid: boolean | null = false;
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({ const [handleLookup, fromHandleInternal] = getHandleLookup({
nodeLookup, nodeLookup,
nodeId, nodeId,
handleId, handleId,
@@ -135,18 +134,30 @@ function onPointerDown(
} }
// Stays the same for all consecutive pointermove events // Stays the same for all consecutive pointermove events
fromHandle = { const fromHandle: Handle = {
...fromHandleInternal,
nodeId, nodeId,
handleId,
type: handleType, type: handleType,
position: (clickedHandle?.getAttribute('data-handlepos') as Position) ?? Position.Top, position: fromHandleInternal.position,
}; };
const fromNodeInternal = nodeLookup.get(nodeId)!;
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
updateConnection({ updateConnection({
position, inProgress: true,
isValid: null, isValid: null,
from,
fromHandle, fromHandle,
fromPosition: fromHandle.position,
fromNode: fromNodeInternal.internals.userNode,
to: pointToRendererPoint(position, getTransform()),
toHandle: null, toHandle: null,
toPosition: oppositePosition[fromHandle.position],
toNode: null,
}); });
onConnectStart?.(event, { nodeId, handleId, handleType }); onConnectStart?.(event, { nodeId, handleId, handleType });
@@ -180,26 +191,29 @@ function onPointerDown(
doc, doc,
lib, lib,
flowId, flowId,
handleLookup,
}); });
handleDomNode = result.handleDomNode; handleDomNode = result.handleDomNode;
connection = result.connection; connection = result.connection;
isValid = result.isValid; isValid = isConnectionValid(!!closestHandle, result.isValid);
updateConnection({ updateConnection({
inProgress: true,
isValid,
from,
fromHandle, fromHandle,
position: fromPosition: fromHandle.position,
fromNode: fromNodeInternal.internals.userNode,
to:
closestHandle && isValid closestHandle && isValid
? rendererPointToPoint( ? { x: closestHandle.x, y: closestHandle.y }
{ : pointToRendererPoint(position, transform),
x: closestHandle.x,
y: closestHandle.y,
},
transform
)
: position,
isValid: isConnectionValid(!!closestHandle, isValid),
toHandle: result.toHandle, toHandle: result.toHandle,
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
}); });
} }
@@ -222,7 +236,6 @@ function onPointerDown(
isValid = false; isValid = false;
connection = null; connection = null;
handleDomNode = null; handleDomNode = null;
fromHandle = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener);
@@ -251,6 +264,7 @@ function isValidHandle(
lib, lib,
flowId, flowId,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
handleLookup,
}: IsValidParams }: IsValidParams
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
@@ -301,12 +315,17 @@ function isValidHandle(
result.isValid = isValid && isValidConnection(connection); result.isValid = isValid && isValidConnection(connection);
result.toHandle = { if (handleLookup) {
nodeId: handleNodeId as string, const toHandle = handleLookup.find(
handleId, (h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
type: handleType as HandleType, );
position: handleToCheck.getAttribute('data-handlepos') as Position,
}; if (toHandle) {
result.toHandle = {
...toHandle,
};
}
}
} }
return result; return result;
+24 -28
View File
@@ -3,40 +3,34 @@ import {
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type ConnectionHandle, type Handle,
InternalNodeBase, InternalNodeBase,
NodeLookup, NodeLookup,
} from '../types'; } from '../types';
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( function getHandles(
node: InternalNodeBase, node: InternalNodeBase,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
): ConnectionHandle[] { ): [Handle[], Handle | null] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => { let excludedHandle = null;
if (`${node.id}-${handle.id}-${type}` !== currentHandle) { const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
const [x, y] = getHandlePosition(node, handle); if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
res.push({ excludedHandle = handle;
id: handle.id || null, } else {
type, const handleXY = getHandlePosition(node, handle);
nodeId: node.id, res.push({ ...handle, ...handleXY });
x,
y,
});
} }
return res; return res;
}, []); }, []);
return [handles, excludedHandle];
} }
export function getClosestHandle( export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
pos: XYPosition, let closestHandles: Handle[] = [];
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity; let minDistance = Infinity;
for (const handle of handles) { for (const handle of handles) {
@@ -66,7 +60,7 @@ type GetHandleLookupParams = {
nodeLookup: NodeLookup; nodeLookup: NodeLookup;
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: HandleType;
}; };
export function getHandleLookup({ export function getHandleLookup({
@@ -74,19 +68,21 @@ export function getHandleLookup({
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
}: GetHandleLookupParams): ConnectionHandle[] { }: GetHandleLookupParams): [Handle[], Handle] {
const connectionHandles: ConnectionHandle[] = []; const connectionHandles: Handle[] = [];
const currentHandle = { nodeId, handleId, handleType };
let excludedHandle: Handle | null = null;
for (const [, node] of nodeLookup) { for (const node of nodeLookup.values()) {
if (node.internals.handleBounds) { if (node.internals.handleBounds) {
const id = `${nodeId}-${handleId}-${handleType}`; const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id); const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id); excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
connectionHandles.push(...sourceHandles, ...targetHandles); connectionHandles.push(...sourceHandles, ...targetHandles);
} }
} }
return connectionHandles; return [connectionHandles, excludedHandle!];
} }
export function getHandleType( export function getHandleType(