Files
xyflow/packages/svelte/src/lib/components/Handle/handler.ts
T

210 lines
5.5 KiB
TypeScript

import { get, type Writable } from 'svelte/store';
import {
getHostForElement,
calcAutoPan,
getEventPosition,
pointToRendererPoint,
rendererPointToPoint
} from '@reactflow/utils';
import type {
OnConnect,
HandleType,
Connection,
ConnectionMode,
XYPosition,
Transform
} from '@reactflow/system';
import {
getClosestHandle,
getConnectionStatus,
getHandleLookup,
getHandleType,
isValidHandle,
resetRecentHandle,
type ConnectionHandle,
type ValidConnectionFunc
} from './utils';
import type { ConnectionData, Node } from '$lib/types';
export function handlePointerDown({
event,
handleId,
nodeId,
onConnect,
domNode,
nodes,
connectionMode,
connectionRadius,
isTarget,
transform: transformStore,
panBy,
updateConnection,
cancelConnection,
isValidConnection,
edgeUpdaterType,
onEdgeUpdateEnd
}: {
event: MouseEvent | TouchEvent;
handleId: string | null;
nodeId: string;
onConnect: OnConnect;
isTarget: boolean;
connectionMode: ConnectionMode;
domNode: HTMLDivElement | null;
nodes: Node[];
connectionRadius: number;
isValidConnection: ValidConnectionFunc;
transform: Writable<Transform>;
updateConnection: (connection: Partial<ConnectionData>) => void;
cancelConnection: () => void;
panBy: (delta: XYPosition) => void;
edgeUpdaterType?: HandleType;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
}): void {
// when react-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0;
let prevClosestHandle: ConnectionHandle | null;
const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y);
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
const containerBounds = domNode?.getBoundingClientRect();
if (!containerBounds || !handleType) {
return;
}
let prevActiveHandle: Element;
let connectionPosition = getEventPosition(event, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid = false;
let handleDomNode: Element | null = null;
const autoPanOnConnect = true;
const handleLookup = getHandleLookup({
nodes,
nodeId,
handleId,
handleType
});
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
const autoPan = (): void => {
// @todd add prop
if (!autoPanOnConnect) {
return;
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x: xMovement, y: yMovement });
autoPanId = requestAnimationFrame(autoPan);
};
updateConnection({
position: connectionPosition,
nodeId,
handleId,
handleType,
status: null
});
// @todo add prop
// onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) {
const transform = get(transformStore);
connectionPosition = getEventPosition(event, containerBounds);
prevClosestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(
event,
prevClosestHandle,
connectionMode,
nodeId,
handleId,
isTarget ? 'target' : 'source',
isValidConnection,
doc
);
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = result.isValid;
updateConnection({
position:
prevClosestHandle && isValid
? rendererPointToPoint(
{
x: prevClosestHandle.x,
y: prevClosestHandle.y
},
transform
)
: connectionPosition,
status: getConnectionStatus(!!prevClosestHandle, isValid)
});
if (!prevClosestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle);
}
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version
handleDomNode.classList.add('connecting');
handleDomNode.classList.toggle('valid', isValid);
}
}
function onPointerUp(event: MouseEvent | TouchEvent) {
if ((prevClosestHandle || 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
// @todo add onConnectEnd prop
// getState().onConnectEnd?.(event);
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(prevActiveHandle);
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);
}