refactor(store): connection data

This commit is contained in:
moklick
2023-06-05 00:02:07 +02:00
parent 1dab08f36e
commit 45f51e96b1
29 changed files with 527 additions and 879 deletions
+1
View File
@@ -1,5 +1,6 @@
export * from './graph';
export * from './utils';
export * from './marker';
export * from './xyhandle';
export * from './xypanzoom';
export * from './xydrag';
+312
View File
@@ -0,0 +1,312 @@
import { getHostForElement, calcAutoPan, getEventPosition, pointToRendererPoint, rendererPointToPoint } from '..';
import {
OnConnect,
OnConnectStart,
HandleType,
Connection,
ConnectionMode,
PanBy,
BaseNode,
Transform,
ConnectingHandle,
OnConnectEnd,
UpdateConnection,
IsValidConnection,
ConnectionHandle,
} from '@reactflow/system';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType, resetRecentHandle } from './utils';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodes: BaseNode[];
transform: Transform;
lib: string;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
};
export type IsValidParams = {
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode;
fromNodeId: string;
fromHandleId: string | null;
fromType: HandleType;
isValidConnection?: IsValidConnection;
doc: Document | ShadowRoot;
lib: string;
};
export type XYHandleInstance = {
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
};
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection;
endHandle: ConnectingHandle | null;
};
const nullConnection: Connection = { source: null, target: null, sourceHandle: null, targetHandle: null };
const alwaysValid = () => true;
function onPointerDown(
event: MouseEvent | TouchEvent,
{
connectionMode,
connectionRadius,
handleId,
nodeId,
edgeUpdaterType,
isTarget,
domNode,
nodes,
transform,
lib,
autoPanOnConnect,
panBy,
cancelConnection,
onConnectStart,
onConnect,
onConnectEnd,
isValidConnection = alwaysValid,
onEdgeUpdateEnd,
updateConnection,
}: OnPointerDownParams
) {
// 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 closestHandle: 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 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
function autoPan(): void {
if (!autoPanOnConnect || !containerBounds) {
return;
}
const [x, y] = calcAutoPan(connectionPosition, containerBounds);
panBy({ x, y });
autoPanId = requestAnimationFrame(autoPan);
}
updateConnection({
connectionPosition,
connectionStatus: null,
// connectionNodeId etc will be removed in the next major in favor of connectionStartHandle
connectionStartHandle: {
nodeId,
handleId,
type: handleType,
},
connectionEndHandle: null,
});
onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) {
connectionPosition = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
connectionRadius,
handleLookup
);
if (!autoPanStarted) {
autoPan();
autoPanStarted = true;
}
const result = isValidHandle(event, {
handle: closestHandle,
connectionMode,
fromNodeId: nodeId,
fromHandleId: handleId,
fromType: isTarget ? 'target' : 'source',
isValidConnection,
doc,
lib,
});
handleDomNode = result.handleDomNode;
connection = result.connection;
isValid = result.isValid;
updateConnection({
connectionPosition:
closestHandle && isValid
? rendererPointToPoint(
{
x: closestHandle.x,
y: closestHandle.y,
},
transform
)
: connectionPosition,
connectionStatus: getConnectionStatus(!!closestHandle, isValid),
connectionEndHandle: result.endHandle,
});
if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle, lib);
}
if (connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle, lib);
prevActiveHandle = handleDomNode;
// @todo: remove the old class names "react-flow__handle-" in the next major version
handleDomNode.classList.add('connecting', `${lib}-flow__handle-connecting`);
handleDomNode.classList.toggle('valid', isValid);
handleDomNode.classList.toggle(`${lib}-flow__handle-valid`, isValid);
}
}
function onPointerUp(event: MouseEvent | TouchEvent) {
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
onConnectEnd?.(event);
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event);
}
resetRecentHandle(prevActiveHandle, lib);
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,
isValidConnection = alwaysValid,
}: IsValidParams
) {
const isTarget = fromType === 'target';
const handleDomNode = doc.querySelector(
`.${lib}-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
);
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: nullConnection,
endHandle: null,
};
console.log(1, result);
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');
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);
if (isValid) {
result.endHandle = {
nodeId: handleNodeId as string,
handleId,
type: handleType as HandleType,
};
result.isValid = isValidConnection(connection);
}
}
return result;
}
export const XYHandle: XYHandleInstance = {
onPointerDown,
isValid: isValidHandle,
};
+118
View File
@@ -0,0 +1,118 @@
import {
internalsSymbol,
ConnectionStatus,
type HandleType,
type NodeHandleBounds,
type XYPosition,
type BaseNode,
type ConnectionHandle,
} from '@reactflow/system';
// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
export function getHandles(
node: BaseNode,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: string
): ConnectionHandle[] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
res.push({
id: h.id || null,
type,
nodeId: node.id,
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
});
}
return res;
}, []);
}
export function getClosestHandle(
pos: XYPosition,
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity;
handles.forEach((handle) => {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius) {
if (distance < minDistance) {
closestHandles = [handle];
} else if (distance === minDistance) {
// when multiple handles are on the same distance we collect all of them
closestHandles.push(handle);
}
minDistance = distance;
}
});
if (!closestHandles.length) {
return null;
}
return closestHandles.length === 1
? closestHandles[0]
: // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one
closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
}
type GetHandleLookupParams = {
nodes: BaseNode[];
nodeId: string;
handleId: string | null;
handleType: string;
};
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
return nodes.reduce<ConnectionHandle[]>((res, node) => {
if (node[internalsSymbol]) {
const { handleBounds } = node[internalsSymbol];
let sourceHandles: ConnectionHandle[] = [];
let targetHandles: ConnectionHandle[] = [];
if (handleBounds) {
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
}
res.push(...sourceHandles, ...targetHandles);
}
return res;
}, []);
}
export function getHandleType(
edgeUpdaterType: HandleType | undefined,
handleDomNode: Element | null
): HandleType | null {
if (edgeUpdaterType) {
return edgeUpdaterType;
} else if (handleDomNode?.classList.contains('target')) {
return 'target';
} else if (handleDomNode?.classList.contains('source')) {
return 'source';
}
return null;
}
export function resetRecentHandle(handleDomNode: Element, lib: string): void {
handleDomNode?.classList.remove('valid', 'connecting', `${lib}-flow__handle-valid`, `${lib}-flow__handle-connecting`);
}
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
let connectionStatus = null;
if (isHandleValid) {
connectionStatus = 'valid';
} else if (isInsideConnectionRadius && !isHandleValid) {
connectionStatus = 'invalid';
}
return connectionStatus as ConnectionStatus;
}