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