330 lines
9.4 KiB
TypeScript
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,
|
|
};
|