refactor(store): connection data

This commit is contained in:
moklick
2023-06-05 00:02:07 +02:00
parent 1dab08f36e
commit 45f51e96b1
29 changed files with 527 additions and 879 deletions
+1 -2
View File
@@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port 3000 --open --host", "dev": "vite --port 3000 --open --host --force",
"serve": "vite serve --port 3000", "serve": "vite serve --port 3000",
"build": "vite build", "build": "vite build",
"test:dev": "cypress open", "test:dev": "cypress open",
@@ -14,7 +14,6 @@
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'" "test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
}, },
"dependencies": { "dependencies": {
"@reactflow/node-resizer": "workspace:*",
"classcat": "^5.0.3", "classcat": "^5.0.3",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
@@ -1,6 +1,6 @@
import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow'; import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow';
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId; const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionStartHandle?.nodeId;
export default function CustomNode({ id }: NodeProps) { export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector); const connectionNodeId = useStore(connectionNodeIdSelector);
@@ -43,7 +43,7 @@ const ConnectionLine = ({
useCallback( useCallback(
(s: ReactFlowStore) => ({ (s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(nodeId), fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId, handleId: s.connectionStartHandle?.handleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2], toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2], toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode, connectionMode: s.connectionMode,
@@ -133,8 +133,8 @@ type ConnectionLineWrapperProps = {
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodeId: s.connectionNodeId, nodeId: s.connectionStartHandle?.nodeId,
handleType: s.connectionHandleType, handleType: s.connectionStartHandle?.type,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
connectionStatus: s.connectionStatus, connectionStatus: s.connectionStatus,
width: s.width, width: s.width,
@@ -1,11 +1,10 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react'; import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { getMarkerId, elementSelectionKeys } from '@reactflow/utils'; import { getMarkerId, elementSelectionKeys, XYHandle } from '@reactflow/utils';
import type { Connection } from '@reactflow/system'; import type { Connection } from '@reactflow/system';
import { useStoreApi } from '../../hooks/useStore'; import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions'; import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { handlePointerDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils'; import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types'; import type { EdgeProps, WrapEdgeProps } from '../../types';
@@ -96,7 +95,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
return; return;
} }
const { edges, isValidConnection: isValidConnectionStore } = store.getState(); const {
autoPanOnConnect,
domNode,
edges,
isValidConnection: isValidConnectionStore,
connectionMode,
connectionRadius,
transform,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
getNodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source; const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null; const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source'; const handleType = isSourceHandle ? 'target' : 'source';
@@ -104,6 +118,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const isTarget = isSourceHandle; const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!; const edge = edges.find((e) => e.id === id)!;
const nodes = getNodes();
setUpdating(true); setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType); onEdgeUpdateStart?.(event, edge, handleType);
@@ -115,17 +130,26 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection); const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
handlePointerDown({ XYHandle.onPointerDown(event.nativeEvent, {
event, autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId, handleId,
nodeId, nodeId,
onConnect: onConnectEdge, nodes,
isTarget, isTarget,
getState: store.getState,
setState: store.setState,
isValidConnection,
edgeUpdaterType: handleType, edgeUpdaterType: handleType,
transform,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd, onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
}); });
}; };
@@ -1,205 +0,0 @@
import type { MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
import { StoreApi } from 'zustand';
import {
getHostForElement,
calcAutoPan,
getEventPosition,
pointToRendererPoint,
rendererPointToPoint,
} from '@reactflow/utils';
import type { OnConnect, HandleType, Connection } from '@reactflow/system';
import {
ConnectionHandle,
getClosestHandle,
getConnectionStatus,
getHandleLookup,
getHandleType,
isValidHandle,
resetRecentHandle,
ValidConnectionFunc,
} from './utils';
import type { ReactFlowState } from '../../types';
export function handlePointerDown({
event,
handleId,
nodeId,
onConnect,
isTarget,
getState,
setState,
isValidConnection,
edgeUpdaterType,
onEdgeUpdateEnd,
}: {
event: ReactMouseEvent | ReactTouchEvent;
handleId: string | null;
nodeId: string;
onConnect: OnConnect;
isTarget: boolean;
getState: StoreApi<ReactFlowState>['getState'];
setState: StoreApi<ReactFlowState>['setState'];
isValidConnection: ValidConnectionFunc;
edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): void {
// when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
const {
connectionMode,
domNode,
autoPanOnConnect,
connectionRadius,
onConnectStart,
panBy,
getNodes,
cancelConnection,
} = getState();
let autoPanId = 0;
let closestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event.nativeEvent);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) {
return;
}
let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event.nativeEvent, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid = false;
let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({
nodes: getNodes(),
nodeId,
handleId,
handleType,
});
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const autoPan = (): void => {
if (!autoPanOnConnect) {
return;
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan);
};
setState({
connectionPosition,
connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
connectionStartHandle: {
nodeId,
handleId,
type: handleType,
},
connectionEndHandle: null,
});
onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) {
const { transform } = getState();
connectionPosition = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(
event,
closestHandle,
connectionMode,
nodeId,
handleId,
isTarget ? 'target' : 'source',
isValidConnection,
doc
);
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = result.isValid;
setState({
connectionPosition:
closestHandle && isValid
? rendererPointToPoint(
{
x: closestHandle.x,
y: closestHandle.y,
},
transform
)
: connectionPosition,
connectionStatus: getConnectionStatus(!!closestHandle, isValid),
connectionEndHandle: result.endHandle,
});
if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle);
}
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version
handleDomNode.classList.add('connecting', 'react-flow__handle-connecting');
handleDomNode.classList.toggle('valid', isValid);
handleDomNode.classList.toggle('react-flow__handle-valid', isValid);
}
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
getState().onConnectEnd?.(event);
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(prevActiveHandle);
cancelConnection();
cancelAnimationFrame(autoPanId);
autoPanStarted = false;
isValid = false;
connection = null;
handleDomNode = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener);
doc.removeEventListener('touchmove', onPointerMove as EventListener);
doc.removeEventListener('touchend', onPointerUp as EventListener);
}
doc.addEventListener('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener);
}
+30 -22
View File
@@ -2,16 +2,12 @@ import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchE
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { errorMessages, Position, type HandleProps, type Connection, type HandleType } from '@reactflow/system'; import { errorMessages, Position, type HandleProps, type Connection, type HandleType } from '@reactflow/system';
import { getHostForElement, isMouseEvent } from '@reactflow/utils'; import { XYHandle, getHostForElement, isMouseEvent } from '@reactflow/utils';
import { useStore, useStoreApi } from '../../hooks/useStore'; import { useStore, useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext'; import { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler';
import { addEdge } from '../../utils/'; import { addEdge } from '../../utils/';
import { type ReactFlowState } from '../../types'; import { type ReactFlowState } from '../../types';
import { isValidHandle } from './utils';
const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>; export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
@@ -95,15 +91,26 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isConnectableStart && isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered) ((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) { ) {
handlePointerDown({ const currentStore = store.getState();
event,
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect: currentStore.autoPanOnConnect,
connectionMode: currentStore.connectionMode,
connectionRadius: currentStore.connectionRadius,
domNode: currentStore.domNode,
nodes: currentStore.getNodes(),
transform: currentStore.transform,
lib: currentStore.lib,
isTarget,
handleId, handleId,
nodeId, nodeId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended, onConnect: onConnectExtended,
isTarget, isValidConnection: isValidConnection || currentStore.isValidConnection,
getState: store.getState,
setState: store.setState,
isValidConnection: isValidConnection || store.getState().isValidConnection || alwaysValid,
}); });
} }
@@ -121,6 +128,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionClickStartHandle, connectionClickStartHandle,
connectionMode, connectionMode,
isValidConnection: isValidConnectionStore, isValidConnection: isValidConnectionStore,
lib,
} = store.getState(); } = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) { if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
@@ -128,27 +136,27 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
} }
if (!connectionClickStartHandle) { if (!connectionClickStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return; return;
} }
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore || alwaysValid; const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = isValidHandle( const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
event.nativeEvent, handle: {
{
nodeId, nodeId,
id: handleId, id: handleId,
type, type,
}, },
connectionMode, connectionMode,
connectionClickStartHandle.nodeId, fromNodeId: connectionClickStartHandle.nodeId,
connectionClickStartHandle.handleId || null, fromHandleId: connectionClickStartHandle.handleId || null,
connectionClickStartHandle.type, fromType: connectionClickStartHandle.type,
isValidConnectionHandler, isValidConnection: isValidConnectionHandler,
doc doc,
); lib,
});
if (isValid) { if (isValid) {
onConnectExtended(connection); onConnectExtended(connection);
+23 -21
View File
@@ -1,12 +1,6 @@
import { createStore } from 'zustand'; import { createStore } from 'zustand';
import { clampPosition, getDimensions, fitView, getHandleBounds } from '@reactflow/utils'; import { clampPosition, getDimensions, fitView, getHandleBounds } from '@reactflow/utils';
import { import { internalsSymbol, type CoordinateExtent } from '@reactflow/system';
internalsSymbol,
type NodeDimensionUpdate,
type CoordinateExtent,
type NodeDragItem,
type XYPosition,
} from '@reactflow/system';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { createNodeInternals, updateAbsoluteNodePositions, updateNodesAndEdgesSelections } from './utils'; import { createNodeInternals, updateAbsoluteNodePositions, updateNodesAndEdgesSelections } from './utils';
@@ -20,7 +14,6 @@ import type {
NodeSelectionChange, NodeSelectionChange,
NodePositionChange, NodePositionChange,
UnselectNodesAndEdgesParams, UnselectNodesAndEdgesParams,
NodeChange,
} from '../types'; } from '../types';
const createRFStore = () => const createRFStore = () =>
@@ -48,7 +41,7 @@ const createRFStore = () =>
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
}, },
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => { updateNodeDimensions: (updates) => {
const { const {
onNodesChange, onNodesChange,
nodeInternals, nodeInternals,
@@ -132,7 +125,7 @@ const createRFStore = () =>
onNodesChange?.(changes); onNodesChange?.(changes);
} }
}, },
updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => { updateNodePositions: (nodeDragItems, positionChanged = true, dragging = false) => {
const { triggerNodeChanges } = get(); const { triggerNodeChanges } = get();
const changes = nodeDragItems.map((node) => { const changes = nodeDragItems.map((node) => {
@@ -153,7 +146,7 @@ const createRFStore = () =>
triggerNodeChanges(changes); triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes: NodeChange[]) => { triggerNodeChanges: (changes) => {
const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin, getNodes, elevateNodesOnSelect } = get(); const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin, getNodes, elevateNodesOnSelect } = get();
if (changes?.length) { if (changes?.length) {
@@ -167,7 +160,7 @@ const createRFStore = () =>
} }
}, },
addSelectedNodes: (selectedNodeIds: string[]) => { addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edges, getNodes } = get(); const { multiSelectionActive, edges, getNodes } = get();
let changedNodes: NodeSelectionChange[]; let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null; let changedEdges: EdgeSelectionChange[] | null = null;
@@ -186,7 +179,7 @@ const createRFStore = () =>
set, set,
}); });
}, },
addSelectedEdges: (selectedEdgeIds: string[]) => { addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edges, getNodes } = get(); const { multiSelectionActive, edges, getNodes } = get();
let changedEdges: EdgeSelectionChange[]; let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null; let changedNodes: NodeSelectionChange[] | null = null;
@@ -225,19 +218,19 @@ const createRFStore = () =>
set, set,
}); });
}, },
setMinZoom: (minZoom: number) => { setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get(); const { panZoom, maxZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]); panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ minZoom }); set({ minZoom });
}, },
setMaxZoom: (maxZoom: number) => { setMaxZoom: (maxZoom) => {
const { panZoom, minZoom } = get(); const { panZoom, minZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]); panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ maxZoom }); set({ maxZoom });
}, },
setTranslateExtent: (translateExtent: CoordinateExtent) => { setTranslateExtent: (translateExtent) => {
get().panZoom?.setTranslateExtent(translateExtent); get().panZoom?.setTranslateExtent(translateExtent);
set({ translateExtent }); set({ translateExtent });
@@ -260,7 +253,7 @@ const createRFStore = () =>
set, set,
}); });
}, },
setNodeExtent: (nodeExtent: CoordinateExtent) => { setNodeExtent: (nodeExtent) => {
const { nodeInternals } = get(); const { nodeInternals } = get();
nodeInternals.forEach((node) => { nodeInternals.forEach((node) => {
@@ -272,7 +265,7 @@ const createRFStore = () =>
nodeInternals: new Map(nodeInternals), nodeInternals: new Map(nodeInternals),
}); });
}, },
panBy: (delta: XYPosition): boolean => { panBy: (delta): boolean => {
const { transform, width, height, panZoom, translateExtent } = get(); const { transform, width, height, panZoom, translateExtent } = get();
if (!panZoom || (!delta.x && !delta.y)) { if (!panZoom || (!delta.x && !delta.y)) {
@@ -300,13 +293,22 @@ const createRFStore = () =>
}, },
cancelConnection: () => cancelConnection: () =>
set({ set({
connectionNodeId: initialState.connectionNodeId,
connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
connectionStatus: initialState.connectionStatus, connectionStatus: initialState.connectionStatus,
connectionStartHandle: initialState.connectionStartHandle, connectionStartHandle: initialState.connectionStartHandle,
connectionEndHandle: initialState.connectionEndHandle, connectionEndHandle: initialState.connectionEndHandle,
}), }),
updateConnection: (params) => {
const { connectionStatus, connectionStartHandle, connectionEndHandle, connectionPosition } = get();
const currentConnection = {
connectionPosition: params.connectionPosition ?? connectionPosition,
connectionStatus: params.connectionStatus ?? connectionStatus,
connectionStartHandle: params.connectionStartHandle ?? connectionStartHandle,
connectionEndHandle: params.connectionEndHandle ?? connectionEndHandle,
};
set(currentConnection);
},
reset: () => set({ ...initialState }), reset: () => set({ ...initialState }),
})); }));
+2 -3
View File
@@ -22,9 +22,6 @@ const initialState: ReactFlowStore = {
nodesSelectionActive: false, nodesSelectionActive: false,
userSelectionActive: false, userSelectionActive: false,
userSelectionRect: null, userSelectionRect: null,
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 }, connectionPosition: { x: 0, y: 0 },
connectionStatus: null, connectionStatus: null,
connectionMode: ConnectionMode.Strict, connectionMode: ConnectionMode.Strict,
@@ -61,6 +58,8 @@ const initialState: ReactFlowStore = {
connectionRadius: 20, connectionRadius: 20,
onError: devWarn, onError: devWarn,
isValidConnection: undefined, isValidConnection: undefined,
lib: 'react',
}; };
export default initialState; export default initialState;
+4 -4
View File
@@ -3,6 +3,8 @@ import type {
ConnectionMode, ConnectionMode,
ConnectionLineType, ConnectionLineType,
OnConnect, OnConnect,
OnConnectStart,
OnConnectEnd,
CoordinateExtent, CoordinateExtent,
KeyCode, KeyCode,
PanOnScrollMode, PanOnScrollMode,
@@ -16,6 +18,7 @@ import type {
HandleType, HandleType,
SelectionMode, SelectionMode,
OnError, OnError,
IsValidConnection,
} from '@reactflow/system'; } from '@reactflow/system';
import type { import type {
@@ -25,8 +28,6 @@ import type {
Node, Node,
Edge, Edge,
ConnectionLineComponent, ConnectionLineComponent,
OnConnectStart,
OnConnectEnd,
OnEdgeUpdateFunc, OnEdgeUpdateFunc,
OnInit, OnInit,
DefaultEdgeOptions, DefaultEdgeOptions,
@@ -40,7 +41,6 @@ import type {
SelectionDragHandler, SelectionDragHandler,
EdgeMouseHandler, EdgeMouseHandler,
} from '.'; } from '.';
import { ValidConnectionFunc } from '../components/Handle/utils';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & { export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
nodes?: Node[]; nodes?: Node[];
@@ -148,7 +148,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
autoPanOnConnect?: boolean; autoPanOnConnect?: boolean;
connectionRadius?: number; connectionRadius?: number;
onError?: OnError; onError?: OnError;
isValidConnection?: ValidConnectionFunc; isValidConnection?: IsValidConnection;
}; };
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
+1 -13
View File
@@ -1,15 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import type { import type { ComponentType, MemoExoticComponent } from 'react';
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
ComponentType,
MemoExoticComponent,
} from 'react';
import { import {
FitViewParamsBase, FitViewParamsBase,
FitViewOptionsBase, FitViewOptionsBase,
NodeProps, NodeProps,
OnConnectStartParams,
ZoomInOut, ZoomInOut,
ZoomTo, ZoomTo,
SetViewport, SetViewport,
@@ -18,7 +12,6 @@ import {
SetCenter, SetCenter,
FitBounds, FitBounds,
Project, Project,
Connection,
} from '@reactflow/system'; } from '@reactflow/system';
import type { NodeChange, EdgeChange, Node, WrapNodeProps, Edge, EdgeProps, WrapEdgeProps, ReactFlowInstance } from '.'; import type { NodeChange, EdgeChange, Node, WrapNodeProps, Edge, EdgeProps, WrapEdgeProps, ReactFlowInstance } from '.';
@@ -46,16 +39,11 @@ export type OnSelectionChangeParams = {
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void; export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type FitViewParams = FitViewParamsBase<Node>; export type FitViewParams = FitViewParamsBase<Node>;
export type FitViewOptions = FitViewOptionsBase<Node>; export type FitViewOptions = FitViewOptionsBase<Node>;
export type FitView = (fitViewOptions?: FitViewOptions) => boolean; export type FitView = (fitViewOptions?: FitViewOptions) => boolean;
export type OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void; export type OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void;
export type IsValidConnection = (edge: Edge | Connection) => boolean;
export type ViewportHelperFunctions = { export type ViewportHelperFunctions = {
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
+13 -12
View File
@@ -2,7 +2,6 @@ import {
ConnectionMode, ConnectionMode,
type ConnectionStatus, type ConnectionStatus,
type CoordinateExtent, type CoordinateExtent,
type HandleType,
type NodeDimensionUpdate, type NodeDimensionUpdate,
type UpdateNodePositions, type UpdateNodePositions,
type NodeOrigin, type NodeOrigin,
@@ -16,11 +15,15 @@ import {
type XYPosition, type XYPosition,
type PanZoomInstance, type PanZoomInstance,
type PanBy, type PanBy,
OnNodeDrag, type OnConnectStart,
OnSelectionDrag, type OnConnectEnd,
OnMoveStart, type OnNodeDrag,
OnMove, type OnSelectionDrag,
OnMoveEnd, type OnMoveStart,
type OnMove,
type OnMoveEnd,
type IsValidConnection,
type UpdateConnection,
} from '@reactflow/system'; } from '@reactflow/system';
import type { import type {
@@ -30,15 +33,12 @@ import type {
OnNodesChange, OnNodesChange,
OnEdgesChange, OnEdgesChange,
NodeInternals, NodeInternals,
OnConnectStart,
OnConnectEnd,
DefaultEdgeOptions, DefaultEdgeOptions,
FitViewOptions, FitViewOptions,
OnNodesDelete, OnNodesDelete,
OnEdgesDelete, OnEdgesDelete,
OnSelectionChangeFunc, OnSelectionChangeFunc,
UnselectNodesAndEdgesParams, UnselectNodesAndEdgesParams,
IsValidConnection,
} from '.'; } from '.';
export type ReactFlowStore = { export type ReactFlowStore = {
@@ -67,9 +67,6 @@ export type ReactFlowStore = {
userSelectionActive: boolean; userSelectionActive: boolean;
userSelectionRect: SelectionRect | null; userSelectionRect: SelectionRect | null;
connectionNodeId: string | null;
connectionHandleId: string | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition; connectionPosition: XYPosition;
connectionStatus: ConnectionStatus | null; connectionStatus: ConnectionStatus | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
@@ -135,6 +132,8 @@ export type ReactFlowStore = {
connectionRadius: number; connectionRadius: number;
isValidConnection?: IsValidConnection; isValidConnection?: IsValidConnection;
lib: string;
}; };
export type ReactFlowActions = { export type ReactFlowActions = {
@@ -153,6 +152,8 @@ export type ReactFlowActions = {
setTranslateExtent: (translateExtent: CoordinateExtent) => void; setTranslateExtent: (translateExtent: CoordinateExtent) => void;
setNodeExtent: (nodeExtent: CoordinateExtent) => void; setNodeExtent: (nodeExtent: CoordinateExtent) => void;
cancelConnection: () => void; cancelConnection: () => void;
// @todo can this be reused by system?
updateConnection: UpdateConnection;
reset: () => void; reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void; triggerNodeChanges: (changes: NodeChange[]) => void;
panBy: PanBy; panBy: PanBy;
@@ -8,7 +8,7 @@
{#if $connectionPath} {#if $connectionPath}
<svg width={$width} height={$height} class="svelte-flow__connectionline"> <svg width={$width} height={$height} class="svelte-flow__connectionline">
<g class={cc(['svelte-flow__connection', $connection.status])}> <g class={cc(['svelte-flow__connection', $connection.connectionStatus])}>
<path d={$connectionPath} fill="none" class="svelte-flow__connection-path" /> <path d={$connectionPath} fill="none" class="svelte-flow__connection-path" />
</g> </g>
</svg> </svg>
@@ -2,9 +2,8 @@
import { getContext, createEventDispatcher } from 'svelte'; import { getContext, createEventDispatcher } from 'svelte';
import cc from 'classcat'; import cc from 'classcat';
import { Position, type Connection } from '@reactflow/system'; import { Position, type Connection } from '@reactflow/system';
import { isMouseEvent } from '@reactflow/utils'; import { XYHandle, isMouseEvent } from '@reactflow/utils';
import { handlePointerDown } from './handler';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { HandleComponentProps } from '$lib/types'; import type { HandleComponentProps } from '$lib/types';
@@ -23,7 +22,8 @@
const handleId = id || null; const handleId = id || null;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const store = useStore();
const { const {
connectionMode, connectionMode,
domNode, domNode,
@@ -31,11 +31,12 @@
connectionRadius, connectionRadius,
transform, transform,
isValidConnection, isValidConnection,
lib,
addEdge, addEdge,
panBy, panBy,
cancelConnection, cancelConnection,
updateConnection, updateConnection,
} = useStore(); } = store;
function dispatchEvent(eventName: string, params?: Connection) { function dispatchEvent(eventName: string, params?: Connection) {
dispatch(eventName, params || { nodeId, handleId, type }); dispatch(eventName, params || { nodeId, handleId, type });
@@ -50,23 +51,22 @@
const isMouseTriggered = isMouseEvent(event); const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({ XYHandle.onPointerDown(event, {
event,
handleId, handleId,
nodeId, nodeId,
isTarget, isTarget,
connectionRadius: $connectionRadius, connectionRadius: $connectionRadius,
domNode: $domNode, domNode: $domNode,
nodes, nodes: $nodes,
connectionMode: $connectionMode, connectionMode: $connectionMode,
transform, transform: $transform,
lib: $lib,
autoPanOnConnect: true,
isValidConnection: $isValidConnection, isValidConnection: $isValidConnection,
onConnect: onConnectExtended,
updateConnection, updateConnection,
cancelConnection, cancelConnection,
panBy, panBy,
onConnectStart: () => dispatchEvent('connect:start'), onConnect: onConnectExtended,
onConnectEnd: () => dispatchEvent('connect:end')
}); });
} }
} }
@@ -1,213 +0,0 @@
import { get, type Writable } from 'svelte/store';
import {
getHostForElement,
calcAutoPan,
getEventPosition,
pointToRendererPoint,
rendererPointToPoint
} from '@reactflow/utils';
import type {
OnConnect,
HandleType,
Connection,
ConnectionMode,
XYPosition,
Transform
} from '@reactflow/system';
import {
getClosestHandle,
getConnectionStatus,
getHandleLookup,
getHandleType,
isValidHandle,
resetRecentHandle,
type ConnectionHandle
} from './utils';
import type { ConnectionData, IsValidConnection, Node } from '$lib/types';
export function handlePointerDown({
event,
handleId,
nodeId,
onConnect,
domNode,
nodes,
connectionMode,
connectionRadius,
isTarget,
transform: transformStore,
panBy,
updateConnection,
cancelConnection,
isValidConnection,
edgeUpdaterType,
onEdgeUpdateEnd,
onConnectStart,
onConnectEnd
}: {
event: MouseEvent | TouchEvent;
handleId: string | null;
nodeId: string;
onConnect: OnConnect;
isTarget: boolean;
connectionMode: ConnectionMode;
domNode: HTMLDivElement | null;
nodes: Writable<Node[]>;
connectionRadius: number;
isValidConnection: IsValidConnection;
transform: Writable<Transform>;
updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void;
panBy: (delta: XYPosition) => void;
edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
onConnectStart: () => void;
onConnectEnd: () => void;
}): void {
// when svelte-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) {
return;
}
let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid = false;
let handleDomNode: Element | null = null;
const autoPanOnConnect = true;
const handleLookup = getHandleLookup({
nodes: get(nodes),
nodeId,
handleId,
handleType
});
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const autoPan = (): void => {
// @todd add prop
if (!autoPanOnConnect) {
return;
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan);
};
updateConnection({
position: connectionPosition,
nodeId,
handleId,
handleType,
status: null
});
// onConnectStart?.(event, { nodeId, handleId, handleType });
onConnectStart();
function onPointerMove(event: MouseEvent | TouchEvent) {
const transform = get(transformStore);
connectionPosition = getEventPosition(event, containerBounds);
prevClosestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(
event,
prevClosestHandle,
connectionMode,
nodeId,
handleId,
isTarget ? 'target' : 'source',
isValidConnection,
doc,
get(nodes)
);
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = result.isValid;
updateConnection({
position:
prevClosestHandle && isValid
? rendererPointToPoint(
{
x: prevClosestHandle.x,
y: prevClosestHandle.y
},
transform
)
: connectionPosition,
status: getConnectionStatus(!!prevClosestHandle, isValid)
});
if (!prevClosestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle);
}
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "svelte-flow__handle-" in the next major version
handleDomNode.classList.add('connecting');
handleDomNode.classList.toggle('valid', isValid);
}
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((prevClosestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
onConnectEnd();
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(prevActiveHandle);
cancelConnection();
cancelAnimationFrame(autoPanId);
autoPanStarted = false;
isValid = false;
connection = null;
handleDomNode = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener);
doc.removeEventListener('touchmove', onPointerMove as EventListener);
doc.removeEventListener('touchend', onPointerUp as EventListener);
}
doc.addEventListener('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener);
}
@@ -1,194 +0,0 @@
import { internalsSymbol, ConnectionMode, type ConnectionStatus } from '@reactflow/system';
import type { Connection, HandleType, XYPosition, NodeHandleBounds } from '@reactflow/system';
import { getEventPosition } from '@reactflow/utils';
import type { IsValidConnection, Node } from '$lib/types';
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
};
export type ValidConnectionFunc = (connection: Connection) => boolean;
// 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(
node: Node,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
res.push({
id: h.id || null,
type,
nodeId: node.id,
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2
});
}
return res;
}, []);
}
export function getClosestHandle(
pos: XYPosition,
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandle: ConnectionHandle | null = null;
let minDistance = Infinity;
handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius && distance < minDistance) {
minDistance = distance;
closestHandle = handle;
}
});
return closestHandle;
}
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
};
const nullConnection: Connection = {
source: null,
target: null,
sourceHandle: null,
targetHandle: null
};
// checks if and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle(
event: MouseEvent | TouchEvent,
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
connectionMode: ConnectionMode,
fromNodeId: string,
fromHandleId: string | null,
fromType: string,
isValidConnection: IsValidConnection,
doc: Document | ShadowRoot,
nodes: Node[]
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.svelte-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
const handleToCheck = handleBelow?.classList.contains('svelte-flow__handle')
? handleBelow
: handleDomNode;
const result: Result = {
handleDomNode: handleToCheck,
isValid: false,
connection: nullConnection
};
if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid');
const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId,
sourceHandle: isTarget ? handleId : fromHandleId,
target: isTarget ? fromNodeId : handleNodeId,
targetHandle: isTarget ? fromHandleId : handleId
};
result.connection = connection;
// in strict mode we don't allow target to target or source to source connections
const isValid =
connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId;
if (isValid) {
const fromNode: Node | undefined = nodes.find((n) => n.id === connection.source);
const toNode: Node | undefined = nodes.find((n) => n.id === connection.target);
if (fromNode && toNode) result.isValid = isValidConnection(connection, { fromNode, toNode });
}
}
return result;
}
type GetHandleLookupParams = {
nodes: Node[];
nodeId: string;
handleId: string | null;
handleType: string;
};
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
return nodes.reduce<ConnectionHandle[]>((res, node) => {
if (node[internalsSymbol]) {
const { handleBounds } = node[internalsSymbol];
let sourceHandles: ConnectionHandle[] = [];
let targetHandles: ConnectionHandle[] = [];
if (handleBounds) {
sourceHandles = getHandles(
node,
handleBounds,
'source',
`${nodeId}-${handleId}-${handleType}`
);
targetHandles = getHandles(
node,
handleBounds,
'target',
`${nodeId}-${handleId}-${handleType}`
);
}
res.push(...sourceHandles, ...targetHandles);
}
return res;
}, []);
}
export function getHandleType(
edgeUpdaterType: HandleType | undefined,
handleDomNode: Element | null
): HandleType | null {
if (edgeUpdaterType) {
return edgeUpdaterType;
} else if (handleDomNode?.classList.contains('target')) {
return 'target';
} else if (handleDomNode?.classList.contains('source')) {
return 'source';
}
return null;
}
export function resetRecentHandle(handleDomNode: Element): void {
handleDomNode?.classList.remove('valid', 'connecting');
}
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
let connectionStatus = null;
if (isHandleValid) {
connectionStatus = 'valid';
} else if (isInsideConnectionRadius && !isHandleValid) {
connectionStatus = 'invalid';
}
return connectionStatus as ConnectionStatus;
}
@@ -36,6 +36,11 @@
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import { getConnectedEdges } from '$lib/utils'; import { getConnectedEdges } from '$lib/utils';
import type { Node, Edge } from '$lib/types'; import type { Node, Edge } from '$lib/types';
import type { PaneProps } from './types';
type $$Props = PaneProps;
export let panOnDrag: $$Props['panOnDrag'] = undefined;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const { const {
@@ -133,9 +138,9 @@
// We only want to trigger click functions when in selection mode if // We only want to trigger click functions when in selection mode if
// the user did not move the mouse. // the user did not move the mouse.
// if (!userSelectionActive && userSelectionRect && event.target === container.current) { if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
// onClick?.(event); onClick?.(event);
// } }
selectionRect.set(null); selectionRect.set(null);
if (selectedNodes.length > 0) { if (selectedNodes.length > 0) {
@@ -155,10 +160,10 @@
}; };
const onContextMenu = (event: MouseEvent) => { const onContextMenu = (event: MouseEvent) => {
// if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) { if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
// event.preventDefault(); event.preventDefault();
// return; return;
// } }
dispatch('pane:contextmenu', event); dispatch('pane:contextmenu', event);
}; };
@@ -0,0 +1,3 @@
export type PaneProps = {
panOnDrag?: boolean | number[];
};
@@ -167,7 +167,7 @@
{panOnScroll} {panOnScroll}
{panOnDrag} {panOnDrag}
> >
<Pane on:pane:click> <Pane on:pane:click {panOnDrag}>
<ViewportComponent> <ViewportComponent>
<EdgeRenderer on:edge:click /> <EdgeRenderer on:edge:click />
<ConnectionLine /> <ConnectionLine />
@@ -11,17 +11,11 @@ import type {
OnMove, OnMove,
OnMoveEnd, OnMoveEnd,
CoordinateExtent, CoordinateExtent,
PanOnScrollMode PanOnScrollMode,
IsValidConnection
} from '@reactflow/system'; } from '@reactflow/system';
import type { import type { Edge, Node, NodeTypes, KeyDefinition, EdgeTypes } from '$lib/types';
Edge,
Node,
NodeTypes,
KeyDefinition,
EdgeTypes,
IsValidConnection
} from '$lib/types';
export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & { export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
id?: string; id?: string;
@@ -21,20 +21,23 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
store.transform store.transform
], ],
([connection, connectionLineType, connectionMode, nodes, transform]) => { ([connection, connectionLineType, connectionMode, nodes, transform]) => {
if (!connection.nodeId) { if (!connection.connectionStartHandle?.nodeId) {
return null; return null;
} }
const fromNode = nodes.find((n) => n.id === connection.nodeId); const fromNode = nodes.find((n) => n.id === connection.connectionStartHandle?.nodeId);
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds; const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
const handleBoundsStrict = fromHandleBounds?.[connection.handleType || 'source'] || []; const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict ? handleBoundsStrict
: fromHandleBounds?.[connection.handleType === 'source' ? 'target' : 'source']!; : fromHandleBounds?.[
connection?.connectionStartHandle?.type === 'source' ? 'target' : 'source'
]!;
const handleBounds = const handleBounds =
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose; connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.handleId const fromHandle = connection.connectionStartHandle?.handleId
? handleBounds.find((d) => d.id === connection.handleId) ? handleBounds.find((d) => d.id === connection.connectionStartHandle?.handleId)
: handleBounds[0]; : handleBounds[0];
const fromHandleX = fromHandle const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2 ? fromHandle.x + fromHandle.width / 2
@@ -49,8 +52,8 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
sourceX: fromX, sourceX: fromX,
sourceY: fromY, sourceY: fromY,
sourcePosition: fromPosition, sourcePosition: fromPosition,
targetX: ((connection.position?.x ?? 0) - transform[0]) / transform[2], targetX: ((connection.connectionPosition?.x ?? 0) - transform[0]) / transform[2],
targetY: ((connection.position?.y ?? 0) - transform[1]) / transform[2], targetY: ((connection.connectionPosition?.y ?? 0) - transform[1]) / transform[2],
targetPosition: toPosition targetPosition: toPosition
}; };
+7 -5
View File
@@ -7,7 +7,8 @@ import {
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
type Connection, type Connection,
type XYPosition, type XYPosition,
type CoordinateExtent type CoordinateExtent,
type UpdateConnection
} from '@reactflow/system'; } from '@reactflow/system';
import { import {
createMarkerIds, createMarkerIds,
@@ -304,21 +305,22 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
return transformChanged; return transformChanged;
} }
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) { const updateConnection: UpdateConnection = (update) => {
const currentConnectionData = get(store.connection); const currentConnectionData = get(store.connection);
const nextConnectionData = currentConnectionData const nextConnectionData = currentConnectionData
? { ? {
...initConnectionData, ...initConnectionData,
...currentConnectionData, ...currentConnectionData,
...connectionUpdate ...update
} }
: { : {
...initConnectionData, ...initConnectionData,
...connectionUpdate ...update
}; };
store.connection.set(nextConnectionData); store.connection.set(nextConnectionData);
} };
function cancelConnection() { function cancelConnection() {
updateConnection(initConnectionData); updateConnection(initConnectionData);
+9 -16
View File
@@ -8,7 +8,8 @@ import {
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
type PanZoomInstance, type PanZoomInstance,
type CoordinateExtent type CoordinateExtent,
type IsValidConnection
} from '@reactflow/system'; } from '@reactflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -18,23 +19,14 @@ import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import StepEdge from '$lib/components/edges/StepEdge.svelte'; import StepEdge from '$lib/components/edges/StepEdge.svelte';
import type { import type { ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted, Edge, Node } from '$lib/types';
ConnectionData,
NodeTypes,
EdgeTypes,
EdgeLayouted,
Edge,
Node,
IsValidConnection
} from '$lib/types';
import { infiniteExtent } from '@reactflow/utils'; import { infiniteExtent } from '@reactflow/utils';
export const initConnectionData = { export const initConnectionData = {
nodeId: null, connectionStartHandle: null,
handleId: null, connectionEndHandle: null,
handleType: null, connectionPosition: null,
position: null, connectionStatus: null
status: null
}; };
export const initialNodeTypes = { export const initialNodeTypes = {
@@ -88,5 +80,6 @@ export const initialStoreState = {
elementsSelectable: writable<boolean>(true), elementsSelectable: writable<boolean>(true),
selectNodesOnDrag: writable<boolean>(true), selectNodesOnDrag: writable<boolean>(true),
markers: readable<MarkerProps[]>([]), markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7') defaultMarkerColor: writable<string>('#b1b1b7'),
lib: readable<string>('svelte')
}; };
+4 -3
View File
@@ -4,11 +4,12 @@ import type {
ViewportHelperFunctionOptions, ViewportHelperFunctionOptions,
Connection, Connection,
UpdateNodePositions, UpdateNodePositions,
CoordinateExtent CoordinateExtent,
UpdateConnection
} from '@reactflow/system'; } from '@reactflow/system';
import type { initialStoreState } from './initial-store'; import type { initialStoreState } from './initial-store';
import type { Node, Edge, ConnectionData, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/types'; import type { Node, Edge, NodeTypes, EdgeTypes, FitViewOptions } from '$lib/types';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
export type SvelteFlowStoreActions = { export type SvelteFlowStoreActions = {
@@ -27,7 +28,7 @@ export type SvelteFlowStoreActions = {
addSelectedNodes: (ids: string[]) => void; addSelectedNodes: (ids: string[]) => void;
addSelectedEdges: (ids: string[]) => void; addSelectedEdges: (ids: string[]) => void;
panBy: (delta: XYPosition) => boolean; panBy: (delta: XYPosition) => boolean;
updateConnection: (connection: Partial<ConnectionData>) => void; updateConnection: UpdateConnection;
cancelConnection: () => void; cancelConnection: () => void;
reset(): void; reset(): void;
}; };
+6 -12
View File
@@ -1,10 +1,10 @@
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut'; import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { import type {
Connection,
FitViewOptionsBase, FitViewOptionsBase,
HandleType, HandleType,
Position, Position,
XYPosition XYPosition,
ConnectingHandle
} from '@reactflow/system'; } from '@reactflow/system';
import type { Node } from './nodes'; import type { Node } from './nodes';
@@ -14,11 +14,10 @@ export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
export type KeyDefinition = string | KeyDefinitionObject; export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = { export type ConnectionData = {
position: XYPosition | null; connectionPosition: XYPosition | null;
nodeId: string | null; connectionStartHandle: ConnectingHandle | null;
handleId: string | null; connectionEndHandle: ConnectingHandle | null;
handleType: HandleType | null; connectionStatus: string | null;
status: string | null;
}; };
export type HandleComponentProps = { export type HandleComponentProps = {
@@ -30,8 +29,3 @@ export type HandleComponentProps = {
}; };
export type FitViewOptions = FitViewOptionsBase<Node>; export type FitViewOptions = FitViewOptionsBase<Node>;
export type IsValidConnection = (
connection: Connection,
{ fromNode, toNode }: { fromNode: Node; toNode: Node }
) => boolean;
+19 -7
View File
@@ -3,8 +3,9 @@ import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavi
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect } from './utils';
import type { BaseNode, NodeDragItem, NodeOrigin } from './nodes'; import type { BaseNode, NodeDragItem, NodeOrigin } from './nodes';
import type { HandleType } from './handles'; import type { ConnectingHandle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { BaseEdge } from '..';
export type Project = (position: XYPosition) => XYPosition; export type Project = (position: XYPosition) => XYPosition;
@@ -34,7 +35,17 @@ export enum ConnectionMode {
Loose = 'loose', Loose = 'loose',
} }
export type OnConnectStartParams = {
nodeId: string | null;
handleId: string | null;
handleType: HandleType | null;
};
export type OnConnectStart = (event: MouseEvent | TouchEvent, params: OnConnectStartParams) => void;
export type OnConnect = (connection: Connection) => void; export type OnConnect = (connection: Connection) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: BaseEdge | Connection) => boolean;
export type FitViewParamsBase<NodeType extends BaseNode> = { export type FitViewParamsBase<NodeType extends BaseNode> = {
nodes: NodeType[]; nodes: NodeType[];
@@ -55,12 +66,6 @@ export type FitViewOptionsBase<NodeType extends BaseNode> = {
nodes?: (Partial<NodeType> & { id: NodeType['id'] })[]; nodes?: (Partial<NodeType> & { id: NodeType['id'] })[];
}; };
export type OnConnectStartParams = {
nodeId: string | null;
handleId: string | null;
handleType: HandleType | null;
};
export type Viewport = { export type Viewport = {
x: number; x: number;
y: number; y: number;
@@ -124,3 +129,10 @@ export type UpdateNodePositions = (
dragging?: boolean dragging?: boolean
) => void; ) => void;
export type PanBy = (delta: XYPosition) => boolean; export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: {
connectionPosition?: XYPosition | null;
connectionStatus?: ConnectionStatus | null;
connectionStartHandle?: ConnectingHandle | null;
connectionEndHandle?: ConnectingHandle | null;
}) => void;
+10 -2
View File
@@ -1,4 +1,4 @@
import type { XYPosition, Position, Dimensions, OnConnect, Connection } from '.'; import type { XYPosition, Position, Dimensions, OnConnect, IsValidConnection } from '.';
export type HandleType = 'source' | 'target'; export type HandleType = 'source' | 'target';
@@ -14,6 +14,14 @@ export type ConnectingHandle = {
handleId?: string | null; handleId?: string | null;
}; };
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
};
export type HandleProps = { export type HandleProps = {
type: HandleType; type: HandleType;
position: Position; position: Position;
@@ -21,6 +29,6 @@ export type HandleProps = {
isConnectableStart?: boolean; isConnectableStart?: boolean;
isConnectableEnd?: boolean; isConnectableEnd?: boolean;
onConnect?: OnConnect; onConnect?: OnConnect;
isValidConnection?: (connection: Connection) => boolean; isValidConnection?: IsValidConnection;
id?: string; id?: string;
}; };
+1
View File
@@ -1,5 +1,6 @@
export * from './graph'; export * from './graph';
export * from './utils'; export * from './utils';
export * from './marker'; export * from './marker';
export * from './xyhandle';
export * from './xypanzoom'; export * from './xypanzoom';
export * from './xydrag'; export * from './xydrag';
+312
View File
@@ -0,0 +1,312 @@
import { getHostForElement, calcAutoPan, getEventPosition, pointToRendererPoint, rendererPointToPoint } from '..';
import {
OnConnect,
OnConnectStart,
HandleType,
Connection,
ConnectionMode,
PanBy,
BaseNode,
Transform,
ConnectingHandle,
OnConnectEnd,
UpdateConnection,
IsValidConnection,
ConnectionHandle,
} from '@reactflow/system';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType, resetRecentHandle } from './utils';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodes: BaseNode[];
transform: Transform;
lib: string;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
};
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;
};
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;
endHandle: ConnectingHandle | null;
};
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
const alwaysValid = () => true;
function onPointerDown(
event: MouseEvent | TouchEvent,
{
connectionMode,
connectionRadius,
handleId,
nodeId,
edgeUpdaterType,
isTarget,
domNode,
nodes,
transform,
lib,
autoPanOnConnect,
panBy,
cancelConnection,
onConnectStart,
onConnect,
onConnectEnd,
isValidConnection = alwaysValid,
onEdgeUpdateEnd,
updateConnection,
}: OnPointerDownParams
) {
// when react-flow 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;
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) {
return;
}
let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid = false;
let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({
nodes,
nodeId,
handleId,
handleType,
});
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
function autoPan(): void {
if (!autoPanOnConnect || !containerBounds) {
return;
}
const [x, y] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x, y });
autoPanId = requestAnimationFrame(autoPan);
}
updateConnection({
connectionPosition,
connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionStartHandle: {
nodeId,
handleId,
type: handleType,
},
connectionEndHandle: null,
});
onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) {
connectionPosition = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(event, {
handle: closestHandle,
connectionMode,
fromNodeId: nodeId,
fromHandleId: handleId,
fromType: isTarget ? 'target' : 'source',
isValidConnection,
doc,
lib,
});
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = result.isValid;
updateConnection({
connectionPosition:
closestHandle && isValid
? rendererPointToPoint(
{
x: closestHandle.x,
y: closestHandle.y,
},
transform
)
: connectionPosition,
connectionStatus: getConnectionStatus(!!closestHandle, isValid),
connectionEndHandle: result.endHandle,
});
if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle, lib);
}
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle, lib);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version
handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
handleDomNode.classList.toggle('valid', isValid);
handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid);
}
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((closestHandle || handleDomNode) && connection && isValid) {
onConnect?.(connection);
}
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
onConnectEnd?.(event);
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(prevActiveHandle, lib);
cancelConnection();
cancelAnimationFrame(autoPanId);
autoPanStarted = false;
isValid = false;
connection = null;
handleDomNode = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener);
doc.removeEventListener('touchmove', onPointerMove as EventListener);
doc.removeEventListener('touchend', onPointerUp as EventListener);
}
doc.addEventListener('mousemove', onPointerMove as EventListener);
doc.addEventListener('mouseup', onPointerUp as EventListener);
doc.addEventListener('touchmove', onPointerMove as EventListener);
doc.addEventListener('touchend', onPointerUp as EventListener);
}
// checks if and returns connection in fom of an object { source: 123, target: 312 }
function isValidHandle(
event: MouseEvent | TouchEvent,
{
handle,
connectionMode,
fromNodeId,
fromHandleId,
fromType,
doc,
lib,
isValidConnection = alwaysValid,
}: IsValidParams
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode;
const result: Result = {
handleDomNode: handleToCheck,
isValid: false,
connection: nullConnection,
endHandle: null,
};
console.log(1, result);
if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid');
const connectable = handleToCheck.classList.contains('connectable');
const connectableEnd = handleToCheck.classList.contains('connectableend');
const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId,
sourceHandle: isTarget ? handleId : fromHandleId,
target: isTarget ? fromNodeId : handleNodeId,
targetHandle: isTarget ? fromHandleId : handleId,
};
result.connection = connection;
const isConnectable = connectable && connectableEnd;
// in strict mode we don't allow target to target or source to source connections
const isValid =
isConnectable &&
(connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) {
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection);
}
}
return result;
}
export const XYHandle: XYHandleInstance = {
onPointerDown,
isValid: isValidHandle,
};
@@ -1,31 +1,17 @@
import { import {
internalsSymbol, internalsSymbol,
ConnectionMode,
ConnectionStatus, ConnectionStatus,
type ConnectingHandle,
type Connection,
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type BaseNode,
type ConnectionHandle,
} from '@reactflow/system'; } from '@reactflow/system';
import { getEventPosition } from '@reactflow/utils';
import type { Node } from '../../types';
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
};
export type ValidConnectionFunc = (connection: Connection) => boolean;
// 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( export function getHandles(
node: Node, node: BaseNode,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: string
@@ -75,83 +61,8 @@ export function getClosestHandle(
closestHandles.find((handle) => handle.type === 'target') || closestHandles[0]; closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
} }
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
endHandle: ConnectingHandle | null;
};
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
// checks if and returns connection in fom of an object { source: 123, target: 312 }
export function isValidHandle(
event: MouseEvent | TouchEvent,
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
connectionMode: ConnectionMode,
fromNodeId: string,
fromHandleId: string | null,
fromType: HandleType,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
const { x, y } = getEventPosition(event);
const handleBelow = doc.elementFromPoint(x, y);
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
const handleToCheck = handleBelow?.classList.contains('react-flow__handle') ? handleBelow : handleDomNode;
const result: Result = {
handleDomNode: handleToCheck,
isValid: false,
connection: nullConnection,
endHandle: null,
};
if (handleToCheck) {
const handleType = getHandleType(undefined, handleToCheck);
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
const handleId = handleToCheck.getAttribute('data-handleid');
const connectable = handleToCheck.classList.contains('connectable');
const connectableEnd = handleToCheck.classList.contains('connectableend');
const connection: Connection = {
source: isTarget ? handleNodeId : fromNodeId,
sourceHandle: isTarget ? handleId : fromHandleId,
target: isTarget ? fromNodeId : handleNodeId,
targetHandle: isTarget ? fromHandleId : handleId,
};
result.connection = connection;
const isConnectable = connectable && connectableEnd;
// in strict mode we don't allow target to target or source to source connections
const isValid =
isConnectable &&
(connectionMode === ConnectionMode.Strict
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
: handleNodeId !== fromNodeId || handleId !== fromHandleId);
if (isValid) {
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection);
}
}
return result;
}
type GetHandleLookupParams = { type GetHandleLookupParams = {
nodes: Node[]; nodes: BaseNode[];
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: string;
@@ -190,8 +101,8 @@ export function getHandleType(
return null; return null;
} }
export function resetRecentHandle(handleDomNode: Element): void { export function resetRecentHandle(handleDomNode: Element, lib: string): void {
handleDomNode?.classList.remove('valid', 'connecting', 'react-flow__handle-valid', 'react-flow__handle-connecting'); handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
} }
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {