Files
xyflow/packages/system/src/xyhandle/XYHandle.ts
T

330 lines
9.4 KiB
TypeScript

import {
pointToRendererPoint,
getHostForElement,
calcAutoPan,
getEventPosition,
getHandlePosition,
rendererPointToPoint,
} from '../utils';
import {
ConnectionMode,
Position,
oppositePosition,
ConnectionInProgress,
type Handle,
type Connection,
} from '../types';
import { getClosestHandle, isConnectionValid, getHandleType, getHandle } from './utils';
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
const alwaysValid = () => true;
function onPointerDown(
event: MouseEvent | TouchEvent,
{
connectionMode,
connectionRadius,
handleId,
nodeId,
edgeUpdaterType,
isTarget,
domNode,
nodeLookup,
lib,
autoPanOnConnect,
flowId,
panBy,
cancelConnection,
onConnectStart,
onConnect,
onConnectEnd,
isValidConnection = alwaysValid,
onReconnectEnd,
updateConnection,
getTransform,
getFromHandle,
autoPanSpeed,
dragThreshold = 1,
}: OnPointerDownParams
) {
// when xyflow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target);
let autoPanId = 0;
let closestHandle: Handle | null;
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
let connectionStarted = false;
if (!containerBounds || !handleType) {
return;
}
const fromHandleInternal = getHandle(nodeId, handleType, handleId, nodeLookup, connectionMode);
if (!fromHandleInternal) {
return;
}
let position = getEventPosition(event, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid: boolean | null = false;
let handleDomNode: Element | null = null;
// 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(position, containerBounds, autoPanSpeed);
panBy({ x, y });
autoPanId = requestAnimationFrame(autoPan);
}
// Stays the same for all consecutive pointermove events
const fromHandle: Handle = {
...fromHandleInternal,
nodeId,
type: handleType,
position: fromHandleInternal.position,
};
const fromNodeInternal = nodeLookup.get(nodeId)!;
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
let previousConnection: ConnectionInProgress = {
inProgress: true,
isValid: null,
from,
fromHandle,
fromPosition: fromHandle.position,
fromNode: fromNodeInternal,
to: position,
toHandle: null,
toPosition: oppositePosition[fromHandle.position],
toNode: null,
};
function startConnection() {
connectionStarted = true;
updateConnection(previousConnection);
onConnectStart?.(event, { nodeId, handleId, handleType });
}
if (dragThreshold === 0) {
startConnection();
}
function onPointerMove(event: MouseEvent | TouchEvent) {
if (!connectionStarted) {
const { x: evtX, y: evtY } = getEventPosition(event);
const dx = evtX - x;
const dy = evtY - y;
const nextConnectionStarted = dx * dx + dy * dy > dragThreshold * dragThreshold;
if (!nextConnectionStarted) {
return;
}
startConnection();
}
if (!getFromHandle() || !fromHandle) {
onPointerUp(event);
return;
}
const transform = getTransform();
position = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle(
pointToRendererPoint(position, transform, false, [1, 1]),
connectionRadius,
nodeLookup,
fromHandle
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(event, {
handle: closestHandle,
connectionMode,
fromNodeId: nodeId,
fromHandleId: handleId,
fromType: isTarget ? 'target' : 'source',
isValidConnection,
doc,
lib,
flowId,
nodeLookup,
});
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = isConnectionValid(!!closestHandle, result.isValid);
const newConnection: ConnectionInProgress = {
// from stays the same
...previousConnection,
isValid,
to:
result.toHandle && isValid
? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, transform)
: position,
toHandle: result.toHandle,
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)! : null,
};
/*
* we don't want to trigger an update when the connection
* is snapped to the same handle as before
*/
if (
isValid &&
closestHandle &&
previousConnection.toHandle &&
newConnection.toHandle &&
previousConnection.toHandle.type === newConnection.toHandle.type &&
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
previousConnection.toHandle.id === newConnection.toHandle.id &&
previousConnection.to.x === newConnection.to.x &&
previousConnection.to.y === newConnection.to.y
) {
return;
}
updateConnection(newConnection);
previousConnection = newConnection;
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if (connectionStarted) {
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
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
onConnectEnd?.(event, finalConnectionState);
if (edgeUpdaterType) {
onReconnectEnd?.(event, finalConnectionState);
}
}
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,
flowId,
isValidConnection = alwaysValid,
nodeLookup,
}: IsValidParams
) {
const isTarget = fromType === 'target';
const handleDomNode = handle
? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
: null;
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: null,
toHandle: 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');
if (!handleNodeId || !handleType) {
return result;
}
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);
result.isValid = isValid && isValidConnection(connection);
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, true);
}
return result;
}
export const XYHandle: XYHandleInstance = {
onPointerDown,
isValid: isValidHandle,
};