feat(props): add connection mode closes #702

This commit is contained in:
moklick
2020-11-30 13:09:56 +01:00
parent 773073b087
commit c429ae7ca4
9 changed files with 268 additions and 257 deletions
+1
View File
@@ -33,6 +33,7 @@ const ProviderFlow = () => {
onConnect={onConnect}
onElementsRemove={onElementsRemove}
onLoad={onLoad}
connectionMode="loose"
>
<Controls />
</ReactFlow>
+6 -3
View File
@@ -1,9 +1,9 @@
import React, { memo, ComponentType, useCallback, useState } from 'react';
import cc from 'classcat';
import { useStoreActions } from '../../store/hooks';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
import { onMouseDown } from '../../components/Handle/BaseHandle';
import { onMouseDown } from '../../components/Handle/handler';
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
@@ -41,6 +41,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const connectionMode = useStoreState((state) => state.connectionMode);
const [updating, setUpdating] = useState<boolean>(false);
const inactive = !elementsSelectable && !onClick;
@@ -85,7 +87,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
setPosition,
onConnectEdge,
isTarget,
isValidConnection
isValidConnection,
connectionMode
);
},
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition]
-229
View File
@@ -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 (
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
className={handleClasses}
onMouseDown={(event) =>
onMouseDown(
event,
handleId,
nodeId,
setConnectionNodeId,
setPosition,
onConnect,
isTarget,
isValidConnection,
onConnectStart,
onConnectStop,
onConnectEnd
)
}
{...rest}
/>
);
};
BaseHandle.displayName = 'BaseHandle';
export default memo(BaseHandle);
+171
View File
@@ -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);
}
+64 -25
View File
@@ -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 (
<BaseHandle
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
className={handleClasses}
id={id}
nodeId={nodeId}
setPosition={setPosition}
setConnectionNodeId={setConnectionNodeId}
onConnect={onConnectExtended}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
type={type}
position={position}
isValidConnection={isValidConnection}
onMouseDown={onMouseDownHandler}
style={style}
/>
);
+8
View File
@@ -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 (
<FlowRenderer
onPaneClick={onPaneClick}
+4
View File
@@ -19,6 +19,7 @@ import {
Node,
Edge,
Connection,
ConnectionMode,
ConnectionLineType,
ConnectionLineComponent,
FlowTransform,
@@ -74,6 +75,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, '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}
+9
View File
@@ -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<StoreModel>;
setMultiSelectionActive: Action<StoreModel, boolean>;
setConnectionMode: Action<StoreModel, ConnectionMode>;
}
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;
+5
View File
@@ -264,6 +264,11 @@ export interface Connection {
targetHandle: ElementId | null;
}
export enum ConnectionMode {
Strict = 'strict',
Loose = 'loose',
}
export enum ConnectionLineType {
Bezier = 'default',
Straight = 'straight',