) => {
setPosition,
onConnectEdge,
isTarget,
- isValidConnection
+ isValidConnection,
+ connectionMode
);
},
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition]
diff --git a/src/components/Handle/BaseHandle.tsx b/src/components/Handle/BaseHandle.tsx
deleted file mode 100644
index 4b7921a0..00000000
--- a/src/components/Handle/BaseHandle.tsx
+++ /dev/null
@@ -1,229 +0,0 @@
-import React, { memo, MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
-import cc from 'classcat';
-
-import {
- HandleType,
- ElementId,
- Position,
- XYPosition,
- OnConnectFunc,
- OnConnectStartFunc,
- OnConnectStopFunc,
- OnConnectEndFunc,
- Connection,
- SetConnectionId,
-} from '../../types';
-
-type ValidConnectionFunc = (connection: Connection) => boolean;
-export type SetSourceIdFunc = (params: SetConnectionId) => void;
-
-interface BaseHandleProps {
- type: HandleType;
- nodeId: ElementId;
- onConnect: OnConnectFunc;
- onConnectStart?: OnConnectStartFunc;
- onConnectStop?: OnConnectStopFunc;
- onConnectEnd?: OnConnectEndFunc;
- position: Position;
- setConnectionNodeId: SetSourceIdFunc;
- setPosition: (pos: XYPosition) => void;
- isValidConnection: ValidConnectionFunc;
- id?: ElementId | null;
- className?: string;
- style?: CSSProperties;
-}
-
-type Result = {
- elementBelow: Element | null;
- isValid: boolean;
- connection: Connection;
- isHoveringHandle: boolean;
-};
-
-export function onMouseDown(
- event: ReactMouseEvent,
- handleId: ElementId | null,
- nodeId: ElementId,
- setConnectionNodeId: SetSourceIdFunc,
- setPosition: (pos: XYPosition) => void,
- onConnect: OnConnectFunc,
- isTarget: boolean,
- isValidConnection: ValidConnectionFunc,
- onConnectStart?: OnConnectStartFunc,
- onConnectStop?: OnConnectStopFunc,
- onConnectEnd?: OnConnectEndFunc
-): void {
- const reactFlowNode = (event.target as Element).closest('.react-flow');
-
- if (!reactFlowNode) {
- return;
- }
-
- const handleType = isTarget ? 'target' : 'source';
- const containerBounds = reactFlowNode.getBoundingClientRect();
- let recentHoveredHandle: Element;
-
- setPosition({
- x: event.clientX - containerBounds.left,
- y: event.clientY - containerBounds.top,
- });
-
- setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
-
- if (onConnectStart) {
- onConnectStart(event, { nodeId, handleId, handleType });
- }
-
- function resetRecentHandle(): void {
- if (!recentHoveredHandle) {
- return;
- }
-
- recentHoveredHandle.classList.remove('react-flow__handle-valid');
- recentHoveredHandle.classList.remove('react-flow__handle-connecting');
- }
-
- // checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
- function checkElementBelowIsValid(event: MouseEvent) {
- const elementBelow = document.elementFromPoint(event.clientX, event.clientY);
-
- const result: Result = {
- elementBelow,
- isValid: false,
- connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
- isHoveringHandle: false,
- };
-
- if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
- result.isHoveringHandle = true;
- if (
- (isTarget && elementBelow.classList.contains('source')) ||
- (!isTarget && elementBelow.classList.contains('target'))
- ) {
- let connection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
-
- if (isTarget) {
- const sourceId = elementBelow.getAttribute('data-nodeid');
- const sourcehandleId = elementBelow.getAttribute('data-handleid');
- connection = { source: sourceId, sourceHandle: sourcehandleId, target: nodeId, targetHandle: handleId };
- } else {
- const targetId = elementBelow.getAttribute('data-nodeid');
- const targetHandleId = elementBelow.getAttribute('data-handleid');
- connection = { source: nodeId, sourceHandle: handleId, target: targetId, targetHandle: targetHandleId };
- }
-
- const isValid = isValidConnection(connection);
-
- result.connection = connection;
- result.isValid = isValid;
- }
- }
-
- return result;
- }
-
- function onMouseMove(event: MouseEvent) {
- setPosition({
- x: event.clientX - containerBounds.left,
- y: event.clientY - containerBounds.top,
- });
-
- const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(event);
-
- if (!isHoveringHandle) {
- return resetRecentHandle();
- }
-
- const isOwnHandle = connection.source === connection.target;
-
- if (!isOwnHandle && elementBelow) {
- recentHoveredHandle = elementBelow;
- elementBelow.classList.add('react-flow__handle-connecting');
- elementBelow.classList.toggle('react-flow__handle-valid', isValid);
- }
- }
-
- function onMouseUp(event: MouseEvent) {
- const { connection, isValid } = checkElementBelowIsValid(event);
-
- if (onConnectStop) {
- onConnectStop(event);
- }
-
- if (isValid && onConnect) {
- onConnect(connection);
- }
-
- if (onConnectEnd) {
- onConnectEnd(event);
- }
-
- resetRecentHandle();
- setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
-
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
- }
-
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
-}
-
-const BaseHandle = ({
- type,
- nodeId,
- onConnect,
- onConnectStart,
- onConnectStop,
- onConnectEnd,
- position,
- setConnectionNodeId,
- setPosition,
- className,
- id = null,
- isValidConnection,
- ...rest
-}: BaseHandleProps) => {
- const isTarget = type === 'target';
- const handleClasses = cc([
- 'react-flow__handle',
- `react-flow__handle-${position}`,
- 'nodrag',
- className,
- {
- source: !isTarget,
- target: isTarget,
- },
- ]);
-
- const handleId = id || null;
-
- return (
-
- onMouseDown(
- event,
- handleId,
- nodeId,
- setConnectionNodeId,
- setPosition,
- onConnect,
- isTarget,
- isValidConnection,
- onConnectStart,
- onConnectStop,
- onConnectEnd
- )
- }
- {...rest}
- />
- );
-};
-
-BaseHandle.displayName = 'BaseHandle';
-
-export default memo(BaseHandle);
diff --git a/src/components/Handle/handler.ts b/src/components/Handle/handler.ts
new file mode 100644
index 00000000..ea72cc6d
--- /dev/null
+++ b/src/components/Handle/handler.ts
@@ -0,0 +1,171 @@
+import { MouseEvent as ReactMouseEvent } from 'react';
+
+import {
+ ElementId,
+ XYPosition,
+ OnConnectFunc,
+ OnConnectStartFunc,
+ OnConnectStopFunc,
+ OnConnectEndFunc,
+ ConnectionMode,
+ SetConnectionId,
+ Connection,
+} from '../../types';
+
+type ValidConnectionFunc = (connection: Connection) => boolean;
+export type SetSourceIdFunc = (params: SetConnectionId) => void;
+
+type Result = {
+ elementBelow: Element | null;
+ isValid: boolean;
+ connection: Connection;
+ isHoveringHandle: boolean;
+};
+
+// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
+function checkElementBelowIsValid(
+ event: MouseEvent,
+ connectionMode: ConnectionMode,
+ isTarget: boolean,
+ nodeId: ElementId,
+ handleId: ElementId | null,
+ isValidConnection: ValidConnectionFunc
+) {
+ const elementBelow = document.elementFromPoint(event.clientX, event.clientY);
+ const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
+ const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
+
+ const result: Result = {
+ elementBelow,
+ isValid: false,
+ connection: { source: null, target: null, sourceHandle: null, targetHandle: null },
+ isHoveringHandle: false,
+ };
+
+ if (elementBelow && (elementBelowIsTarget || elementBelowIsSource)) {
+ result.isHoveringHandle = true;
+
+ // in strict mode we don't allow target to target or source to source connections
+ const isValid =
+ connectionMode === ConnectionMode.Strict
+ ? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget)
+ : true;
+
+ if (isValid) {
+ const elementBelowNodeId = elementBelow.getAttribute('data-nodeid');
+ const elementBelowHandleId = elementBelow.getAttribute('data-handleid');
+ const connection: Connection = isTarget
+ ? {
+ source: elementBelowNodeId,
+ sourceHandle: elementBelowHandleId,
+ target: nodeId,
+ targetHandle: handleId,
+ }
+ : {
+ source: nodeId,
+ sourceHandle: handleId,
+ target: elementBelowNodeId,
+ targetHandle: elementBelowHandleId,
+ };
+
+ result.connection = connection;
+ result.isValid = isValidConnection(connection);
+ }
+ }
+
+ return result;
+}
+
+function resetRecentHandle(hoveredHandle: Element): void {
+ hoveredHandle?.classList.remove('react-flow__handle-valid');
+ hoveredHandle?.classList.remove('react-flow__handle-connecting');
+}
+
+export function onMouseDown(
+ event: ReactMouseEvent,
+ handleId: ElementId | null,
+ nodeId: ElementId,
+ setConnectionNodeId: SetSourceIdFunc,
+ setPosition: (pos: XYPosition) => void,
+ onConnect: OnConnectFunc,
+ isTarget: boolean,
+ isValidConnection: ValidConnectionFunc,
+ connectionMode: ConnectionMode,
+ onConnectStart?: OnConnectStartFunc,
+ onConnectStop?: OnConnectStopFunc,
+ onConnectEnd?: OnConnectEndFunc
+): void {
+ const reactFlowNode = (event.target as Element).closest('.react-flow');
+
+ if (!reactFlowNode) {
+ return;
+ }
+
+ const handleType = isTarget ? 'target' : 'source';
+ const containerBounds = reactFlowNode.getBoundingClientRect();
+ let recentHoveredHandle: Element;
+
+ setPosition({
+ x: event.clientX - containerBounds.left,
+ y: event.clientY - containerBounds.top,
+ });
+
+ setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
+ onConnectStart?.(event, { nodeId, handleId, handleType });
+
+ function onMouseMove(event: MouseEvent) {
+ setPosition({
+ x: event.clientX - containerBounds.left,
+ y: event.clientY - containerBounds.top,
+ });
+
+ const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
+ event,
+ connectionMode,
+ isTarget,
+ nodeId,
+ handleId,
+ isValidConnection
+ );
+
+ if (!isHoveringHandle) {
+ return resetRecentHandle(recentHoveredHandle);
+ }
+
+ const isOwnHandle = connection.source === connection.target;
+
+ if (!isOwnHandle && elementBelow) {
+ recentHoveredHandle = elementBelow;
+ elementBelow.classList.add('react-flow__handle-connecting');
+ elementBelow.classList.toggle('react-flow__handle-valid', isValid);
+ }
+ }
+
+ function onMouseUp(event: MouseEvent) {
+ const { connection, isValid } = checkElementBelowIsValid(
+ event,
+ connectionMode,
+ isTarget,
+ nodeId,
+ handleId,
+ isValidConnection
+ );
+
+ onConnectStop?.(event);
+
+ if (isValid) {
+ onConnect?.(connection);
+ }
+
+ onConnectEnd?.(event);
+
+ resetRecentHandle(recentHoveredHandle);
+ setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
+
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('mouseup', onMouseUp);
+ }
+
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('mouseup', onMouseUp);
+}
diff --git a/src/components/Handle/index.tsx b/src/components/Handle/index.tsx
index 627afb86..1fb15a42 100644
--- a/src/components/Handle/index.tsx
+++ b/src/components/Handle/index.tsx
@@ -1,16 +1,18 @@
-import React, { memo, useContext } from 'react';
+import React, { memo, useContext, useCallback } from 'react';
import cc from 'classcat';
import { useStoreActions, useStoreState } from '../../store/hooks';
-import BaseHandle from './BaseHandle';
import NodeIdContext from '../../contexts/NodeIdContext';
+import { HandleProps, Connection, ElementId, Position } from '../../types';
-import { HandleProps, ElementId, Position, Connection } from '../../types';
+import { onMouseDown } from './handler';
+
+const alwaysValid = () => true;
const Handle = ({
type = 'source',
position = Position.Top,
- isValidConnection = () => true,
+ isValidConnection = alwaysValid,
isConnectable = true,
style,
className,
@@ -24,32 +26,69 @@ const Handle = ({
const onConnectStart = useStoreState((state) => state.onConnectStart);
const onConnectStop = useStoreState((state) => state.onConnectStop);
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
+ const connectionMode = useStoreState((state) => state.connectionMode);
+ const handleId = id || null;
+ const isTarget = type === 'target';
- const onConnectExtended = (params: Connection) => {
- if (onConnectAction) {
- onConnectAction(params);
- }
+ const onConnectExtended = useCallback(
+ (params: Connection) => {
+ onConnectAction?.(params);
+ onConnect?.(params);
+ },
+ [onConnectAction, onConnect]
+ );
- if (onConnect) {
- onConnect(params);
- }
- };
- const handleClasses = cc([className, { connectable: isConnectable }]);
+ const onMouseDownHandler = useCallback(
+ (event: React.MouseEvent) => {
+ onMouseDown(
+ event,
+ handleId,
+ nodeId,
+ setConnectionNodeId,
+ setPosition,
+ onConnectExtended,
+ isTarget,
+ isValidConnection,
+ connectionMode,
+ onConnectStart,
+ onConnectStop,
+ onConnectEnd
+ );
+ },
+ [
+ handleId,
+ nodeId,
+ setConnectionNodeId,
+ setPosition,
+ onConnectExtended,
+ isTarget,
+ isValidConnection,
+ connectionMode,
+ onConnectStart,
+ onConnectStop,
+ onConnectEnd,
+ ]
+ );
+
+ const handleClasses = cc([
+ 'react-flow__handle',
+ `react-flow__handle-${position}`,
+ 'nodrag',
+ className,
+ {
+ source: !isTarget,
+ target: isTarget,
+ connectable: isConnectable,
+ },
+ ]);
return (
-
);
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx
index 683829df..9a649e07 100644
--- a/src/container/GraphView/index.tsx
+++ b/src/container/GraphView/index.tsx
@@ -45,6 +45,7 @@ const GraphView = ({
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
+ connectionMode,
connectionLineType,
connectionLineStyle,
connectionLineComponent,
@@ -94,6 +95,7 @@ const GraphView = ({
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
+ const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
@@ -188,6 +190,12 @@ const GraphView = ({
}
}, [translateExtent]);
+ useEffect(() => {
+ if (typeof connectionMode !== 'undefined') {
+ setConnectionMode(connectionMode);
+ }
+ }, [connectionMode]);
+
return (
, 'on
onPaneContextMenu?: (event: MouseEvent) => void;
nodeTypes?: NodeTypesType;
edgeTypes?: EdgeTypesType;
+ connectionMode?: ConnectionMode;
connectionLineType?: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
@@ -129,6 +131,7 @@ const ReactFlow = ({
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
+ connectionMode,
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
connectionLineComponent,
@@ -183,6 +186,7 @@ const ReactFlow = ({
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesParsed}
edgeTypes={edgeTypesParsed}
+ connectionMode={connectionMode}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
diff --git a/src/store/index.ts b/src/store/index.ts
index 851e1f76..e9f3973a 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -26,6 +26,7 @@ import {
NodeDiffUpdate,
TranslateExtent,
SnapGrid,
+ ConnectionMode,
} from '../types';
type NodeDimensionUpdate = {
@@ -70,6 +71,7 @@ export interface StoreModel {
connectionHandleId: ElementId | null;
connectionHandleType: HandleType | null;
connectionPosition: XYPosition;
+ connectionMode: ConnectionMode;
snapToGrid: boolean;
snapGrid: SnapGrid;
@@ -136,6 +138,8 @@ export interface StoreModel {
unsetUserSelection: Action;
setMultiSelectionActive: Action;
+
+ setConnectionMode: Action;
}
export const storeModel: StoreModel = {
@@ -175,6 +179,7 @@ export const storeModel: StoreModel = {
connectionHandleId: null,
connectionHandleType: 'source',
connectionPosition: { x: 0, y: 0 },
+ connectionMode: ConnectionMode.Strict,
snapGrid: [15, 15],
snapToGrid: false,
@@ -500,6 +505,10 @@ export const storeModel: StoreModel = {
setMultiSelectionActive: action((state, isActive) => {
state.multiSelectionActive = isActive;
}),
+
+ setConnectionMode: action((state, connectionMode) => {
+ state.connectionMode = connectionMode;
+ }),
};
const nodeEnv: string = (typeof __ENV__ !== 'undefined' && __ENV__) as string;
diff --git a/src/types/index.ts b/src/types/index.ts
index 50e1377a..50e68a15 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -264,6 +264,11 @@ export interface Connection {
targetHandle: ElementId | null;
}
+export enum ConnectionMode {
+ Strict = 'strict',
+ Loose = 'loose',
+}
+
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',