Files
xyflow/src/components/Handle/BaseHandle.tsx
T

230 lines
6.1 KiB
TypeScript

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