refactor(store): connection data
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
@@ -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,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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
+6
-95
@@ -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) {
|
||||||
Reference in New Issue
Block a user