Merge pull request #4397 from xyflow/enhance-connection-more
Streamline Connections
This commit is contained in:
@@ -1,9 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { useConnection } from '@xyflow/svelte';
|
||||
import { getBezierPath, useConnection } from '@xyflow/svelte';
|
||||
|
||||
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>
|
||||
|
||||
{#if $connection.path}
|
||||
<path d={$connection.path} fill="none" stroke={$connection.startHandle?.handleId} />
|
||||
{#if $connection.inProgress}
|
||||
<path d={path} fill="none" stroke={$connection.fromHandle.id} />
|
||||
{/if}
|
||||
|
||||
@@ -1,140 +1,18 @@
|
||||
import { CSSProperties, useCallback } from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
Position,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
getBezierPath,
|
||||
getSmoothStepPath,
|
||||
type HandleType,
|
||||
getConnectionStatus,
|
||||
getStraightPath,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
|
||||
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } 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';
|
||||
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
|
||||
import { useConnection } from '../../hooks/useConnection';
|
||||
|
||||
type ConnectionLineWrapperProps = {
|
||||
type: ConnectionLineType;
|
||||
@@ -144,19 +22,18 @@ type ConnectionLineWrapperProps = {
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
nodeId: s.connection.fromHandle?.nodeId,
|
||||
handleType: s.connection.fromHandle?.type,
|
||||
nodesConnectable: s.nodesConnectable,
|
||||
isValid: s.connection.isValid,
|
||||
inProgress: s.connection.inProgress,
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
});
|
||||
|
||||
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
|
||||
const { nodeId, handleType, nodesConnectable, width, height, isValid } = useStore(selector, shallow);
|
||||
const renderConnectionLine = !!(nodeId && handleType && width && nodesConnectable);
|
||||
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
|
||||
const renderConnection = !!(width && nodesConnectable && inProgress);
|
||||
|
||||
if (!renderConnectionLine) {
|
||||
if (!renderConnection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -168,15 +45,78 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
|
||||
className="react-flow__connectionline react-flow__container"
|
||||
>
|
||||
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
|
||||
<ConnectionLine
|
||||
nodeId={nodeId}
|
||||
handleType={handleType}
|
||||
style={style}
|
||||
type={type}
|
||||
CustomComponent={component}
|
||||
isValid={isValid}
|
||||
/>
|
||||
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
|
||||
</g>
|
||||
</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 } = useConnection();
|
||||
|
||||
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 connectingTo = toHandle?.nodeId === nodeId && toHandle?.handleId === handleId && toHandle?.type === type;
|
||||
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
|
||||
|
||||
return {
|
||||
connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.handleId === handleId && fromHandle?.type === type,
|
||||
connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.id === handleId && fromHandle?.type === type,
|
||||
connectingTo,
|
||||
clickConnecting:
|
||||
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
|
||||
clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.id === handleId && clickHandle?.type === type,
|
||||
isPossibleEndHandle:
|
||||
connectionMode === ConnectionMode.Strict
|
||||
? fromHandle?.type !== type
|
||||
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.handleId,
|
||||
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
|
||||
connectionInProcess: !!fromHandle,
|
||||
valid: connectingTo && isValid,
|
||||
};
|
||||
@@ -167,7 +166,7 @@ function HandleComponent(
|
||||
|
||||
if (!connectionClickStartHandle) {
|
||||
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
|
||||
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
|
||||
store.setState({ connectionClickStartHandle: { nodeId, type, id: handleId } });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,7 +180,7 @@ function HandleComponent(
|
||||
},
|
||||
connectionMode,
|
||||
fromNodeId: connectionClickStartHandle.nodeId,
|
||||
fromHandleId: connectionClickStartHandle.handleId || null,
|
||||
fromHandleId: connectionClickStartHandle.id || null,
|
||||
fromType: connectionClickStartHandle.type,
|
||||
isValidConnection: isValidConnectionHandler,
|
||||
flowId,
|
||||
|
||||
@@ -2,19 +2,19 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { ReactFlowStore } from '../types/store';
|
||||
import { ConnectionState } from '@xyflow/system';
|
||||
|
||||
const selector = (s: ReactFlowStore) => ({
|
||||
...s.connection,
|
||||
inProgress: s.connection.fromHandle !== null,
|
||||
});
|
||||
import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
|
||||
|
||||
const selector = (s: ReactFlowStore): ConnectionState => {
|
||||
return s.connection.inProgress
|
||||
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
|
||||
: { ...s.connection };
|
||||
};
|
||||
/**
|
||||
* Hook for accessing the ongoing connection.
|
||||
* Hook for accessing the connection state.
|
||||
*
|
||||
* @public
|
||||
* @returns ongoing connection
|
||||
* @returns ConnectionState
|
||||
*/
|
||||
export function useConnection(): ConnectionState & { inProgress: boolean } {
|
||||
export function useConnection(): ConnectionState {
|
||||
return useStore(selector, shallow);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
EdgeSelectionChange,
|
||||
NodeSelectionChange,
|
||||
ParentExpandChild,
|
||||
initialConnection,
|
||||
NodeOrigin,
|
||||
} from '@xyflow/system';
|
||||
|
||||
@@ -310,27 +311,12 @@ const createStore = ({
|
||||
);
|
||||
},
|
||||
cancelConnection: () => {
|
||||
const { connection } = get();
|
||||
set({
|
||||
connection: {
|
||||
position: connection.position,
|
||||
fromHandle: null,
|
||||
toHandle: null,
|
||||
isValid: null,
|
||||
},
|
||||
connection: { ...initialConnection },
|
||||
});
|
||||
},
|
||||
updateConnection: (params) => {
|
||||
const { connection } = get();
|
||||
|
||||
const currentConnection = {
|
||||
connection: {
|
||||
...params,
|
||||
position: params.position ?? connection.position,
|
||||
},
|
||||
};
|
||||
|
||||
set(currentConnection);
|
||||
updateConnection: (connection) => {
|
||||
set({ connection });
|
||||
},
|
||||
|
||||
reset: () => set({ ...getInitialState() }),
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
devWarn,
|
||||
getInternalNodesBounds,
|
||||
NodeOrigin,
|
||||
initialConnection,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
|
||||
@@ -104,12 +105,7 @@ const getInitialState = ({
|
||||
|
||||
multiSelectionActive: false,
|
||||
|
||||
connection: {
|
||||
fromHandle: null,
|
||||
toHandle: null,
|
||||
position: { x: 0, y: 0 },
|
||||
isValid: null,
|
||||
},
|
||||
connection: { ...initialConnection },
|
||||
connectionClickStartHandle: null,
|
||||
connectOnClick: true,
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
HandleType,
|
||||
Connection,
|
||||
ConnectionLineType,
|
||||
HandleElement,
|
||||
Handle,
|
||||
EdgePosition,
|
||||
StepPathOptions,
|
||||
OnError,
|
||||
@@ -193,8 +193,8 @@ export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newC
|
||||
export type ConnectionLineComponentProps = {
|
||||
connectionLineStyle?: CSSProperties;
|
||||
connectionLineType: ConnectionLineType;
|
||||
fromNode?: Node;
|
||||
fromHandle?: HandleElement;
|
||||
fromNode: Node;
|
||||
fromHandle: Handle;
|
||||
fromX: number;
|
||||
fromY: number;
|
||||
toX: number;
|
||||
@@ -202,6 +202,8 @@ export type ConnectionLineComponentProps = {
|
||||
fromPosition: Position;
|
||||
toPosition: Position;
|
||||
connectionStatus: 'valid' | 'invalid' | null;
|
||||
toNode: Node | null;
|
||||
toHandle: Handle | null;
|
||||
};
|
||||
|
||||
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type OnViewportChange,
|
||||
type SelectionRect,
|
||||
type SnapGrid,
|
||||
type ConnectingHandle,
|
||||
type Handle,
|
||||
type Transform,
|
||||
type PanZoomInstance,
|
||||
type PanBy,
|
||||
@@ -80,7 +80,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
|
||||
connection: ConnectionState;
|
||||
connectionMode: ConnectionMode;
|
||||
connectionClickStartHandle: ConnectingHandle | null;
|
||||
connectionClickStartHandle: (Pick<Handle, 'nodeId' | 'id'> & Required<Pick<Handle, 'type'>>) | null;
|
||||
|
||||
snapToGrid: boolean;
|
||||
snapGrid: SnapGrid;
|
||||
|
||||
@@ -2,21 +2,59 @@
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStore } from '$lib/store';
|
||||
import {
|
||||
ConnectionLineType,
|
||||
getBezierPath,
|
||||
getConnectionStatus,
|
||||
getSmoothStepPath,
|
||||
getStraightPath
|
||||
} from '@xyflow/system';
|
||||
|
||||
export let containerStyle: string = '';
|
||||
export let style: string = '';
|
||||
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>
|
||||
|
||||
{#if $connection.path}
|
||||
{#if $connection.inProgress}
|
||||
<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 fallbacks do not work if slots are forwarded in parent -->
|
||||
{#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}
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
$onConnectEndAction?.(event);
|
||||
},
|
||||
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
|
||||
getFromHandle: () => $connection.startHandle
|
||||
getFromHandle: () => $connection.fromHandle
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -128,21 +128,20 @@
|
||||
prevConnections = connections ?? new Map();
|
||||
}
|
||||
|
||||
$: connectionInProcess = !!$connection.startHandle;
|
||||
$: connectionInProcess = !!$connection.fromHandle;
|
||||
$: connectingFrom =
|
||||
$connection.startHandle?.nodeId === nodeId &&
|
||||
$connection.startHandle?.type === type &&
|
||||
$connection.startHandle?.handleId === handleId;
|
||||
$connection.fromHandle?.nodeId === nodeId &&
|
||||
$connection.fromHandle?.type === type &&
|
||||
$connection.fromHandle?.id === handleId;
|
||||
$: connectingTo =
|
||||
$connection.endHandle?.nodeId === nodeId &&
|
||||
$connection.endHandle?.type === type &&
|
||||
$connection.endHandle?.handleId === handleId;
|
||||
$connection.toHandle?.nodeId === nodeId &&
|
||||
$connection.toHandle?.type === type &&
|
||||
$connection.toHandle?.id === handleId;
|
||||
$: isPossibleEndHandle =
|
||||
$connectionMode === ConnectionMode.Strict
|
||||
? $connection.startHandle?.type !== type
|
||||
: nodeId !== $connection.startHandle?.nodeId ||
|
||||
handleId !== $connection.startHandle?.handleId;
|
||||
$: valid = connectingTo && $connection.status === 'valid';
|
||||
? $connection.fromHandle?.type !== type
|
||||
: nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
|
||||
$: valid = connectingTo && $connection.isValid;
|
||||
</script>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Readable } from 'svelte/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.
|
||||
@@ -9,7 +9,7 @@ import type { ConnectionProps } from '$lib/store/derived-connection-props';
|
||||
* @public
|
||||
* @returns current connection as a readable store
|
||||
*/
|
||||
export function useConnection(): Readable<ConnectionProps> {
|
||||
export function useConnection(): Readable<ConnectionState> {
|
||||
const { connection } = useStore();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,9 @@ import {
|
||||
panBy as panBySystem,
|
||||
updateNodeInternals as updateNodeInternalsSystem,
|
||||
addEdge as addEdgeUtil,
|
||||
initialConnection,
|
||||
errorMessages,
|
||||
pointToRendererPoint,
|
||||
type UpdateNodePositions,
|
||||
type InternalNodeUpdate,
|
||||
type ViewportHelperFunctionOptions,
|
||||
@@ -14,18 +17,16 @@ import {
|
||||
type XYPosition,
|
||||
type CoordinateExtent,
|
||||
type UpdateConnection,
|
||||
errorMessages,
|
||||
type NodeOrigin,
|
||||
type ConnectionState
|
||||
type ConnectionState,
|
||||
type NodeOrigin
|
||||
} 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 type { SvelteFlowStore } from './types';
|
||||
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
|
||||
import { getVisibleEdges } from './visible-edges';
|
||||
import { getVisibleNodes } from './visible-nodes';
|
||||
import { getDerivedConnectionProps } from './derived-connection-props';
|
||||
|
||||
export const key = Symbol();
|
||||
|
||||
@@ -341,27 +342,13 @@ 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 _connection = writable<ConnectionState>(initialConnection);
|
||||
const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
|
||||
currentConnection.set({
|
||||
connectionStartHandle: newConnection.fromHandle,
|
||||
connectionEndHandle: newConnection.toHandle,
|
||||
connectionPosition: newConnection.position,
|
||||
connectionStatus: newConnection.isValid ? 'valid' : 'invalid'
|
||||
});
|
||||
_connection.set({ ...newConnection });
|
||||
};
|
||||
|
||||
function cancelConnection() {
|
||||
currentConnection.set(initConnectionUpdateData);
|
||||
_connection.set(initialConnection);
|
||||
}
|
||||
|
||||
function reset() {
|
||||
@@ -380,9 +367,16 @@ export function createStore({
|
||||
...store,
|
||||
|
||||
// derived state
|
||||
connection: getDerivedConnectionProps(store, currentConnection),
|
||||
visibleEdges: getVisibleEdges(store),
|
||||
visibleNodes: getVisibleNodes(store),
|
||||
connection: derived([_connection, store.viewport], ([connection, viewport]) => {
|
||||
return connection.inProgress
|
||||
? {
|
||||
...connection,
|
||||
to: pointToRendererPoint(connection.to, [viewport.x, viewport.y, viewport.zoom])
|
||||
}
|
||||
: { ...connection };
|
||||
}),
|
||||
markers: derived(
|
||||
[store.edges, store.defaultMarkerColor, store.flowId],
|
||||
([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
adoptUserNodes,
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
initialConnection,
|
||||
type SelectionRect,
|
||||
type SnapGrid,
|
||||
type MarkerProps,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
type OnConnectEnd,
|
||||
type NodeLookup,
|
||||
type EdgeLookup,
|
||||
type ConnectionState,
|
||||
type ParentLookup,
|
||||
getInternalNodesBounds
|
||||
} from '@xyflow/system';
|
||||
@@ -52,7 +54,6 @@ import type {
|
||||
InternalNode
|
||||
} from '$lib/types';
|
||||
import { createNodesStore, createEdgesStore } from './utils';
|
||||
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
|
||||
|
||||
export const initialNodeTypes = {
|
||||
input: InputNode,
|
||||
@@ -143,7 +144,7 @@ export const getInitialStore = ({
|
||||
viewport: writable<Viewport>(viewport),
|
||||
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
|
||||
domNode: writable<HTMLDivElement | null>(null),
|
||||
connection: readable<ConnectionProps>(initConnectionProps),
|
||||
connection: readable<ConnectionState>(initialConnection),
|
||||
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
|
||||
connectionRadius: writable<number>(20),
|
||||
isValidConnection: writable<IsValidConnection>(() => true),
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
||||
import type {
|
||||
FitViewOptionsBase,
|
||||
XYPosition,
|
||||
ConnectingHandle,
|
||||
Handle,
|
||||
Connection,
|
||||
OnBeforeDeleteBase,
|
||||
HandleProps as HandlePropsSystem
|
||||
@@ -17,8 +17,8 @@ export type KeyDefinition = string | KeyDefinitionObject;
|
||||
|
||||
export type ConnectionData = {
|
||||
connectionPosition: XYPosition | null;
|
||||
connectionStartHandle: ConnectingHandle | null;
|
||||
connectionEndHandle: ConnectingHandle | null;
|
||||
connectionStartHandle: Handle | null;
|
||||
connectionEndHandle: Handle | null;
|
||||
connectionStatus: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import type { ZoomBehavior } from 'd3-zoom';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { Transition } from 'd3-transition';
|
||||
|
||||
import type { XYPosition, Rect } from './utils';
|
||||
import type { XYPosition, Rect, Position } from './utils';
|
||||
import type { InternalNodeBase, NodeBase, NodeDragItem } from './nodes';
|
||||
import type { ConnectingHandle, HandleType } from './handles';
|
||||
import type { Handle, HandleType } from './handles';
|
||||
import { PanZoomInstance } from './panzoom';
|
||||
import { EdgeBase } from '..';
|
||||
|
||||
@@ -132,21 +132,50 @@ export type OnError = (id: string, message: string) => void;
|
||||
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
|
||||
export type PanBy = (delta: XYPosition) => boolean;
|
||||
|
||||
export type NoConnectionInProgress = {
|
||||
position: XYPosition;
|
||||
export const initialConnection: NoConnection = {
|
||||
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;
|
||||
|
||||
from: null;
|
||||
fromHandle: null;
|
||||
fromPosition: null;
|
||||
fromNode: null;
|
||||
|
||||
to: null;
|
||||
toHandle: null;
|
||||
toPosition: null;
|
||||
toNode: null;
|
||||
};
|
||||
|
||||
export type ConnectionInProgress = {
|
||||
position: XYPosition;
|
||||
inProgress: true;
|
||||
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;
|
||||
|
||||
|
||||
@@ -2,29 +2,15 @@ import type { Position, IsValidConnection } from '.';
|
||||
|
||||
export type HandleType = 'source' | 'target';
|
||||
|
||||
export type HandleElement = {
|
||||
export type Handle = {
|
||||
id?: string | null;
|
||||
nodeId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
position: Position;
|
||||
type: HandleType;
|
||||
width: 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 = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
|
||||
import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
|
||||
import { Optional } from '../utils/types';
|
||||
|
||||
/**
|
||||
@@ -111,8 +111,8 @@ export type NodeProps<NodeType extends NodeBase> = Pick<
|
||||
};
|
||||
|
||||
export type NodeHandleBounds = {
|
||||
source: HandleElement[] | null;
|
||||
target: HandleElement[] | null;
|
||||
source: Handle[] | null;
|
||||
target: Handle[] | null;
|
||||
};
|
||||
|
||||
export type InternalNodeUpdate = {
|
||||
@@ -149,7 +149,7 @@ export type NodeOrigin = [number, number];
|
||||
|
||||
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';
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ export enum Position {
|
||||
Bottom = 'bottom',
|
||||
}
|
||||
|
||||
export const oppositePosition = {
|
||||
[Position.Left]: Position.Right,
|
||||
[Position.Right]: Position.Left,
|
||||
[Position.Top]: Position.Bottom,
|
||||
[Position.Bottom]: Position.Top,
|
||||
};
|
||||
|
||||
export type XYPosition = {
|
||||
x: number;
|
||||
y: number;
|
||||
|
||||
@@ -51,3 +51,7 @@ export function handleConnectionChange(
|
||||
cb(diff);
|
||||
}
|
||||
}
|
||||
|
||||
export function getConnectionStatus(isValid: boolean | null) {
|
||||
return isValid === null ? null : isValid ? 'valid' : 'invalid';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, HandleElement, Position } from '../types';
|
||||
import type { Transform, XYPosition, SnapGrid, Dimensions, Position, Handle } from '../types';
|
||||
import { snapPosition, pointToRendererPoint } from './general';
|
||||
|
||||
export type GetPointerPositionParams = {
|
||||
@@ -59,22 +59,25 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
|
||||
// We store them in the internals object of the node in order to avoid
|
||||
// unnecessary recalculations.
|
||||
export const getHandleBounds = (
|
||||
selector: string,
|
||||
type: 'source' | 'target',
|
||||
nodeElement: HTMLDivElement,
|
||||
nodeBounds: DOMRect,
|
||||
zoom: number
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
zoom: number,
|
||||
nodeId: string
|
||||
): Handle[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(`.${type}`);
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Array.from(handles).map((handle): HandleElement => {
|
||||
return Array.from(handles).map((handle): Handle => {
|
||||
const handleBounds = handle.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
type,
|
||||
nodeId,
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position } from '../../types/utils';
|
||||
import { Position, XYPosition } from '../../types/utils';
|
||||
import { errorMessages } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
import { Handle } from '../../types';
|
||||
import { getNodeDimensions } from '../general';
|
||||
|
||||
export type GetEdgePositionParams = {
|
||||
@@ -58,14 +58,14 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
const [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
|
||||
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition);
|
||||
const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
|
||||
const target = getHandlePosition(targetNode, targetHandle, targetPosition);
|
||||
|
||||
return {
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceX: source.x,
|
||||
sourceY: source.y,
|
||||
targetX: target.x,
|
||||
targetY: target.y,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
};
|
||||
@@ -84,9 +84,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
handle.height = handle.height ?? 1;
|
||||
|
||||
if (handle.type === 'source') {
|
||||
source.push(handle as HandleElement);
|
||||
source.push(handle as Handle);
|
||||
} 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(
|
||||
node: InternalNodeBase,
|
||||
handle: HandleElement | null,
|
||||
fallbackPosition: Position = Position.Left
|
||||
): number[] {
|
||||
handle: Handle | null,
|
||||
fallbackPosition: Position = Position.Left,
|
||||
center = false
|
||||
): XYPosition {
|
||||
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
|
||||
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
|
||||
const { width, height } = handle ?? getNodeDimensions(node);
|
||||
|
||||
if (center) {
|
||||
return { x: x + width / 2, y: y + height / 2 };
|
||||
}
|
||||
|
||||
const position = handle?.position ?? fallbackPosition;
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return [x + width / 2, y];
|
||||
return { x: x + width / 2, y };
|
||||
case Position.Right:
|
||||
return [x + width, y + height / 2];
|
||||
return { x: x + width, y: y + height / 2 };
|
||||
case Position.Bottom:
|
||||
return [x + width / 2, y + height];
|
||||
return { x: x + width / 2, y: y + height };
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -288,8 +288,8 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
||||
...node.internals,
|
||||
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
|
||||
handleBounds: {
|
||||
source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom),
|
||||
target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom),
|
||||
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
|
||||
},
|
||||
};
|
||||
if (node.parentId) {
|
||||
|
||||
@@ -1,75 +1,25 @@
|
||||
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils';
|
||||
import {
|
||||
pointToRendererPoint,
|
||||
getHostForElement,
|
||||
calcAutoPan,
|
||||
getEventPosition,
|
||||
getHandlePosition,
|
||||
rendererPointToPoint,
|
||||
} from '../utils';
|
||||
import {
|
||||
ConnectionMode,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
NodeLookup,
|
||||
Position,
|
||||
oppositePosition,
|
||||
ConnectionInProgress,
|
||||
type Handle,
|
||||
type Connection,
|
||||
} from '../types';
|
||||
|
||||
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
|
||||
|
||||
export type OnPointerDownParams = {
|
||||
autoPanOnConnect: boolean;
|
||||
connectionMode: ConnectionMode;
|
||||
connectionRadius: number;
|
||||
domNode: HTMLDivElement | null;
|
||||
handleId: string | null;
|
||||
nodeId: string;
|
||||
isTarget: boolean;
|
||||
nodeLookup: NodeLookup;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
getFromHandle: () => ConnectingHandle | null;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
|
||||
connectionMode: ConnectionMode;
|
||||
fromNodeId: string;
|
||||
fromHandleId: string | null;
|
||||
fromType: HandleType;
|
||||
isValidConnection?: IsValidConnection;
|
||||
doc: Document | ShadowRoot;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
};
|
||||
|
||||
export type XYHandleInstance = {
|
||||
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
|
||||
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
|
||||
};
|
||||
|
||||
type Result = {
|
||||
handleDomNode: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection | null;
|
||||
toHandle: ConnectingHandle | null;
|
||||
};
|
||||
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
|
||||
|
||||
const alwaysValid = () => true;
|
||||
|
||||
let fromHandle: ConnectingHandle | null = null;
|
||||
|
||||
function onPointerDown(
|
||||
event: MouseEvent | TouchEvent,
|
||||
{
|
||||
@@ -99,7 +49,7 @@ function onPointerDown(
|
||||
// when xyflow is used inside a shadow root we can't use document
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
let autoPanId = 0;
|
||||
let closestHandle: ConnectionHandle | null;
|
||||
let closestHandle: Handle | null;
|
||||
|
||||
const { x, y } = getEventPosition(event);
|
||||
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||
@@ -113,10 +63,10 @@ function onPointerDown(
|
||||
let position = getEventPosition(event, containerBounds);
|
||||
let autoPanStarted = false;
|
||||
let connection: Connection | null = null;
|
||||
let isValid = false;
|
||||
let isValid: boolean | null = false;
|
||||
let handleDomNode: Element | null = null;
|
||||
|
||||
const handleLookup = getHandleLookup({
|
||||
const [handleLookup, fromHandleInternal] = getHandleLookup({
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
@@ -135,19 +85,34 @@ function onPointerDown(
|
||||
}
|
||||
|
||||
// Stays the same for all consecutive pointermove events
|
||||
fromHandle = {
|
||||
const fromHandle: Handle = {
|
||||
...fromHandleInternal,
|
||||
nodeId,
|
||||
handleId,
|
||||
type: handleType,
|
||||
position: (clickedHandle?.getAttribute('data-handlepos') as Position) ?? Position.Top,
|
||||
position: fromHandleInternal.position,
|
||||
};
|
||||
|
||||
updateConnection({
|
||||
position,
|
||||
const fromNodeInternal = nodeLookup.get(nodeId)!;
|
||||
|
||||
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
|
||||
|
||||
const newConnection: ConnectionInProgress = {
|
||||
inProgress: true,
|
||||
isValid: null,
|
||||
|
||||
from,
|
||||
fromHandle,
|
||||
fromPosition: fromHandle.position,
|
||||
fromNode: fromNodeInternal.internals.userNode,
|
||||
|
||||
to: position,
|
||||
toHandle: null,
|
||||
});
|
||||
toPosition: oppositePosition[fromHandle.position],
|
||||
toNode: null,
|
||||
};
|
||||
|
||||
updateConnection(newConnection);
|
||||
let previousConnection: ConnectionInProgress = newConnection;
|
||||
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
|
||||
@@ -180,27 +145,41 @@ function onPointerDown(
|
||||
doc,
|
||||
lib,
|
||||
flowId,
|
||||
handleLookup,
|
||||
});
|
||||
|
||||
handleDomNode = result.handleDomNode;
|
||||
connection = result.connection;
|
||||
isValid = result.isValid;
|
||||
isValid = isConnectionValid(!!closestHandle, result.isValid);
|
||||
|
||||
updateConnection({
|
||||
fromHandle,
|
||||
position:
|
||||
const newConnection: ConnectionInProgress = {
|
||||
// from stays the same
|
||||
...previousConnection,
|
||||
isValid,
|
||||
to:
|
||||
closestHandle && isValid
|
||||
? rendererPointToPoint(
|
||||
{
|
||||
x: closestHandle.x,
|
||||
y: closestHandle.y,
|
||||
},
|
||||
transform
|
||||
)
|
||||
? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, transform)
|
||||
: position,
|
||||
isValid: isConnectionValid(!!closestHandle, isValid),
|
||||
toHandle: result.toHandle,
|
||||
});
|
||||
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
|
||||
};
|
||||
|
||||
// we don't want to trigger an update when the connection
|
||||
// is snapped to the same handle as before
|
||||
if (
|
||||
isValid &&
|
||||
closestHandle &&
|
||||
previousConnection.toHandle &&
|
||||
newConnection.toHandle &&
|
||||
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
||||
previousConnection.toHandle.id === newConnection.toHandle.id
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateConnection(newConnection);
|
||||
previousConnection = newConnection;
|
||||
}
|
||||
|
||||
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||
@@ -222,7 +201,6 @@ function onPointerDown(
|
||||
isValid = false;
|
||||
connection = null;
|
||||
handleDomNode = null;
|
||||
fromHandle = null;
|
||||
|
||||
doc.removeEventListener('mousemove', onPointerMove as EventListener);
|
||||
doc.removeEventListener('mouseup', onPointerUp as EventListener);
|
||||
@@ -251,6 +229,7 @@ function isValidHandle(
|
||||
lib,
|
||||
flowId,
|
||||
isValidConnection = alwaysValid,
|
||||
handleLookup,
|
||||
}: IsValidParams
|
||||
) {
|
||||
const isTarget = fromType === 'target';
|
||||
@@ -301,12 +280,17 @@ function isValidHandle(
|
||||
|
||||
result.isValid = isValid && isValidConnection(connection);
|
||||
|
||||
result.toHandle = {
|
||||
nodeId: handleNodeId as string,
|
||||
handleId,
|
||||
type: handleType as HandleType,
|
||||
position: handleToCheck.getAttribute('data-handlepos') as Position,
|
||||
};
|
||||
if (handleLookup) {
|
||||
const toHandle = handleLookup.find(
|
||||
(h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
|
||||
);
|
||||
|
||||
if (toHandle) {
|
||||
result.toHandle = {
|
||||
...toHandle,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
63
packages/system/src/xyhandle/types.ts
Normal file
63
packages/system/src/xyhandle/types.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
ConnectionMode,
|
||||
type Connection,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
type PanBy,
|
||||
type Transform,
|
||||
type Handle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
|
||||
export type OnPointerDownParams = {
|
||||
autoPanOnConnect: boolean;
|
||||
connectionMode: ConnectionMode;
|
||||
connectionRadius: number;
|
||||
domNode: HTMLDivElement | null;
|
||||
handleId: string | null;
|
||||
nodeId: string;
|
||||
isTarget: boolean;
|
||||
nodeLookup: NodeLookup;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
getFromHandle: () => Handle | null;
|
||||
};
|
||||
|
||||
export type IsValidParams = {
|
||||
handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
|
||||
connectionMode: ConnectionMode;
|
||||
fromNodeId: string;
|
||||
fromHandleId: string | null;
|
||||
fromType: HandleType;
|
||||
isValidConnection?: IsValidConnection;
|
||||
doc: Document | ShadowRoot;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
handleLookup?: Handle[];
|
||||
};
|
||||
|
||||
export type XYHandleInstance = {
|
||||
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
|
||||
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
|
||||
};
|
||||
|
||||
export type Result = {
|
||||
handleDomNode: Element | null;
|
||||
isValid: boolean;
|
||||
connection: Connection | null;
|
||||
toHandle: Handle | null;
|
||||
};
|
||||
@@ -3,40 +3,34 @@ import {
|
||||
type HandleType,
|
||||
type NodeHandleBounds,
|
||||
type XYPosition,
|
||||
type ConnectionHandle,
|
||||
type Handle,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
|
||||
// this functions collects all handles and adds an absolute position
|
||||
// so that we can later find the closest handle to the mouse position
|
||||
export function getHandles(
|
||||
function getHandles(
|
||||
node: InternalNodeBase,
|
||||
handleBounds: NodeHandleBounds,
|
||||
type: HandleType,
|
||||
currentHandle: string
|
||||
): ConnectionHandle[] {
|
||||
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => {
|
||||
if (`${node.id}-${handle.id}-${type}` !== currentHandle) {
|
||||
const [x, y] = getHandlePosition(node, handle);
|
||||
res.push({
|
||||
id: handle.id || null,
|
||||
type,
|
||||
nodeId: node.id,
|
||||
x,
|
||||
y,
|
||||
});
|
||||
currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
|
||||
): [Handle[], Handle | null] {
|
||||
let excludedHandle = null;
|
||||
const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
|
||||
if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
|
||||
excludedHandle = handle;
|
||||
} else {
|
||||
const handleXY = getHandlePosition(node, handle, handle.position, true);
|
||||
res.push({ ...handle, ...handleXY });
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
return [handles, excludedHandle];
|
||||
}
|
||||
|
||||
export function getClosestHandle(
|
||||
pos: XYPosition,
|
||||
connectionRadius: number,
|
||||
handles: ConnectionHandle[]
|
||||
): ConnectionHandle | null {
|
||||
let closestHandles: ConnectionHandle[] = [];
|
||||
export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
|
||||
let closestHandles: Handle[] = [];
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const handle of handles) {
|
||||
@@ -66,7 +60,7 @@ type GetHandleLookupParams = {
|
||||
nodeLookup: NodeLookup;
|
||||
nodeId: string;
|
||||
handleId: string | null;
|
||||
handleType: string;
|
||||
handleType: HandleType;
|
||||
};
|
||||
|
||||
export function getHandleLookup({
|
||||
@@ -74,19 +68,21 @@ export function getHandleLookup({
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
}: GetHandleLookupParams): ConnectionHandle[] {
|
||||
const connectionHandles: ConnectionHandle[] = [];
|
||||
}: GetHandleLookupParams): [Handle[], Handle] {
|
||||
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) {
|
||||
const id = `${nodeId}-${handleId}-${handleType}`;
|
||||
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
|
||||
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
|
||||
const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
|
||||
const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
|
||||
excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
|
||||
connectionHandles.push(...sourceHandles, ...targetHandles);
|
||||
}
|
||||
}
|
||||
|
||||
return connectionHandles;
|
||||
return [connectionHandles, excludedHandle!];
|
||||
}
|
||||
|
||||
export function getHandleType(
|
||||
|
||||
Reference in New Issue
Block a user