streamlined connections
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() }),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -51,3 +51,7 @@ export function handleConnectionChange(
|
|||||||
cb(diff);
|
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, 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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user