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

View File

@@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port 3000 --open --host",
"dev": "vite --port 3000 --open --host --force",
"serve": "vite serve --port 3000",
"build": "vite build",
"test:dev": "cypress open",
@@ -14,7 +14,6 @@
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
},
"dependencies": {
"@reactflow/node-resizer": "workspace:*",
"classcat": "^5.0.3",
"dagre": "^0.8.5",
"localforage": "^1.10.0",

View File

@@ -1,6 +1,6 @@
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) {
const connectionNodeId = useStore(connectionNodeIdSelector);

View File

@@ -43,7 +43,7 @@ const ConnectionLine = ({
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeInternals.get(nodeId),
handleId: s.connectionHandleId,
handleId: s.connectionStartHandle?.handleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
@@ -133,8 +133,8 @@ type ConnectionLineWrapperProps = {
};
const selector = (s: ReactFlowState) => ({
nodeId: s.connectionNodeId,
handleType: s.connectionHandleType,
nodeId: s.connectionStartHandle?.nodeId,
handleType: s.connectionStartHandle?.type,
nodesConnectable: s.nodesConnectable,
connectionStatus: s.connectionStatus,
width: s.width,

View File

@@ -1,11 +1,10 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { getMarkerId, elementSelectionKeys } from '@reactflow/utils';
import { getMarkerId, elementSelectionKeys, XYHandle } from '@reactflow/utils';
import type { Connection } from '@reactflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { handlePointerDown } from '../Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
@@ -96,7 +95,22 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
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 handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
@@ -104,6 +118,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
const nodes = getNodes();
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
@@ -115,17 +130,26 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
handlePointerDown({
event,
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
onConnect: onConnectEdge,
nodes,
isTarget,
getState: store.getState,
setState: store.setState,
isValidConnection,
edgeUpdaterType: handleType,
transform,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
});
};

View File

@@ -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);
}

View File

@@ -2,16 +2,12 @@ import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchE
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
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 { useNodeId } from '../../contexts/NodeIdContext';
import { handlePointerDown } from './handler';
import { addEdge } from '../../utils/';
import { type ReactFlowState } from '../../types';
import { isValidHandle } from './utils';
const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
@@ -95,15 +91,26 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
isConnectableStart &&
((isMouseTriggered && (event as ReactMouseEvent<HTMLDivElement>).button === 0) || !isMouseTriggered)
) {
handlePointerDown({
event,
const currentStore = store.getState();
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,
nodeId,
panBy: currentStore.panBy,
cancelConnection: currentStore.cancelConnection,
onConnectStart: currentStore.onConnectStart,
onConnectEnd: currentStore.onConnectEnd,
updateConnection: currentStore.updateConnection,
onConnect: onConnectExtended,
isTarget,
getState: store.getState,
setState: store.setState,
isValidConnection: isValidConnection || store.getState().isValidConnection || alwaysValid,
isValidConnection: isValidConnection || currentStore.isValidConnection,
});
}
@@ -121,6 +128,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
connectionClickStartHandle,
connectionMode,
isValidConnection: isValidConnectionStore,
lib,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
@@ -128,27 +136,27 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}
if (!connectionClickStartHandle) {
onClickConnectStart?.(event, { nodeId, handleId, handleType: type });
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } });
return;
}
const doc = getHostForElement(event.target as HTMLElement);
const isValidConnectionHandler = isValidConnection || isValidConnectionStore || alwaysValid;
const { connection, isValid } = isValidHandle(
event.nativeEvent,
{
const isValidConnectionHandler = isValidConnection || isValidConnectionStore;
const { connection, isValid } = XYHandle.isValid(event.nativeEvent, {
handle: {
nodeId,
id: handleId,
type,
},
connectionMode,
connectionClickStartHandle.nodeId,
connectionClickStartHandle.handleId || null,
connectionClickStartHandle.type,
isValidConnectionHandler,
doc
);
fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null,
fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler,
doc,
lib,
});
if (isValid) {
onConnectExtended(connection);

View File

@@ -1,12 +1,6 @@
import { createStore } from 'zustand';
import { clampPosition, getDimensions, fitView, getHandleBounds } from '@reactflow/utils';
import {
internalsSymbol,
type NodeDimensionUpdate,
type CoordinateExtent,
type NodeDragItem,
type XYPosition,
} from '@reactflow/system';
import { internalsSymbol, type CoordinateExtent } from '@reactflow/system';
import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import { createNodeInternals, updateAbsoluteNodePositions, updateNodesAndEdgesSelections } from './utils';
@@ -20,7 +14,6 @@ import type {
NodeSelectionChange,
NodePositionChange,
UnselectNodesAndEdgesParams,
NodeChange,
} from '../types';
const createRFStore = () =>
@@ -48,7 +41,7 @@ const createRFStore = () =>
set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges });
},
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => {
updateNodeDimensions: (updates) => {
const {
onNodesChange,
nodeInternals,
@@ -132,7 +125,7 @@ const createRFStore = () =>
onNodesChange?.(changes);
}
},
updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => {
updateNodePositions: (nodeDragItems, positionChanged = true, dragging = false) => {
const { triggerNodeChanges } = get();
const changes = nodeDragItems.map((node) => {
@@ -153,7 +146,7 @@ const createRFStore = () =>
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes: NodeChange[]) => {
triggerNodeChanges: (changes) => {
const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin, getNodes, elevateNodesOnSelect } = get();
if (changes?.length) {
@@ -167,7 +160,7 @@ const createRFStore = () =>
}
},
addSelectedNodes: (selectedNodeIds: string[]) => {
addSelectedNodes: (selectedNodeIds) => {
const { multiSelectionActive, edges, getNodes } = get();
let changedNodes: NodeSelectionChange[];
let changedEdges: EdgeSelectionChange[] | null = null;
@@ -186,7 +179,7 @@ const createRFStore = () =>
set,
});
},
addSelectedEdges: (selectedEdgeIds: string[]) => {
addSelectedEdges: (selectedEdgeIds) => {
const { multiSelectionActive, edges, getNodes } = get();
let changedEdges: EdgeSelectionChange[];
let changedNodes: NodeSelectionChange[] | null = null;
@@ -225,19 +218,19 @@ const createRFStore = () =>
set,
});
},
setMinZoom: (minZoom: number) => {
setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ minZoom });
},
setMaxZoom: (maxZoom: number) => {
setMaxZoom: (maxZoom) => {
const { panZoom, minZoom } = get();
panZoom?.setScaleExtent([minZoom, maxZoom]);
set({ maxZoom });
},
setTranslateExtent: (translateExtent: CoordinateExtent) => {
setTranslateExtent: (translateExtent) => {
get().panZoom?.setTranslateExtent(translateExtent);
set({ translateExtent });
@@ -260,7 +253,7 @@ const createRFStore = () =>
set,
});
},
setNodeExtent: (nodeExtent: CoordinateExtent) => {
setNodeExtent: (nodeExtent) => {
const { nodeInternals } = get();
nodeInternals.forEach((node) => {
@@ -272,7 +265,7 @@ const createRFStore = () =>
nodeInternals: new Map(nodeInternals),
});
},
panBy: (delta: XYPosition): boolean => {
panBy: (delta): boolean => {
const { transform, width, height, panZoom, translateExtent } = get();
if (!panZoom || (!delta.x && !delta.y)) {
@@ -300,13 +293,22 @@ const createRFStore = () =>
},
cancelConnection: () =>
set({
connectionNodeId: initialState.connectionNodeId,
connectionHandleId: initialState.connectionHandleId,
connectionHandleType: initialState.connectionHandleType,
connectionStatus: initialState.connectionStatus,
connectionStartHandle: initialState.connectionStartHandle,
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 }),
}));

