feat(utils): vanilla interactive minimap
This commit is contained in:
@@ -3,4 +3,5 @@ export * from './types';
|
||||
export * from './utils';
|
||||
export * from './xydrag';
|
||||
export * from './xyhandle';
|
||||
export * from './xyminimap';
|
||||
export * from './xypanzoom';
|
||||
|
||||
311
packages/system/src/xyhandle/XYHandle.ts
Normal file
311
packages/system/src/xyhandle/XYHandle.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils';
|
||||
import {
|
||||
ConnectionMode,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type BaseNode,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
} from '../types';
|
||||
|
||||
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[];
|
||||
lib: string;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
};
|
||||
|
||||
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,
|
||||
lib,
|
||||
autoPanOnConnect,
|
||||
panBy,
|
||||
cancelConnection,
|
||||
onConnectStart,
|
||||
onConnect,
|
||||
onConnectEnd,
|
||||
isValidConnection = alwaysValid,
|
||||
onEdgeUpdateEnd,
|
||||
updateConnection,
|
||||
getTransform,
|
||||
}: 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) {
|
||||
const transform = getTransform();
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -1,311 +1 @@
|
||||
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils';
|
||||
import {
|
||||
ConnectionMode,
|
||||
type OnConnect,
|
||||
type OnConnectStart,
|
||||
type HandleType,
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type BaseNode,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
} from '../types';
|
||||
|
||||
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[];
|
||||
lib: string;
|
||||
edgeUpdaterType?: HandleType;
|
||||
updateConnection: UpdateConnection;
|
||||
panBy: PanBy;
|
||||
cancelConnection: () => void;
|
||||
onConnectStart?: OnConnectStart;
|
||||
onConnect?: OnConnect;
|
||||
onConnectEnd?: OnConnectEnd;
|
||||
isValidConnection?: IsValidConnection;
|
||||
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||
getTransform: () => Transform;
|
||||
};
|
||||
|
||||
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,
|
||||
lib,
|
||||
autoPanOnConnect,
|
||||
panBy,
|
||||
cancelConnection,
|
||||
onConnectStart,
|
||||
onConnect,
|
||||
onConnectEnd,
|
||||
isValidConnection = alwaysValid,
|
||||
onEdgeUpdateEnd,
|
||||
updateConnection,
|
||||
getTransform,
|
||||
}: 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) {
|
||||
const transform = getTransform();
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
export * from './XYHandle';
|
||||
|
||||
104
packages/system/src/xyminimap/index.ts
Normal file
104
packages/system/src/xyminimap/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { type D3ZoomEvent, zoom } from 'd3-zoom';
|
||||
import { select, pointer } from 'd3-selection';
|
||||
|
||||
import type { CoordinateExtent, PanZoomInstance, Transform } from '../types';
|
||||
|
||||
export type XYMinimapInstance = {
|
||||
update: (params: XYMinimapUpdate) => void;
|
||||
destroy: () => void;
|
||||
pointer: typeof pointer;
|
||||
};
|
||||
|
||||
export type XYMinimapParams = {
|
||||
panZoom: PanZoomInstance;
|
||||
domNode: Element;
|
||||
getTransform: () => Transform;
|
||||
getViewScale: () => number;
|
||||
};
|
||||
|
||||
export type XYMinimapUpdate = {
|
||||
translateExtent: CoordinateExtent;
|
||||
width: number;
|
||||
height: number;
|
||||
inversePan?: boolean;
|
||||
zoomStep?: number;
|
||||
pannable?: boolean;
|
||||
zoomable?: boolean;
|
||||
};
|
||||
|
||||
export function XYMinimap({ domNode, panZoom, getTransform, getViewScale }: XYMinimapParams) {
|
||||
const selection = select(domNode);
|
||||
|
||||
function update({
|
||||
translateExtent,
|
||||
width,
|
||||
height,
|
||||
zoomStep = 10,
|
||||
pannable = true,
|
||||
zoomable = true,
|
||||
inversePan = false,
|
||||
}: XYMinimapUpdate) {
|
||||
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
|
||||
const transform = getTransform();
|
||||
|
||||
if (event.sourceEvent.type !== 'wheel' || !panZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pinchDelta =
|
||||
-event.sourceEvent.deltaY *
|
||||
(event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
|
||||
zoomStep;
|
||||
const nextZoom = transform[2] * Math.pow(2, pinchDelta);
|
||||
|
||||
panZoom.scaleTo(nextZoom);
|
||||
};
|
||||
|
||||
const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
|
||||
const transform = getTransform();
|
||||
|
||||
if (event.sourceEvent.type !== 'mousemove' || !panZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround.
|
||||
const moveScale = getViewScale() * Math.max(1, transform[2]) * (inversePan ? -1 : 1);
|
||||
const position = {
|
||||
x: transform[0] - event.sourceEvent.movementX * moveScale,
|
||||
y: transform[1] - event.sourceEvent.movementY * moveScale,
|
||||
};
|
||||
const extent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[width, height],
|
||||
];
|
||||
|
||||
panZoom.setViewportConstrained(
|
||||
{
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
zoom: transform[2],
|
||||
},
|
||||
extent,
|
||||
translateExtent
|
||||
);
|
||||
};
|
||||
|
||||
const zoomAndPanHandler = zoom()
|
||||
// @ts-ignore
|
||||
.on('zoom', pannable ? panHandler : null)
|
||||
// @ts-ignore
|
||||
.on('zoom.wheel', zoomable ? zoomHandler : null);
|
||||
|
||||
selection.call(zoomAndPanHandler, {});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
selection.on('zoom', null);
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy,
|
||||
pointer,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user