feat(utils): vanilla interactive minimap
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "turbo run dev",
|
||||
"dev": "turbo run dev --parallel",
|
||||
"build": "turbo run build",
|
||||
"test": "turbo run test",
|
||||
"lint": "turbo run lint",
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
import { memo, useEffect, useRef, type MouseEvent } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { type D3ZoomEvent, zoom } from 'd3-zoom';
|
||||
import { select, pointer } from 'd3-selection';
|
||||
import {
|
||||
getRectOfNodes,
|
||||
getBoundsOfRects,
|
||||
getNodePositionWithOrigin,
|
||||
CoordinateExtent,
|
||||
XYMinimap,
|
||||
type Rect,
|
||||
type XYMinimapInstance,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
@@ -40,12 +39,17 @@ const selector = (s: ReactFlowState) => {
|
||||
boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes, s.nodeOrigin), viewBB) : viewBB,
|
||||
rfId: s.rfId,
|
||||
nodeOrigin: s.nodeOrigin,
|
||||
panZoom: s.panZoom,
|
||||
translateExtent: s.translateExtent,
|
||||
flowWidth: s.width,
|
||||
flowHeight: s.height,
|
||||
};
|
||||
};
|
||||
|
||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||
|
||||
const ARIA_LABEL_KEY = 'react-flow__minimap-desc';
|
||||
|
||||
function MiniMap({
|
||||
style,
|
||||
className,
|
||||
@@ -66,12 +70,15 @@ function MiniMap({
|
||||
pannable = false,
|
||||
zoomable = false,
|
||||
ariaLabel = 'React Flow mini map',
|
||||
inversePan = false,
|
||||
zoomStep = 10,
|
||||
inversePan,
|
||||
zoomStep,
|
||||
}: MiniMapProps) {
|
||||
const store = useStoreApi();
|
||||
const svg = useRef<SVGSVGElement>(null);
|
||||
const { boundingRect, viewBB, nodes, rfId, nodeOrigin } = useStore(selector, shallow);
|
||||
const { boundingRect, viewBB, nodes, rfId, nodeOrigin, panZoom, translateExtent, flowWidth, flowHeight } = useStore(
|
||||
selector,
|
||||
shallow
|
||||
);
|
||||
const elementWidth = (style?.width as number) ?? defaultWidth;
|
||||
const elementHeight = (style?.height as number) ?? defaultHeight;
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
@@ -90,76 +97,41 @@ function MiniMap({
|
||||
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
|
||||
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
|
||||
const viewScaleRef = useRef(0);
|
||||
const minimapInstance = useRef<XYMinimapInstance>();
|
||||
|
||||
viewScaleRef.current = viewScale;
|
||||
|
||||
useEffect(() => {
|
||||
if (svg.current) {
|
||||
const selection = select(svg.current as Element);
|
||||
|
||||
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
|
||||
const { transform, panZoom } = store.getState();
|
||||
|
||||
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, panZoom, translateExtent, width, height } = store.getState();
|
||||
|
||||
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 = viewScaleRef.current * 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, {});
|
||||
if (svg.current && panZoom) {
|
||||
minimapInstance.current = XYMinimap({
|
||||
domNode: svg.current,
|
||||
panZoom,
|
||||
getTransform: () => store.getState().transform,
|
||||
getViewScale: () => viewScaleRef.current,
|
||||
});
|
||||
|
||||
return () => {
|
||||
selection.on('zoom', null);
|
||||
minimapInstance.current?.destroy();
|
||||
};
|
||||
}
|
||||
}, [pannable, zoomable, inversePan, zoomStep]);
|
||||
}, [panZoom]);
|
||||
|
||||
useEffect(() => {
|
||||
minimapInstance.current?.update({
|
||||
translateExtent,
|
||||
width: flowWidth,
|
||||
height: flowHeight,
|
||||
inversePan,
|
||||
pannable,
|
||||
zoomStep,
|
||||
zoomable,
|
||||
});
|
||||
}, [pannable, zoomable, inversePan, zoomStep, translateExtent, flowWidth, flowHeight]);
|
||||
|
||||
const onSvgClick = onClick
|
||||
? (event: MouseEvent) => {
|
||||
const rfCoord = pointer(event);
|
||||
onClick(event, { x: rfCoord[0], y: rfCoord[1] });
|
||||
const rfCoord = minimapInstance.current?.pointer(event);
|
||||
onClick(event, { x: rfCoord?.[0] ?? 0, y: rfCoord?.[1] ?? 0 });
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -98,10 +98,9 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
autoPanOnConnect,
|
||||
domNode,
|
||||
edges,
|
||||
isValidConnection: isValidConnectionStore,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
transform,
|
||||
lib,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
@@ -113,7 +112,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const nodeId = isSourceHandle ? target : source;
|
||||
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
|
||||
const handleType = isSourceHandle ? 'target' : 'source';
|
||||
const isValidConnection = isValidConnectionStore || alwaysValidConnection;
|
||||
|
||||
const isTarget = isSourceHandle;
|
||||
const edge = edges.find((e) => e.id === id)!;
|
||||
@@ -139,7 +137,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
nodes,
|
||||
isTarget,
|
||||
edgeUpdaterType: handleType,
|
||||
transform,
|
||||
lib,
|
||||
cancelConnection,
|
||||
panBy,
|
||||
@@ -149,6 +146,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
onConnectEnd,
|
||||
onEdgeUpdateEnd: _onEdgeUpdateEnd,
|
||||
updateConnection,
|
||||
getTransform: () => store.getState().transform,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ A basic flow looks like this:
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
Panel,
|
||||
createNodes,
|
||||
createEdges,
|
||||
@@ -73,7 +73,7 @@ A basic flow looks like this:
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
```
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import { useStore } from '$lib/store';
|
||||
import { Panel } from '$lib/container/Panel';
|
||||
import MinimapNode from './MinimapNode.svelte';
|
||||
import interactive from './interactive'
|
||||
import type { GetMiniMapNodeAttribute, MiniMapProps } from './types';
|
||||
|
||||
type $$Props = MiniMapProps;
|
||||
@@ -30,13 +31,17 @@
|
||||
let maskStrokeWidth: $$Props['maskStrokeWidth'] = 1;
|
||||
let width: $$Props['width'] = undefined;
|
||||
let height: $$Props['height'] = undefined;
|
||||
let pannable: $$Props['pannable'] = true;
|
||||
let zoomable: $$Props['zoomable'] = true;
|
||||
let inversePan: $$Props['inversePan'] = undefined;
|
||||
let zoomStep: $$Props['zoomStep'] = undefined;
|
||||
|
||||
let className: $$Props['class'] = '';
|
||||
export { className as class };
|
||||
|
||||
const defaultWidth = 200;
|
||||
const defaultHeight = 150;
|
||||
const { nodes, transform, width: containerWidth, height: containerHeight, flowId } = useStore();
|
||||
|
||||
const { nodes, transform, width: containerWidth, height: containerHeight, flowId, panZoom, translateExtent } = useStore();
|
||||
|
||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||
@@ -65,6 +70,8 @@
|
||||
$: y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
|
||||
$: viewboxWidth = viewWidth + offset * 2;
|
||||
$: viewboxHeight = viewHeight + offset * 2;
|
||||
|
||||
const getViewScale = () => viewScale;
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
@@ -73,44 +80,58 @@
|
||||
style={`background-color: ${bgColor}; ${style}`}
|
||||
data-testid="svelte-flow__minimap"
|
||||
>
|
||||
<svg
|
||||
width={elementWidth}
|
||||
height={elementHeight}
|
||||
viewBox={`${x} ${y} ${viewboxWidth} ${viewboxHeight}`}
|
||||
role="img"
|
||||
aria-labelledby={labelledBy}
|
||||
>
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
{#if $panZoom}
|
||||
<svg
|
||||
width={elementWidth}
|
||||
height={elementHeight}
|
||||
viewBox={`${x} ${y} ${viewboxWidth} ${viewboxHeight}`}
|
||||
role="img"
|
||||
aria-labelledby={labelledBy}
|
||||
use:interactive={{
|
||||
panZoom: $panZoom,
|
||||
transform,
|
||||
getViewScale,
|
||||
translateExtent: $translateExtent,
|
||||
width: $containerWidth,
|
||||
height: $containerHeight,
|
||||
inversePan,
|
||||
zoomStep,
|
||||
pannable,
|
||||
zoomable,
|
||||
}}
|
||||
>
|
||||
{#if ariaLabel}<title id={labelledBy}>{ariaLabel}</title>{/if}
|
||||
|
||||
{#each $nodes as node (node.id)}
|
||||
{#if node.width && node.height}
|
||||
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
|
||||
<MinimapNode
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
{shapeRendering}
|
||||
class={nodeClassNameFunc(node)}
|
||||
style={{}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<path
|
||||
class="svelte-flow__minimap-mask"
|
||||
d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${
|
||||
-viewboxWidth - offset * 2
|
||||
}z
|
||||
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
|
||||
fill={maskColor}
|
||||
fill-rule="evenodd"
|
||||
stroke={maskStrokeColor}
|
||||
stroke-width={maskStrokeWidth}
|
||||
pointer-events="none"
|
||||
/>
|
||||
</svg>
|
||||
{#each $nodes as node (node.id)}
|
||||
{#if node.width && node.height}
|
||||
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
|
||||
<MinimapNode
|
||||
x={pos.x}
|
||||
y={pos.y}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
{shapeRendering}
|
||||
class={nodeClassNameFunc(node)}
|
||||
style={{}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
<path
|
||||
class="svelte-flow__minimap-mask"
|
||||
d={`M${x - offset},${y - offset}h${viewboxWidth + offset * 2}v${viewboxHeight + offset * 2}h${
|
||||
-viewboxWidth - offset * 2
|
||||
}z
|
||||
M${viewBB.x},${viewBB.y}h${viewBB.width}v${viewBB.height}h${-viewBB.width}z`}
|
||||
fill={maskColor}
|
||||
fill-rule="evenodd"
|
||||
stroke={maskStrokeColor}
|
||||
stroke-width={maskStrokeWidth}
|
||||
pointer-events="none"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as Minimap } from './Minimap.svelte';
|
||||
export { default as MiniMap } from './Minimap.svelte';
|
||||
|
||||
41
packages/svelte/src/lib/plugins/Minimap/interactive.ts
Normal file
41
packages/svelte/src/lib/plugins/Minimap/interactive.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
import {
|
||||
XYMinimap,
|
||||
type PanZoomInstance,
|
||||
type Transform,
|
||||
type XYMinimapUpdate
|
||||
} from '@xyflow/system';
|
||||
|
||||
export type UseInteractiveParams = {
|
||||
panZoom: PanZoomInstance;
|
||||
transform: Writable<Transform>;
|
||||
getViewScale: () => number;
|
||||
} & XYMinimapUpdate;
|
||||
|
||||
export default function interactive(domNode: Element, params: UseInteractiveParams) {
|
||||
const minimap = XYMinimap({
|
||||
domNode,
|
||||
panZoom: params.panZoom,
|
||||
getTransform: () => get(params.transform),
|
||||
getViewScale: params.getViewScale
|
||||
});
|
||||
|
||||
function update(params: UseInteractiveParams) {
|
||||
minimap.update({
|
||||
translateExtent: params.translateExtent,
|
||||
width: params.width,
|
||||
height: params.height,
|
||||
inversePan: params.inversePan,
|
||||
zoomStep: params.zoomStep,
|
||||
pannable: params.pannable,
|
||||
zoomable: params.zoomable
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy() {
|
||||
minimap.destroy();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,8 @@ export type MiniMapProps = {
|
||||
height?: number;
|
||||
// onClick?: (event: MouseEvent, position: XYPosition) => void;
|
||||
// onNodeClick?: (event: MouseEvent, node: Node) => void;
|
||||
// pannable?: boolean;
|
||||
// zoomable?: boolean;
|
||||
pannable?: boolean;
|
||||
zoomable?: boolean;
|
||||
inversePan?: boolean;
|
||||
zoomStep?: number;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
createNodes,
|
||||
createEdges,
|
||||
type NodeTypes,
|
||||
@@ -108,7 +108,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
useSvelteFlow,
|
||||
type Node
|
||||
} from '../../lib/index';
|
||||
@@ -52,7 +52,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
<Sidebar />
|
||||
</main>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
createNodes,
|
||||
createEdges,
|
||||
type NodeTypes,
|
||||
@@ -154,7 +154,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
createNodes,
|
||||
createEdges,
|
||||
PanOnScrollMode,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
type OnMoveEnd,
|
||||
type Node,
|
||||
type Edge,
|
||||
@@ -70,7 +70,7 @@
|
||||
panOnDrag={panOnDrag}
|
||||
onMoveEnd={onMoveEnd}
|
||||
>
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
<Panel position="top-right">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
Panel,
|
||||
createNodes,
|
||||
createEdges,
|
||||
@@ -138,7 +138,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
<Panel position="top-right">
|
||||
<button on:click={updateNode}>update node pos</button>
|
||||
</Panel>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
createNodes,
|
||||
createEdges,
|
||||
type Node,
|
||||
@@ -59,6 +59,6 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
@@ -4,7 +4,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
createNodes,
|
||||
createEdges,
|
||||
type NodeTypes,
|
||||
@@ -130,7 +130,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Controls,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Minimap,
|
||||
MiniMap,
|
||||
} from '../../lib/index';
|
||||
import Sidebar from './Sidebar.svelte';
|
||||
</script>
|
||||
@@ -14,7 +14,7 @@
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Dots} />
|
||||
<Minimap />
|
||||
<MiniMap />
|
||||
</SvelteFlow>
|
||||
<Sidebar />
|
||||
</main>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
33
pnpm-lock.yaml
generated
33
pnpm-lock.yaml
generated
@@ -61,7 +61,6 @@ importers:
|
||||
'@vitejs/plugin-react': 4.0.0
|
||||
'@vitejs/plugin-react-swc': ^3.3.2
|
||||
'@xyflow/react': workspace:*
|
||||
'@xyflow/test': workspace:^0.0.1
|
||||
classcat: ^5.0.3
|
||||
cypress: ^10.6.0
|
||||
cypress-real-events: ^1.7.1
|
||||
@@ -76,7 +75,6 @@ importers:
|
||||
zustand: ^4.3.1
|
||||
dependencies:
|
||||
'@xyflow/react': link:../../packages/react
|
||||
'@xyflow/test': link:../../packages/test
|
||||
classcat: registry.npmjs.org/classcat/5.0.4
|
||||
dagre: registry.npmjs.org/dagre/0.8.5
|
||||
localforage: registry.npmjs.org/localforage/1.10.0
|
||||
@@ -224,35 +222,6 @@ importers:
|
||||
'@xyflow/tsconfig': link:../../tooling/tsconfig
|
||||
typescript: registry.npmjs.org/typescript/4.9.5
|
||||
|
||||
packages/test:
|
||||
specifiers:
|
||||
'@types/d3': ^7.4.0
|
||||
'@types/d3-drag': ^3.0.1
|
||||
'@types/d3-selection': ^3.0.3
|
||||
'@types/d3-zoom': ^3.0.1
|
||||
'@types/node': ^18.7.16
|
||||
'@xyflow/eslint-config': workspace:*
|
||||
'@xyflow/rollup-config': workspace:*
|
||||
'@xyflow/tsconfig': workspace:*
|
||||
d3-drag: ^3.0.0
|
||||
d3-selection: ^3.0.0
|
||||
d3-zoom: ^3.0.0
|
||||
typescript: ^4.9.4
|
||||
dependencies:
|
||||
'@types/d3': registry.npmjs.org/@types/d3/7.4.0
|
||||
'@types/d3-drag': registry.npmjs.org/@types/d3-drag/3.0.2
|
||||
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.5
|
||||
'@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.3
|
||||
d3-drag: registry.npmjs.org/d3-drag/3.0.0
|
||||
d3-selection: registry.npmjs.org/d3-selection/3.0.0
|
||||
d3-zoom: registry.npmjs.org/d3-zoom/3.0.0
|
||||
devDependencies:
|
||||
'@types/node': registry.npmjs.org/@types/node/18.16.16
|
||||
'@xyflow/eslint-config': link:../../tooling/eslint-config
|
||||
'@xyflow/rollup-config': link:../../tooling/rollup-config
|
||||
'@xyflow/tsconfig': link:../../tooling/tsconfig
|
||||
typescript: registry.npmjs.org/typescript/4.9.5
|
||||
|
||||
tooling/eslint-config:
|
||||
specifiers:
|
||||
eslint: ^8.22.0
|
||||
@@ -2083,7 +2052,7 @@ packages:
|
||||
version: 2.10.0
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@types/node': registry.npmjs.org/@types/node/18.16.16
|
||||
'@types/node': registry.npmjs.org/@types/node/14.18.48
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user