View File

@@ -22,9 +22,6 @@ const initialState: ReactFlowStore = {
nodesSelectionActive: false,
userSelectionActive: false,
userSelectionRect: null,
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict,
@@ -61,6 +58,8 @@ const initialState: ReactFlowStore = {
connectionRadius: 20,
onError: devWarn,
isValidConnection: undefined,
lib: 'react',
};
export default initialState;

View File

@@ -3,6 +3,8 @@ import type {
ConnectionMode,
ConnectionLineType,
OnConnect,
OnConnectStart,
OnConnectEnd,
CoordinateExtent,
KeyCode,
PanOnScrollMode,
@@ -16,6 +18,7 @@ import type {
HandleType,
SelectionMode,
OnError,
IsValidConnection,
} from '@reactflow/system';
import type {
@@ -25,8 +28,6 @@ import type {
Node,
Edge,
ConnectionLineComponent,
OnConnectStart,
OnConnectEnd,
OnEdgeUpdateFunc,
OnInit,
DefaultEdgeOptions,
@@ -40,7 +41,6 @@ import type {
SelectionDragHandler,
EdgeMouseHandler,
} from '.';
import { ValidConnectionFunc } from '../components/Handle/utils';
export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
nodes?: Node[];
@@ -148,7 +148,7 @@ export type ReactFlowProps = HTMLAttributes<HTMLDivElement> & {
autoPanOnConnect?: boolean;
connectionRadius?: number;
onError?: OnError;
isValidConnection?: ValidConnectionFunc;
isValidConnection?: IsValidConnection;
};
export type ReactFlowRefType = HTMLDivElement;

View File

@@ -1,15 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
ComponentType,
MemoExoticComponent,
} from 'react';
import type { ComponentType, MemoExoticComponent } from 'react';
import {
FitViewParamsBase,
FitViewOptionsBase,
NodeProps,
OnConnectStartParams,
ZoomInOut,
ZoomTo,
SetViewport,
@@ -18,7 +12,6 @@ import {
SetCenter,
FitBounds,
Project,
Connection,
} from '@reactflow/system';
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 OnConnectStart = (event: ReactMouseEvent | ReactTouchEvent, params: OnConnectStartParams) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type FitViewParams = FitViewParamsBase<Node>;
export type FitViewOptions = FitViewOptionsBase<Node>;
export type FitView = (fitViewOptions?: FitViewOptions) => boolean;
export type OnInit<NodeData = any, EdgeData = any> = (reactFlowInstance: ReactFlowInstance<NodeData, EdgeData>) => void;
export type IsValidConnection = (edge: Edge | Connection) => boolean;
export type ViewportHelperFunctions = {
zoomIn: ZoomInOut;
zoomOut: ZoomInOut;

View File

@@ -2,7 +2,6 @@ import {
ConnectionMode,
type ConnectionStatus,
type CoordinateExtent,
type HandleType,
type NodeDimensionUpdate,
type UpdateNodePositions,
type NodeOrigin,
@@ -16,11 +15,15 @@ import {
type XYPosition,
type PanZoomInstance,
type PanBy,
OnNodeDrag,
OnSelectionDrag,
OnMoveStart,
OnMove,
OnMoveEnd,
type OnConnectStart,
type OnConnectEnd,
type OnNodeDrag,
type OnSelectionDrag,
type OnMoveStart,
type OnMove,
type OnMoveEnd,
type IsValidConnection,
type UpdateConnection,
} from '@reactflow/system';
import type {
@@ -30,15 +33,12 @@ import type {
OnNodesChange,
OnEdgesChange,
NodeInternals,
OnConnectStart,
OnConnectEnd,
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete,
OnSelectionChangeFunc,
UnselectNodesAndEdgesParams,
IsValidConnection,
} from '.';
export type ReactFlowStore = {
@@ -67,9 +67,6 @@ export type ReactFlowStore = {
userSelectionActive: boolean;
userSelectionRect: SelectionRect | null;
connectionNodeId: string | null;
connectionHandleId: string | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
connectionStatus: ConnectionStatus | null;
connectionMode: ConnectionMode;
@@ -135,6 +132,8 @@ export type ReactFlowStore = {
connectionRadius: number;
isValidConnection?: IsValidConnection;
lib: string;
};
export type ReactFlowActions = {
@@ -153,6 +152,8 @@ export type ReactFlowActions = {
setTranslateExtent: (translateExtent: CoordinateExtent) => void;
setNodeExtent: (nodeExtent: CoordinateExtent) => void;
cancelConnection: () => void;
// @todo can this be reused by system?
updateConnection: UpdateConnection;
reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void;
panBy: PanBy;

View File

@@ -8,7 +8,7 @@
{#if $connectionPath}
<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" />
</g>
</svg>

View File

@@ -2,9 +2,8 @@
import { getContext, createEventDispatcher } from 'svelte';
import cc from 'classcat';
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 type { HandleComponentProps } from '$lib/types';
@@ -23,7 +22,8 @@
const handleId = id || null;
const dispatch = createEventDispatcher();
const store = useStore();
const {
connectionMode,
domNode,
@@ -31,11 +31,12 @@
connectionRadius,
transform,
isValidConnection,
lib,
addEdge,
panBy,
cancelConnection,
updateConnection,
} = useStore();
} = store;
function dispatchEvent(eventName: string, params?: Connection) {
dispatch(eventName, params || { nodeId, handleId, type });
@@ -50,23 +51,22 @@
const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
handlePointerDown({
event,
XYHandle.onPointerDown(event, {
handleId,
nodeId,
isTarget,
connectionRadius: $connectionRadius,
domNode: $domNode,
nodes,
nodes: $nodes,
connectionMode: $connectionMode,
transform,
transform: $transform,
lib: $lib,
autoPanOnConnect: true,
isValidConnection: $isValidConnection,
onConnect: onConnectExtended,
updateConnection,
cancelConnection,
panBy,
onConnectStart: () => dispatchEvent('connect:start'),
onConnectEnd: () => dispatchEvent('connect:end')
onConnect: onConnectExtended,
});
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -36,6 +36,11 @@
import { useStore } from '$lib/store';
import { getConnectedEdges } from '$lib/utils';
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 {
@@ -133,9 +138,9 @@
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
// if (!userSelectionActive && userSelectionRect && event.target === container.current) {
// onClick?.(event);
// }
if (!isSelecting && $selectionRectMode === 'user' && event.target === container) {
onClick?.(event);
}
selectionRect.set(null);
if (selectedNodes.length > 0) {
@@ -155,10 +160,10 @@
};
const onContextMenu = (event: MouseEvent) => {
// if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
// event.preventDefault();
// return;
// }
if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
event.preventDefault();
return;
}
dispatch('pane:contextmenu', event);
};

View File

@@ -0,0 +1,3 @@
export type PaneProps = {
panOnDrag?: boolean | number[];
};

View File

@@ -167,7 +167,7 @@
{panOnScroll}
{panOnDrag}
>
<Pane on:pane:click>
<Pane on:pane:click {panOnDrag}>
<ViewportComponent>
<EdgeRenderer on:edge:click />
<ConnectionLine />

View File

@@ -11,17 +11,11 @@ import type {
OnMove,
OnMoveEnd,
CoordinateExtent,
PanOnScrollMode
PanOnScrollMode,
IsValidConnection
} from '@reactflow/system';
import type {
Edge,
Node,
NodeTypes,
KeyDefinition,
EdgeTypes,
IsValidConnection
} from '$lib/types';
import type { Edge, Node, NodeTypes, KeyDefinition, EdgeTypes } from '$lib/types';
export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
id?: string;

View File

@@ -21,20 +21,23 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
store.transform
],
([connection, connectionLineType, connectionMode, nodes, transform]) => {
if (!connection.nodeId) {
if (!connection.connectionStartHandle?.nodeId) {
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 handleBoundsStrict = fromHandleBounds?.[connection.handleType || 'source'] || [];
const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[connection.handleType === 'source' ? 'target' : 'source']!;
: fromHandleBounds?.[
connection?.connectionStartHandle?.type === 'source' ? 'target' : 'source'
]!;
const handleBounds =
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.handleId
? handleBounds.find((d) => d.id === connection.handleId)
const fromHandle = connection.connectionStartHandle?.handleId
? handleBounds.find((d) => d.id === connection.connectionStartHandle?.handleId)
: handleBounds[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
@@ -49,8 +52,8 @@ export function getConnectionPath(store: SvelteFlowStoreState) {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: ((connection.position?.x ?? 0) - transform[0]) / transform[2],
targetY: ((connection.position?.y ?? 0) - transform[1]) / transform[2],
targetX: ((connection.connectionPosition?.x ?? 0) - transform[0]) / transform[2],
targetY: ((connection.connectionPosition?.y ?? 0) - transform[1]) / transform[2],
targetPosition: toPosition
};

View File

@@ -7,7 +7,8 @@ import {
type ViewportHelperFunctionOptions,
type Connection,
type XYPosition,
type CoordinateExtent
type CoordinateExtent,
type UpdateConnection
} from '@reactflow/system';
import {
createMarkerIds,
@@ -304,21 +305,22 @@ export function createStore(params: CreateStoreParams): SvelteFlowStore {
return transformChanged;
}
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) {
const updateConnection: UpdateConnection = (update) => {
const currentConnectionData = get(store.connection);
const nextConnectionData = currentConnectionData
? {
...initConnectionData,
...currentConnectionData,
...connectionUpdate
...update
}
: {
...initConnectionData,
...connectionUpdate
...update
};
store.connection.set(nextConnectionData);
}
};
function cancelConnection() {
updateConnection(initConnectionData);

View File

@@ -8,7 +8,8 @@ import {
type SnapGrid,
type MarkerProps,
type PanZoomInstance,
type CoordinateExtent
type CoordinateExtent,
type IsValidConnection
} from '@reactflow/system';
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 SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
import StepEdge from '$lib/components/edges/StepEdge.svelte';
import type {
ConnectionData,
NodeTypes,
EdgeTypes,
EdgeLayouted,
Edge,
Node,
IsValidConnection
} from '$lib/types';
import type { ConnectionData, NodeTypes, EdgeTypes, EdgeLayouted, Edge, Node } from '$lib/types';
import { infiniteExtent } from '@reactflow/utils';
export const initConnectionData = {
nodeId: null,
handleId: null,
handleType: null,
position: null,
status: null
connectionStartHandle: null,
connectionEndHandle: null,
connectionPosition: null,
connectionStatus: null
};
export const initialNodeTypes = {
@@ -88,5 +80,6 @@ export const initialStoreState = {
elementsSelectable: writable<boolean>(true),
selectNodesOnDrag: writable<boolean>(true),
markers: readable<MarkerProps[]>([]),
defaultMarkerColor: writable<string>('#b1b1b7')
defaultMarkerColor: writable<string>('#b1b1b7'),
lib: readable<string>('svelte')
};

View File

@@ -4,11 +4,12 @@ import type {
ViewportHelperFunctionOptions,
Connection,
UpdateNodePositions,
CoordinateExtent
CoordinateExtent,
UpdateConnection
} from '@reactflow/system';
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';
export type SvelteFlowStoreActions = {
@@ -27,7 +28,7 @@ export type SvelteFlowStoreActions = {
addSelectedNodes: (ids: string[]) => void;
addSelectedEdges: (ids: string[]) => void;
panBy: (delta: XYPosition) => boolean;
updateConnection: (connection: Partial<ConnectionData>) => void;
updateConnection: UpdateConnection;
cancelConnection: () => void;
reset(): void;
};

View File

@@ -1,10 +1,10 @@
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type {
Connection,
FitViewOptionsBase,
HandleType,
Position,
XYPosition
XYPosition,
ConnectingHandle
} from '@reactflow/system';
import type { Node } from './nodes';
@@ -14,11 +14,10 @@ export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = {
position: XYPosition | null;
nodeId: string | null;
handleId: string | null;
handleType: HandleType | null;
status: string | null;
connectionPosition: XYPosition | null;
connectionStartHandle: ConnectingHandle | null;
connectionEndHandle: ConnectingHandle | null;
connectionStatus: string | null;
};
export type HandleComponentProps = {
@@ -30,8 +29,3 @@ export type HandleComponentProps = {
};
export type FitViewOptions = FitViewOptionsBase<Node>;
export type IsValidConnection = (
connection: Connection,
{ fromNode, toNode }: { fromNode: Node; toNode: Node }
) => boolean;

View File

@@ -3,8 +3,9 @@ import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavi
import type { XYPosition, Rect } from './utils';
import type { BaseNode, NodeDragItem, NodeOrigin } from './nodes';
import type { HandleType } from './handles';
import type { ConnectingHandle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom';
import { BaseEdge } from '..';
export type Project = (position: XYPosition) => XYPosition;
@@ -34,7 +35,17 @@ export enum ConnectionMode {
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 OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type IsValidConnection = (edge: BaseEdge | Connection) => boolean;
export type FitViewParamsBase<NodeType extends BaseNode> = {
nodes: NodeType[];
@@ -55,12 +66,6 @@ export type FitViewOptionsBase<NodeType extends BaseNode> = {
nodes?: (Partial<NodeType> & { id: NodeType['id'] })[];
};
export type OnConnectStartParams = {
nodeId: string | null;
handleId: string | null;
handleType: HandleType | null;
};
export type Viewport = {
x: number;
y: number;
@@ -124,3 +129,10 @@ export type UpdateNodePositions = (
dragging?: boolean
) => void;
export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: {
connectionPosition?: XYPosition | null;
connectionStatus?: ConnectionStatus | null;
connectionStartHandle?: ConnectingHandle | null;
connectionEndHandle?: ConnectingHandle | null;
}) => void;

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';
@@ -14,6 +14,14 @@ export type ConnectingHandle = {
handleId?: string | null;
};
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
};
export type HandleProps = {
type: HandleType;
position: Position;
@@ -21,6 +29,6 @@ export type HandleProps = {
isConnectableStart?: boolean;
isConnectableEnd?: boolean;
onConnect?: OnConnect;
isValidConnection?: (connection: Connection) => boolean;
isValidConnection?: IsValidConnection;
id?: string;
};

View File

@@ -1,5 +1,6 @@
export * from './graph';
export * from './utils';
export * from './marker';
export * from './xyhandle';
export * from './xypanzoom';
export * from './xydrag';

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,
};

View File

@@ -1,31 +1,17 @@
import {
internalsSymbol,
ConnectionMode,
ConnectionStatus,
type ConnectingHandle,
type Connection,
type HandleType,
type NodeHandleBounds,
type XYPosition,
type BaseNode,
type ConnectionHandle,
} 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
// so that we can later find the closest handle to the mouse position
export function getHandles(
node: Node,
node: BaseNode,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
@@ -75,83 +61,8 @@ export function getClosestHandle(
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 = {
nodes: Node[];
nodes: BaseNode[];
nodeId: string;
handleId: string | null;
handleType: string;
@@ -190,8 +101,8 @@ export function getHandleType(
return null;
}
export function resetRecentHandle(handleDomNode: Element): void {
handleDomNode?.classList.remove('valid', 'connecting', 'react-flow__handle-valid', 'react-flow__handle-connecting');
export function resetRecentHandle(handleDomNode: Element, lib: string): void {
handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
}
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {