Merge branch 'main' into nodesbounds

This commit is contained in:
Moritz Klack
2024-08-29 00:09:57 +02:00
committed by GitHub
44 changed files with 1388 additions and 450 deletions

View File

@@ -53,6 +53,7 @@ import UseHandleConnections from '../examples/UseHandleConnections';
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
import DevTools from '../examples/DevTools';
import Redux from '../examples/Redux';
import MovingHandles from '../examples/MovingHandles';
export interface IRoute {
name: string;
@@ -206,6 +207,11 @@ const routes: IRoute[] = [
path: 'multi-setnodes',
component: MultiSetNodes,
},
{
name: 'Moving Handles',
path: 'moving-handles',
component: MovingHandles,
},
{
name: 'Multi Flows',
path: 'multiflows',

View File

@@ -1,13 +1,8 @@
import { Handle, NodeProps, Position, ReactFlowState, useStore } from '@xyflow/react';
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionStartHandle?.nodeId;
import { Handle, NodeProps, Position, useConnection } from '@xyflow/react';
export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector);
const isConnecting = !!connectionNodeId;
const isTarget = connectionNodeId && connectionNodeId !== id;
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
const connection = useConnection();
const isTarget = connection.inProgress && connection.fromNode.id !== id;
const label = isTarget ? 'Drop here' : 'Drag to connect';
return (
@@ -19,17 +14,13 @@ export default function CustomNode({ id }: NodeProps) {
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}}
>
{!isConnecting && (
<Handle className="customHandle" style={{ zIndex: 2 }} position={Position.Right} type="source" />
{/* If handles are conditionally rendered and not present initially, you need to update the node internals https://reactflow.dev/docs/api/hooks/use-update-node-internals/ */}
{/* In this case we don't need to use useUpdateNodeInternals, since !isConnecting is true at the beginning and all handles are rendered initially. */}
{!connection.inProgress && <Handle className="customHandle" position={Position.Right} type="source" />}
{/* We want to disable the target handle, if the connection was started from this node */}
{(!connection.inProgress || isTarget) && (
<Handle className="customHandle" position={Position.Left} type="target" isConnectableStart={false} />
)}
<Handle
className="customHandle"
style={targetHandleStyle}
position={Position.Left}
type="target"
isConnectableStart={false}
/>
{label}
</div>
</div>

View File

@@ -1,5 +1,4 @@
import { useCallback } from 'react';
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
import { EdgeProps, getStraightPath, useInternalNode } from '@xyflow/react';
import { getEdgeParams } from './utils.js';

View File

@@ -1,5 +1,15 @@
import { useCallback } from 'react';
import { ReactFlow, Node, Edge, addEdge, useNodesState, useEdgesState, MarkerType, OnConnect } from '@xyflow/react';
import {
ReactFlow,
Node,
Edge,
addEdge,
useNodesState,
useEdgesState,
MarkerType,
OnConnect,
ConnectionMode,
} from '@xyflow/react';
import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';

View File

@@ -19,12 +19,11 @@ function getNodeIntersection(intersectionNode: InternalNode, targetNode: Interna
const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h);
const a = 1 / (Math.abs(xx1) + Math.abs(yy1));
const a = 1 / (Math.abs(xx1) + Math.abs(yy1) || 1);
const xx3 = a * xx1;
const yy3 = a * yy1;
const x = w * (xx3 + yy3) + x2;
const y = h * (-xx3 + yy3) + y2;
return { x, y };
}

View File

@@ -0,0 +1,57 @@
import React, { memo, CSSProperties } from 'react';
import { Handle, Position, NodeProps, useConnection } from '@xyflow/react';
import type { MovingHandleNode } from '.';
const sourceHandleStyle: CSSProperties = {
position: 'relative',
transform: 'translate(-50%, 0)',
top: 0,
transition: 'transform 0.5s',
};
function MovingHandleNode({}: NodeProps<MovingHandleNode>) {
const connection = useConnection();
return (
<>
<div
style={{
display: 'flex',
flexDirection: 'column',
position: 'absolute',
left: 0,
top: 0,
justifyContent: 'space-around',
height: '100%',
}}
>
<Handle
type="target"
id="a"
position={Position.Left}
style={{
...sourceHandleStyle,
transform: connection.inProgress ? 'translate(-20px, 0)' : 'translate(-50%, 0)',
}}
/>
<Handle
type="target"
id="b"
position={Position.Left}
style={{
...sourceHandleStyle,
transform: connection.inProgress ? 'translate(-20px, 0)' : 'translate(-50%, 0)',
}}
/>
</div>
<div style={{ background: '#f4f4f4', padding: 10 }}>
<div>moving handles</div>
<Handle type="source" position={Position.Right} />
<Handle type="source" position={Position.Right} />
</div>
</>
);
}
export default memo(MovingHandleNode);

View File

@@ -0,0 +1,116 @@
import { useState, useCallback, useEffect } from 'react';
import {
ReactFlow,
Controls,
addEdge,
Node,
Position,
useEdgesState,
Background,
applyNodeChanges,
OnNodesChange,
OnConnect,
BuiltInNode,
BuiltInEdge,
NodeTypes,
ReactFlowProvider,
useConnection,
useReactFlow,
useUpdateNodeInternals,
} from '@xyflow/react';
import MovingHandleNode from './MovingHandleNode';
export type MovingHandleNode = Node<{}, 'movingHandle'>;
export type MyNode = BuiltInNode | MovingHandleNode;
export type MyEdge = BuiltInEdge;
const nodeTypes: NodeTypes = {
movingHandle: MovingHandleNode,
};
const initNodes: MyNode[] = [
{
id: 'input',
type: 'input',
data: { label: 'input' },
position: { x: -300, y: 0 },
sourcePosition: Position.Right,
},
];
for (let i = 0; i < 10; i++) {
initNodes.push({
id: `${i}`,
type: 'movingHandle',
position: { x: 0, y: i * 60 },
data: {},
});
}
const initEdges: MyEdge[] = [];
const CustomNodeFlow = () => {
const [nodes, setNodes] = useState<MyNode[]>(initNodes);
const onNodesChange: OnNodesChange<MyNode> = useCallback(
(changes) =>
setNodes((nds) => {
const nextNodes = applyNodeChanges(changes, nds);
return nextNodes;
}),
[setNodes]
);
const [edges, setEdges, onEdgesChange] = useEdgesState<MyEdge>(initEdges);
const onConnect: OnConnect = useCallback(
(connection) => setEdges((eds) => addEdge({ ...connection, animated: true }, eds)),
[setEdges]
);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
minZoom={0.2}
fitView
>
<Controls />
<Background />
<NodeUpdater />
</ReactFlow>
);
};
function NodeUpdater() {
const connection = useConnection();
const { getNodes } = useReactFlow();
const updateNodeInternals = useUpdateNodeInternals();
useEffect(() => {
const startTime = Date.now();
const nodeIds = getNodes().map((n) => n.id);
function update() {
if (Date.now() - startTime < 500) {
updateNodeInternals(nodeIds);
requestAnimationFrame(update);
}
}
update();
}, [connection.inProgress]);
return null;
}
export default () => (
<ReactFlowProvider>
<CustomNodeFlow />
</ReactFlowProvider>
);

View File

@@ -13,12 +13,14 @@
const { getIntersectingNodes } = useSvelteFlow();
function onNodeDrag({ detail: { targetNode } }) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
if (targetNode) {
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
$nodes.forEach((n) => {
n.class = intersections.includes(n.id) ? 'highlight' : '';
});
$nodes = $nodes;
$nodes.forEach((n) => {
n.class = intersections.includes(n.id) ? 'highlight' : '';
});
$nodes = $nodes;
}
}
</script>

View File

@@ -10,7 +10,8 @@ export const initialNodes: Node[] = [
{
id: '2',
data: { label: 'Node 2' },
position: { x: 0, y: 150 }
position: { x: 0, y: 150 },
parentId: '1'
},
{
id: '3',

View File

@@ -1,5 +1,62 @@
# @xyflow/react
## 12.2.0
### Minor Changes
- [#4574](https://github.com/xyflow/xyflow/pull/4574) [`b65aed19`](https://github.com/xyflow/xyflow/commit/b65aed19840c515949bef236a23d5f0a754cdeb4) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Add `getHandleConnections` helper to `useReactFlow` & `useSvelteFlow`
### Patch Changes
- [#4594](https://github.com/xyflow/xyflow/pull/4594) [`5138d90b`](https://github.com/xyflow/xyflow/commit/5138d90bdb91ff5d8dbeb8c8d29bdfd31c5b59d6) Thanks [@peterkogo](https://github.com/peterkogo)! - Fixed reconnecting edges with loose connectionMode
- [#4603](https://github.com/xyflow/xyflow/pull/4603) [`12dbe125`](https://github.com/xyflow/xyflow/commit/12dbe125755fad7d2f6dff19100872dd823d1012) Thanks [@moklick](https://github.com/moklick)! - use correct index when using setNodes for inserting
- Updated dependencies [[`5138d90b`](https://github.com/xyflow/xyflow/commit/5138d90bdb91ff5d8dbeb8c8d29bdfd31c5b59d6), [`12dbe125`](https://github.com/xyflow/xyflow/commit/12dbe125755fad7d2f6dff19100872dd823d1012)]:
- @xyflow/system@0.0.40
## 12.1.1
### Patch Changes
- [#4568](https://github.com/xyflow/xyflow/pull/4568) [`c3e62782`](https://github.com/xyflow/xyflow/commit/c3e6278222dc13333f75ecdbe634201ddabab87a) Thanks [@peterkogo](https://github.com/peterkogo)! - Only display grab cursor when panOnDrag is on left mouse button
- [#4567](https://github.com/xyflow/xyflow/pull/4567) [`cc75c29f`](https://github.com/xyflow/xyflow/commit/cc75c29f3c7450321c64107f5b2a0f2084a0431c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix internal type of edges in store
- Updated dependencies [[`c3e62782`](https://github.com/xyflow/xyflow/commit/c3e6278222dc13333f75ecdbe634201ddabab87a)]:
- @xyflow/system@0.0.39
## 12.1.0
### Minor Changes
- [#4555](https://github.com/xyflow/xyflow/pull/4555) [`24e87e39`](https://github.com/xyflow/xyflow/commit/24e87e398419646f671af1085fbfec3e197bc56b) Thanks [@peterkogo](https://github.com/peterkogo)! - Added final connection state as a function parameter to onReconnectEnd
- [#4554](https://github.com/xyflow/xyflow/pull/4554) [`cca11ea1`](https://github.com/xyflow/xyflow/commit/cca11ea1c549f3fae1e52f5c121f607ded9764f3) Thanks [@peterkogo](https://github.com/peterkogo)! - Added optional selector for useConnection hook
- [#4549](https://github.com/xyflow/xyflow/pull/4549) [`99733c01`](https://github.com/xyflow/xyflow/commit/99733c01bc70f9463e7dba0046c5f8d839a1d2ba) Thanks [@moklick](https://github.com/moklick)! - feat(onConnectEnd): pass connectionState param
### Patch Changes
- [#4550](https://github.com/xyflow/xyflow/pull/4550) [`41981970`](https://github.com/xyflow/xyflow/commit/41981970e40baae29ec1631ea5f1eec9c27dfb12) Thanks [@moklick](https://github.com/moklick)! - fix(fitView): only trigger for resize observer
- [#4501](https://github.com/xyflow/xyflow/pull/4501) [`ec64b572`](https://github.com/xyflow/xyflow/commit/ec64b57240f0c61912d4910b095210f57d8df8ce) Thanks [@jeffgord](https://github.com/jeffgord)! - fix(background): use offset prop correctly for dots variant
- [#4548](https://github.com/xyflow/xyflow/pull/4548) [`692e6440`](https://github.com/xyflow/xyflow/commit/692e6440b10e75cb31f3f3172aede9ed4d7f05d2) Thanks [@peterkogo](https://github.com/peterkogo)! - Replaced algorithm used for searching close handles while connecting
- [#4547](https://github.com/xyflow/xyflow/pull/4547) [`fdff601d`](https://github.com/xyflow/xyflow/commit/fdff601de418f2ac6a78f04e5a586d67b8d436e6) Thanks [@moklick](https://github.com/moklick)! - chore(react): re-export Handle type
- Updated dependencies [[`b63a3734`](https://github.com/xyflow/xyflow/commit/b63a3734b84b6817603c8e6e48e2836f048acc3b), [`24e87e39`](https://github.com/xyflow/xyflow/commit/24e87e398419646f671af1085fbfec3e197bc56b), [`692e6440`](https://github.com/xyflow/xyflow/commit/692e6440b10e75cb31f3f3172aede9ed4d7f05d2), [`559d4926`](https://github.com/xyflow/xyflow/commit/559d49264b940f93c5e205bf984aa76230b10806), [`4ecfd7e1`](https://github.com/xyflow/xyflow/commit/4ecfd7e19720b70024d0b5dff27d4537dd46b49a), [`e7ef328f`](https://github.com/xyflow/xyflow/commit/e7ef328f8f9286a817b19457d38c491e6c0bcffd), [`99733c01`](https://github.com/xyflow/xyflow/commit/99733c01bc70f9463e7dba0046c5f8d839a1d2ba)]:
- @xyflow/system@0.0.38
## 12.0.4
### Patch Changes
- [#4480](https://github.com/xyflow/xyflow/pull/4480) [`aae526f4`](https://github.com/xyflow/xyflow/commit/aae526f4ce0818e8ab5ee9f44dd7ce4b70eb4cf9) Thanks [@peterkogo](https://github.com/peterkogo)! - fix(dragging) nodeExtent breaks dragging nodes in subflows
- [#4498](https://github.com/xyflow/xyflow/pull/4498) [`7a6e9e30`](https://github.com/xyflow/xyflow/commit/7a6e9e3091c8ee0aedbf8ae6e5c4ee08485417ab) Thanks [@peterkogo](https://github.com/peterkogo)! - fix(pane) only capture pointer after a valid selection has started, fixes #4492
## 12.0.3
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.0.3",
"version": "12.2.0",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",

View File

@@ -24,7 +24,7 @@ function BackgroundComponent({
// only used for lines and cross
size,
lineWidth = 1,
offset = 2,
offset = 0,
color,
bgColor,
style,
@@ -39,13 +39,11 @@ function BackgroundComponent({
const gapXY: [number, number] = Array.isArray(gap) ? gap : [gap, gap];
const scaledGap: [number, number] = [gapXY[0] * transform[2] || 1, gapXY[1] * transform[2] || 1];
const scaledSize = patternSize * transform[2];
const offsetXY: [number, number] = Array.isArray(offset) ? offset : [offset, offset];
const scaledOffset: [number, number] = [offsetXY[0] * transform[2] || 1, offsetXY[1] * transform[2] || 1]
const patternDimensions: [number, number] = isCross ? [scaledSize, scaledSize] : scaledGap;
const patternOffset = isDots
? [scaledSize / offset, scaledSize / offset]
: [patternDimensions[0] / offset, patternDimensions[1] / offset];
const _patternId = `${patternId}${id ? id : ''}`;
return (
@@ -69,10 +67,10 @@ function BackgroundComponent({
width={scaledGap[0]}
height={scaledGap[1]}
patternUnits="userSpaceOnUse"
patternTransform={`translate(-${patternOffset[0]},-${patternOffset[1]})`}
patternTransform={`translate(-${scaledOffset[0]},-${scaledOffset[1]})`}
>
{isDots ? (
<DotPattern radius={scaledSize / offset} className={patternClassName} />
<DotPattern radius={scaledSize / 2} className={patternClassName} />
) : (
<LinePattern
dimensions={patternDimensions}

View File

@@ -21,7 +21,7 @@ export type BackgroundProps = {
/** Size of a single pattern element */
size?: number;
/** Offset of the pattern */
offset?: number;
offset?: number | [number, number];
/** Line width of the Line pattern */
lineWidth?: number;
/** Variant of the pattern

View File

@@ -43,7 +43,7 @@ export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
queue.reset();
}
// Beacuse we're using reactive state to trigger this effect, we need to flip
// Because we're using reactive state to trigger this effect, we need to flip
// it back to false.
setShouldFlush(false);
}, [shouldFlush]);

View File

@@ -1,5 +1,5 @@
// Reconnectable edges have a anchors around their handles to reconnect the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
@@ -9,8 +9,6 @@ type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = {
edge: EdgeType;
isReconnectable: boolean | 'source' | 'target';
reconnectRadius: EdgeWrapperProps['reconnectRadius'];
sourceHandleId: Edge['sourceHandle'];
targetHandleId: Edge['targetHandle'];
onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
@@ -22,8 +20,6 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
isReconnectable,
reconnectRadius,
edge,
targetHandleId,
sourceHandleId,
sourceX,
sourceY,
targetX,
@@ -38,7 +34,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
}: EdgeUpdateAnchorsProps<EdgeType>) {
const store = useStoreApi();
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const handleEdgeUpdater = (
event: React.MouseEvent<SVGGElement, MouseEvent>,
oppositeHandle: { nodeId: string; id: string | null; type: HandleType }
) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
@@ -59,18 +58,14 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? edge.target : edge.source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const isTarget = oppositeHandle.type === 'target';
setReconnecting(true);
onReconnectStart?.(event, edge, handleType);
onReconnectStart?.(event, edge, oppositeHandle.type);
const _onReconnectEnd = (evt: MouseEvent | TouchEvent) => {
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
setReconnecting(false);
onReconnectEnd?.(evt, edge, handleType);
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
};
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
@@ -80,11 +75,11 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
handleId: oppositeHandle.id,
nodeId: oppositeHandle.nodeId,
nodeLookup,
isTarget,
edgeUpdaterType: handleType,
edgeUpdaterType: oppositeHandle.type,
lib,
flowId,
cancelConnection,
@@ -101,15 +96,15 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
};
const onReconnectSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
handleEdgeUpdater(event, { nodeId: edge.target, id: edge.targetHandle ?? null, type: 'target' });
const onReconnectTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
handleEdgeUpdater(event, { nodeId: edge.source, id: edge.sourceHandle ?? null, type: 'source' });
const onReconnectMouseEnter = () => setUpdateHover(true);
const onReconnectMouseOut = () => setUpdateHover(false);
return (
<>
{(isReconnectable === 'source' || isReconnectable === true) && (
{(isReconnectable === true || isReconnectable === 'source') && (
<EdgeAnchor
position={sourcePosition}
centerX={sourceX}
@@ -121,7 +116,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
type="source"
/>
)}
{(isReconnectable === 'target' || isReconnectable === true) && (
{(isReconnectable === true || isReconnectable === 'target') && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}

View File

@@ -255,8 +255,6 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
targetPosition={targetPosition}
setUpdateHover={setUpdateHover}
setReconnecting={setReconnecting}
sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle}
/>
)}
</g>

View File

@@ -19,6 +19,8 @@ import {
type HandleType,
ConnectionMode,
OnConnect,
ConnectionState,
Optional,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
@@ -159,6 +161,8 @@ function HandleComponent(
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
nodeLookup,
connection: connectionState,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
@@ -187,13 +191,17 @@ function HandleComponent(
flowId,
doc,
lib,
nodeLookup,
});
if (isValid && connection) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
const connectionClone = structuredClone(connectionState) as Optional<ConnectionState, 'inProgress'>;
delete connectionClone.inProgress;
connectionClone.toPosition = connectionClone.toHandle ? connectionClone.toHandle.position : null;
onClickConnectEnd?.(event as unknown as MouseEvent, connectionClone);
store.setState({ connectionClickStartHandle: null });
};

View File

@@ -1,5 +1,5 @@
/*
* This component helps us to update the store with the vlues coming from the user.
* This component helps us to update the store with the values coming from the user.
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
* and values that have a dedicated setter function in the store (like `setNodes`).
*/

View File

@@ -120,7 +120,6 @@ export function Pane({
const onPointerDown = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode, edgeLookup } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect();
(event.target as Element)?.setPointerCapture?.(event.pointerId);
if (
!elementsSelectable ||
@@ -132,6 +131,8 @@ export function Pane({
return;
}
(event.target as Element)?.setPointerCapture?.(event.pointerId);
selectionStarted.current = true;
selectionInProgress.current = false;
edgeIdLookup.current = new Map();
@@ -252,9 +253,11 @@ export function Pane({
selectionStarted.current = false;
};
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
return (
<div
className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])}
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)}

View File

@@ -4,17 +4,34 @@ import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
import { useStore } from './useStore';
import type { InternalNode, Node, ReactFlowStore } from '../types';
const selector = (s: ReactFlowStore) => {
function storeSelector(s: ReactFlowStore) {
return s.connection.inProgress
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
: { ...s.connection };
};
}
function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
connectionSelector?: (connection: ConnectionState<InternalNode<NodeType>>) => SelectorReturn
): (s: ReactFlowStore) => SelectorReturn | ConnectionState<InternalNode> {
if (connectionSelector) {
const combinedSelector = (s: ReactFlowStore) => {
const connection = storeSelector(s) as ConnectionState<InternalNode<NodeType>>;
return connectionSelector(connection);
};
return combinedSelector;
}
return storeSelector;
}
/**
* Hook for accessing the connection state.
*
* @public
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node>(): ConnectionState<InternalNode<NodeType>> {
return useStore(selector, shallow) as ConnectionState<InternalNode<NodeType>>;
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
connectionSelector?: (connection: ConnectionState<InternalNode<NodeType>>) => SelectorReturn
): SelectorReturn {
const combinedSelector = getSelector<NodeType, SelectorReturn>(connectionSelector);
return useStore(combinedSelector, shallow) as SelectorReturn;
}

View File

@@ -258,6 +258,13 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return boxToRect(box);
},
getHandleConnections: ({ type, id, nodeId }) =>
Array.from(
store
.getState()
.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)
?.values() ?? []
),
};
}, []);

View File

@@ -25,6 +25,6 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
}
});
requestAnimationFrame(() => updateNodeInternals(updates));
requestAnimationFrame(() => updateNodeInternals(updates, { triggerFitView: false }));
}, []);
}

View File

@@ -101,8 +101,16 @@ export {
type EdgeAddChange,
type EdgeReplaceChange,
type KeyCode,
type ConnectionState,
type FinalConnectionState,
type ConnectionInProgress,
type NoConnection,
} from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error
import { type Handle as HandleBound } from '@xyflow/system';
export type Handle = HandleBound;
// system utils
export {
type GetBezierPathParams,

View File

@@ -77,7 +77,7 @@ const createStore = ({
// Every node gets registerd at a ResizeObserver. Whenever a node
// changes its dimensions, this function is called to measure the
// new dimensions and update the nodes.
updateNodeInternals: (updates) => {
updateNodeInternals: (updates, params = { triggerFitView: true }) => {
const {
triggerNodeChanges,
nodeLookup,
@@ -105,23 +105,28 @@ const createStore = ({
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (params.triggerFitView) {
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
}
// here we are cirmumventing the onNodesChange handler
// in order to be able to display nodes even if the user
// has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height
// attribute which they get from this handler.
set({ fitViewDone: nextFitViewDone });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
set({});
}
// here we are cirmumventing the onNodesChange handler
// in order to be able to display nodes even if the user
// has not provided an onNodesChange handler.
// Nodes are only rendered if they have a width and height
// attribute which they get from this handler.
set({ fitViewDone: nextFitViewDone });
if (changes?.length > 0) {
if (debug) {
console.log('React Flow: trigger node changes', changes);
@@ -276,7 +281,7 @@ const createStore = ({
const { nodeLookup } = get();
for (const [, node] of nodeLookup) {
const positionAbsolute = clampPosition(node.position, nodeExtent);
const positionAbsolute = clampPosition(node.internals.positionAbsolute, nodeExtent);
nodeLookup.set(node.id, {
...node,

View File

@@ -14,6 +14,8 @@ import type {
EdgePosition,
StepPathOptions,
OnError,
ConnectionState,
FinalConnectionState,
} from '@xyflow/system';
import { EdgeTypes, InternalNode, Node } from '.';
@@ -78,7 +80,12 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
onMouseLeave?: EdgeMouseHandler<EdgeType>;
reconnectRadius?: number;
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (
event: MouseEvent | TouchEvent,
edge: EdgeType,
handleType: HandleType,
connectionState: FinalConnectionState
) => void;
rfId?: string;
edgeTypes?: EdgeTypes;
onError?: OnError;

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { Rect, Viewport } from '@xyflow/system';
import type { HandleConnection, HandleType, Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -181,6 +181,22 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
* @returns the bounds of the given nodes
*/
getNodesBounds: (nodes: (NodeType | string)[]) => Rect;
* Gets all connections for a given handle belonging to a specific node.
*
* @param type - handle type 'source' or 'target'
* @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to
* @returns an array with handle connections
*/
getHandleConnections: ({
type,
id,
nodeId,
}: {
type: HandleType;
nodeId: string;
id?: string | null;
}) => HandleConnection[];
};
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<

View File

@@ -55,7 +55,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
nodes: NodeType[];
nodeLookup: NodeLookup<InternalNode<NodeType>>;
parentLookup: ParentLookup<InternalNode<NodeType>>;
edges: Edge[];
edges: EdgeType[];
edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange<NodeType> | null;
@@ -152,7 +152,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
setNodes: (nodes: NodeType[]) => void;
setEdges: (edges: EdgeType[]) => void;
setDefaultNodesAndEdges: (nodes?: NodeType[], edges?: EdgeType[]) => void;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>, params?: { triggerFitView: boolean }) => void;
updateNodePositions: UpdateNodePositions;
resetSelectedElements: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;

View File

@@ -19,10 +19,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
const changesMap = new Map<any, any[]>();
const addItemChanges: any[] = [];
for (const change of changes) {
if (change.type === 'add') {
updatedElements.push(change.item);
addItemChanges.push(change);
continue;
} else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for
@@ -73,6 +74,18 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updatedElements.push(updatedElement);
}
// we need to wait for all changes to be applied before adding new items
// to be able to add them at the correct index
if (addItemChanges.length) {
addItemChanges.forEach((change) => {
if (change.index !== undefined) {
updatedElements.splice(change.index, 0, { ...change.item });
} else {
updatedElements.push({ ...change.item });
}
});
}
return updatedElements;
}
@@ -237,7 +250,7 @@ export function getElementsDiffChanges({
const changes: any[] = [];
const itemsLookup = new Map<string, any>(items.map((item) => [item.id, item]));
for (const item of items) {
for (const [index, item] of items.entries()) {
const lookupItem = lookup.get(item.id);
const storeItem = lookupItem?.internals?.userNode ?? lookupItem;
@@ -246,7 +259,7 @@ export function getElementsDiffChanges({
}
if (storeItem === undefined) {
changes.push({ item: item, type: 'add' });
changes.push({ item: item, type: 'add', index });
}
}

View File

@@ -1,5 +1,44 @@
# @xyflow/svelte
## 0.1.17
### Patch Changes
- [#4574](https://github.com/xyflow/xyflow/pull/4574) [`b65aed19`](https://github.com/xyflow/xyflow/commit/b65aed19840c515949bef236a23d5f0a754cdeb4) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Add `getHandleConnections` helper to `useReactFlow` & `useSvelteFlow`
- Updated dependencies [[`5138d90b`](https://github.com/xyflow/xyflow/commit/5138d90bdb91ff5d8dbeb8c8d29bdfd31c5b59d6), [`12dbe125`](https://github.com/xyflow/xyflow/commit/12dbe125755fad7d2f6dff19100872dd823d1012)]:
- @xyflow/system@0.0.40
## 0.1.16
### Patch Changes
- [#4568](https://github.com/xyflow/xyflow/pull/4568) [`c3e62782`](https://github.com/xyflow/xyflow/commit/c3e6278222dc13333f75ecdbe634201ddabab87a) Thanks [@peterkogo](https://github.com/peterkogo)! - Only display grab cursor when panOnDrag is on left mouse button
- [#4569](https://github.com/xyflow/xyflow/pull/4569) [`54bfb6d9`](https://github.com/xyflow/xyflow/commit/54bfb6d9383b2c041f243c1ba16ce169c2b90085) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix getIntersectingNodes for subflows
- Updated dependencies [[`c3e62782`](https://github.com/xyflow/xyflow/commit/c3e6278222dc13333f75ecdbe634201ddabab87a)]:
- @xyflow/system@0.0.39
## 0.1.15
### Patch Changes
- [#4510](https://github.com/xyflow/xyflow/pull/4510) [`12313a5b`](https://github.com/xyflow/xyflow/commit/12313a5b01312ef4425d3fa666e578961a151fe2) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Rename `isConnectable` prop locally to `isConnectableProp` to avoid naming collision with derived value of `isConnectable` in `<Handle>` component.
- [#4517](https://github.com/xyflow/xyflow/pull/4517) [`085951bc`](https://github.com/xyflow/xyflow/commit/085951bc07f02ac7af143409fe156bade8a63113) Thanks [@ghostdevv](https://github.com/ghostdevv)! - fix: make svelte-preprocess a dev dep
- [#4549](https://github.com/xyflow/xyflow/pull/4549) [`99733c01`](https://github.com/xyflow/xyflow/commit/99733c01bc70f9463e7dba0046c5f8d839a1d2ba) Thanks [@moklick](https://github.com/moklick)! - feat(onConnectEnd): pass connectionState param
- Updated dependencies [[`b63a3734`](https://github.com/xyflow/xyflow/commit/b63a3734b84b6817603c8e6e48e2836f048acc3b), [`24e87e39`](https://github.com/xyflow/xyflow/commit/24e87e398419646f671af1085fbfec3e197bc56b), [`692e6440`](https://github.com/xyflow/xyflow/commit/692e6440b10e75cb31f3f3172aede9ed4d7f05d2), [`559d4926`](https://github.com/xyflow/xyflow/commit/559d49264b940f93c5e205bf984aa76230b10806), [`4ecfd7e1`](https://github.com/xyflow/xyflow/commit/4ecfd7e19720b70024d0b5dff27d4537dd46b49a), [`e7ef328f`](https://github.com/xyflow/xyflow/commit/e7ef328f8f9286a817b19457d38c491e6c0bcffd), [`99733c01`](https://github.com/xyflow/xyflow/commit/99733c01bc70f9463e7dba0046c5f8d839a1d2ba)]:
- @xyflow/system@0.0.38
## 0.1.14
### Patch Changes
- [#4498](https://github.com/xyflow/xyflow/pull/4498) [`7a6e9e30`](https://github.com/xyflow/xyflow/commit/7a6e9e3091c8ee0aedbf8ae6e5c4ee08485417ab) Thanks [@peterkogo](https://github.com/peterkogo)! - fix(pane) only capture pointer after a valid selection has started, fixes #4492
## 0.1.13
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/svelte",
"version": "0.1.13",
"version": "0.1.17",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [
"svelte",
@@ -43,8 +43,7 @@
"dependencies": {
"@svelte-put/shortcut": "^3.1.0",
"@xyflow/system": "workspace:*",
"classcat": "^5.0.4",
"svelte-preprocess": "^5.1.3"
"classcat": "^5.0.4"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.1.1",
@@ -69,6 +68,7 @@
"svelte": "^4.2.12",
"svelte-check": "^3.6.7",
"svelte-eslint-parser": "^0.33.1",
"svelte-preprocess": "^5.1.3",
"tslib": "^2.6.2",
"typescript": "5.4.2"
},

View File

@@ -21,7 +21,6 @@
export let type: $$Props['type'] = 'source';
export let position: $$Props['position'] = Position.Top;
export let style: $$Props['style'] = undefined;
export let isConnectable: $$Props['isConnectable'] = undefined;
export let isValidConnection: $$Props['isValidConnection'] = undefined;
export let onconnect: $$Props['onconnect'] = undefined;
export let ondisconnect: $$Props['ondisconnect'] = undefined;
@@ -29,13 +28,16 @@
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
let isConnectableProp: $$Props['isConnectable'] = undefined;
export { isConnectableProp as isConnectable };
let className: $$Props['class'] = undefined;
export { className as class };
$: isTarget = type === 'target';
const nodeId = getContext<string>('svelteflow__node_id');
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
$: isConnectable = isConnectable !== undefined ? isConnectable : $connectable;
$: isConnectable = isConnectableProp !== undefined ? isConnectableProp : $connectable;
$: handleId = id || null;
@@ -99,8 +101,8 @@
handleType: startParams.handleType
});
},
onConnectEnd: (event) => {
$onConnectEndAction?.(event);
onConnectEnd: (event, connectionState) => {
$onConnectEndAction?.(event, connectionState);
},
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getFromHandle: () => $connection.fromHandle

View File

@@ -92,7 +92,6 @@
function onPointerDown(event: PointerEvent) {
containerBounds = container.getBoundingClientRect();
(event.target as Element)?.setPointerCapture?.(event.pointerId);
if (
!elementsSelectable ||
@@ -104,6 +103,8 @@
return;
}
(event.target as Element)?.setPointerCapture?.(event.pointerId);
const { x, y } = getEventPosition(event, containerBounds);
unselectNodesAndEdges();
@@ -211,7 +212,7 @@
<div
bind:this={container}
class="svelte-flow__pane"
class:draggable={panOnDrag}
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
class:dragging={$dragging}
class:selection={isSelecting}
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}

View File

@@ -19,6 +19,9 @@ import {
getBoundsOfBoxes,
boxToRect,
isInternalNodeBase
evaluateAbsolutePosition,
type HandleType,
type HandleConnection
} from '@xyflow/system';
import { useStore } from '$lib/store';
@@ -242,6 +245,22 @@ export function useSvelteFlow(): {
* @returns the bounds of the given nodes
*/
getNodesBounds: (nodes: (Node | InternalNode | string)[]) => Rect;
* Gets all connections for a given handle belonging to a specific node.
*
* @param type - handle type 'source' or 'target'
* @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to
* @returns an array with handle connections
*/
getHandleConnections: ({
type,
id,
nodeId
}: {
type: HandleType;
nodeId: string;
id?: string | null;
}) => HandleConnection[];
} {
const {
zoomIn,
@@ -260,15 +279,32 @@ export function useSvelteFlow(): {
domNode,
nodeLookup,
nodeOrigin,
edgeLookup
edgeLookup,
connectionLookup,
} = useStore();
const getNodeRect = (nodeOrRect: Node | { id: Node['id'] }): Rect | null => {
const node =
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
? nodeOrRect
: get(nodeLookup).get(nodeOrRect.id);
return node ? nodeToRect(node) : null;
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
const $nodeLookup = get(nodeLookup);
const nodeToUse = isNode(node) ? node : $nodeLookup.get(node.id)!;
const position = nodeToUse.parentId
? evaluateAbsolutePosition(
nodeToUse.position,
nodeToUse.measured,
nodeToUse.parentId,
$nodeLookup,
get(nodeOrigin)
)
: nodeToUse.position;
const nodeWithPosition = {
id: nodeToUse.id,
position,
width: nodeToUse.measured?.width ?? nodeToUse.width,
height: nodeToUse.measured?.height ?? nodeToUse.height,
data: nodeToUse.data
};
return nodeToRect(nodeWithPosition);
};
const updateNode = (
@@ -519,7 +555,6 @@ export function useSvelteFlow(): {
nodes.update((nds) => nds);
},
viewport,
getNodesBounds: (nodes) => {
if (nodes.length === 0) {
return { x: 0, y: 0, width: 0, height: 0 };
@@ -547,6 +582,13 @@ export function useSvelteFlow(): {
return boxToRect(box);
}
getHandleConnections: ({ type, id, nodeId }) =>
Array.from(
get(connectionLookup)
.get(`${nodeId}-${type}-${id ?? null}`)
?.values() ?? []
),
viewport
};
}
function getElements(lookup: Map<string, InternalNode>, ids: string[]): Node[];

View File

@@ -1,5 +1,37 @@
# @xyflow/system
## 0.0.40
### Patch Changes
- [#4594](https://github.com/xyflow/xyflow/pull/4594) [`5138d90b`](https://github.com/xyflow/xyflow/commit/5138d90bdb91ff5d8dbeb8c8d29bdfd31c5b59d6) Thanks [@peterkogo](https://github.com/peterkogo)! - Fixed reconnecting edges with loose connectionMode
- [#4603](https://github.com/xyflow/xyflow/pull/4603) [`12dbe125`](https://github.com/xyflow/xyflow/commit/12dbe125755fad7d2f6dff19100872dd823d1012) Thanks [@moklick](https://github.com/moklick)! - use correct index when using setNodes for inserting
## 0.0.39
### Patch Changes
- [#4568](https://github.com/xyflow/xyflow/pull/4568) [`c3e62782`](https://github.com/xyflow/xyflow/commit/c3e6278222dc13333f75ecdbe634201ddabab87a) Thanks [@peterkogo](https://github.com/peterkogo)! - Only display grab cursor when panOnDrag is on left mouse button
## 0.0.38
### Patch Changes
- [#4544](https://github.com/xyflow/xyflow/pull/4544) [`b63a3734`](https://github.com/xyflow/xyflow/commit/b63a3734b84b6817603c8e6e48e2836f048acc3b) Thanks [@moklick](https://github.com/moklick)! - strengthen css selector for edges for overflow visible
- [#4555](https://github.com/xyflow/xyflow/pull/4555) [`24e87e39`](https://github.com/xyflow/xyflow/commit/24e87e398419646f671af1085fbfec3e197bc56b) Thanks [@peterkogo](https://github.com/peterkogo)! - Added final connection state as a function parameter to onReconnectEnd
- [#4548](https://github.com/xyflow/xyflow/pull/4548) [`692e6440`](https://github.com/xyflow/xyflow/commit/692e6440b10e75cb31f3f3172aede9ed4d7f05d2) Thanks [@peterkogo](https://github.com/peterkogo)! - Replaced algorithm used for searching close handles while connecting
- [#4519](https://github.com/xyflow/xyflow/pull/4519) [`559d4926`](https://github.com/xyflow/xyflow/commit/559d49264b940f93c5e205bf984aa76230b10806) Thanks [@peterkogo](https://github.com/peterkogo)! - fix(connection) snapped position not updated correctly
- [#4538](https://github.com/xyflow/xyflow/pull/4538) [`4ecfd7e1`](https://github.com/xyflow/xyflow/commit/4ecfd7e19720b70024d0b5dff27d4537dd46b49a) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Use the handle id of the matching handle type when warning about an edge that can't be created due to missing handle ids.
- [#4536](https://github.com/xyflow/xyflow/pull/4536) [`e7ef328f`](https://github.com/xyflow/xyflow/commit/e7ef328f8f9286a817b19457d38c491e6c0bcffd) Thanks [@peterkogo](https://github.com/peterkogo)! - fix(onlyRenderVisible) edges to offscreen nodes with fixed width & height displayed correctly
- [#4549](https://github.com/xyflow/xyflow/pull/4549) [`99733c01`](https://github.com/xyflow/xyflow/commit/99733c01bc70f9463e7dba0046c5f8d839a1d2ba) Thanks [@moklick](https://github.com/moklick)! - feat(onConnectEnd): pass connectionState param
## 0.0.37
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.37",
"version": "0.0.40",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",

View File

@@ -16,7 +16,7 @@ export const errorMessages = {
{ id, sourceHandle, targetHandle }: { id: string; sourceHandle: string | null; targetHandle: string | null }
) =>
`Couldn't create edge for ${handleType} handle id: "${
!sourceHandle ? sourceHandle : targetHandle
handleType === 'source' ? sourceHandle : targetHandle
}", edge id: ${id}.`,
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,

View File

@@ -69,16 +69,16 @@
.xy-flow__pane {
z-index: 1;
&.selection {
cursor: pointer;
}
&.draggable {
cursor: grab;
}
&.dragging {
cursor: grabbing;
}
&.dragging {
cursor: grabbing;
}
&.selection {
cursor: pointer;
}
}
@@ -113,7 +113,7 @@
fill: none;
}
.xy-flow__edges {
.xy-flow .xy-flow__edges {
position: absolute;
svg {

View File

@@ -32,6 +32,7 @@ export type NodeRemoveChange = {
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
item: NodeType;
type: 'add';
index?: number;
};
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
@@ -57,6 +58,7 @@ export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
item: EdgeType;
type: 'add';
index?: number;
};
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {

View File

@@ -50,7 +50,7 @@ export type OnConnectStartParams = {
export type OnConnectStart = (event: MouseEvent | TouchEvent, params: OnConnectStartParams) => void;
export type OnConnect = (connection: Connection) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
@@ -173,6 +173,11 @@ export type ConnectionState<NodeType extends InternalNodeBase = InternalNodeBase
| ConnectionInProgress<NodeType>
| NoConnection;
export type FinalConnectionState<NodeType extends InternalNodeBase = InternalNodeBase> = Omit<
ConnectionState<NodeType>,
'inProgress'
>;
export type UpdateConnection<NodeType extends InternalNodeBase = InternalNodeBase> = (
params: ConnectionState<NodeType>
) => void;

View File

@@ -196,21 +196,22 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
const visibleNodes: InternalNodeBase<NodeType>[] = [];
for (const [, node] of nodes) {
for (const node of nodes.values()) {
const { measured, selectable = true, hidden = false } = node;
const width = measured.width ?? node.width ?? node.initialWidth ?? null;
const height = measured.height ?? node.height ?? node.initialHeight ?? null;
if ((excludeNonSelectableNodes && !selectable) || hidden) {
continue;
}
const width = measured.width ?? node.width ?? node.initialWidth ?? null;
const height = measured.height ?? node.height ?? node.initialHeight ?? null;
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node));
const notInitialized = width === null || height === null;
const area = (width ?? 0) * (height ?? 0);
const partiallyVisible = partially && overlappingArea > 0;
const area = (width ?? 0) * (height ?? 0);
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
const forceInitialRender = !node.internals.handleBounds;
const isVisible = forceInitialRender || partiallyVisible || overlappingArea >= area;
if (isVisible || node.dragging) {
visibleNodes.push(node);

View File

@@ -15,7 +15,7 @@ import {
type Connection,
} from '../types';
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
import { getClosestHandle, isConnectionValid, getHandleType, getHandle } from './utils';
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
const alwaysValid = () => true;
@@ -61,19 +61,17 @@ function onPointerDown(
return;
}
const fromHandleInternal = getHandle(nodeId, handleType, handleId, nodeLookup, connectionMode);
if (!fromHandleInternal) {
return;
}
let position = getEventPosition(event, containerBounds);
let autoPanStarted = false;
let connection: Connection | null = null;
let isValid: boolean | null = false;
let handleDomNode: Element | null = null;
const [handleLookup, fromHandleInternal] = getHandleLookup({
nodeLookup,
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) {
@@ -128,7 +126,8 @@ function onPointerDown(
closestHandle = getClosestHandle(
pointToRendererPoint(position, transform, false, [1, 1]),
connectionRadius,
handleLookup
nodeLookup,
fromHandle
);
if (!autoPanStarted) {
@@ -146,7 +145,7 @@ function onPointerDown(
doc,
lib,
flowId,
handleLookup,
nodeLookup,
});
handleDomNode = result.handleDomNode;
@@ -175,7 +174,9 @@ function onPointerDown(
newConnection.toHandle &&
previousConnection.toHandle.type === newConnection.toHandle.type &&
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
previousConnection.toHandle.id === newConnection.toHandle.id
previousConnection.toHandle.id === newConnection.toHandle.id &&
previousConnection.to.x === newConnection.to.x &&
previousConnection.to.y === newConnection.to.y
) {
return;
}
@@ -191,10 +192,16 @@ function onPointerDown(
// it's important to get a fresh reference from the store here
// in order to get the latest state of onConnectEnd
onConnectEnd?.(event);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { inProgress, ...connectionState } = previousConnection;
const finalConnectionState = {
...connectionState,
toPosition: previousConnection.toHandle ? previousConnection.toPosition : null,
};
onConnectEnd?.(event, finalConnectionState);
if (edgeUpdaterType) {
onReconnectEnd?.(event);
onReconnectEnd?.(event, finalConnectionState);
}
cancelConnection();
@@ -231,7 +238,7 @@ function isValidHandle(
lib,
flowId,
isValidConnection = alwaysValid,
handleLookup,
nodeLookup,
}: IsValidParams
) {
const isTarget = fromType === 'target';
@@ -259,7 +266,7 @@ function isValidHandle(
const connectable = handleToCheck.classList.contains('connectable');
const connectableEnd = handleToCheck.classList.contains('connectableend');
if (!handleNodeId) {
if (!handleNodeId || !handleType) {
return result;
}
@@ -282,13 +289,7 @@ function isValidHandle(
result.isValid = isValid && isValidConnection(connection);
const toHandle = handleLookup?.get(`${handleNodeId}-${handleType}-${handleId}`);
if (toHandle) {
result.toHandle = {
...toHandle,
};
}
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, false);
}
return result;

View File

@@ -11,6 +11,8 @@ import {
type UpdateConnection,
type IsValidConnection,
NodeLookup,
ConnectionState,
FinalConnectionState,
} from '../types';
export type OnPointerDownParams = {
@@ -32,7 +34,7 @@ export type OnPointerDownParams = {
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
onReconnectEnd?: (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => void;
getTransform: () => Transform;
getFromHandle: () => Handle | null;
autoPanSpeed?: number;
@@ -48,7 +50,7 @@ export type IsValidParams = {
doc: Document | ShadowRoot;
lib: string;
flowId: string | null;
handleLookup?: Map<string, Handle>;
nodeLookup: NodeLookup;
};
export type XYHandleInstance = {

View File

@@ -1,109 +1,100 @@
import { getHandlePosition } from '../utils';
import {
type HandleType,
type NodeHandleBounds,
type XYPosition,
type Handle,
InternalNodeBase,
NodeLookup,
} from '../types';
import { getHandlePosition, getOverlappingArea, nodeToRect } from '../utils';
import type { HandleType, XYPosition, Handle, InternalNodeBase, NodeLookup, ConnectionMode } from '../types';
// this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position
function getHandles(
node: InternalNodeBase,
handleBounds: NodeHandleBounds,
type: HandleType,
currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
): [Handle[], Handle | null] {
let excludedHandle = null;
const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
excludedHandle = handle;
} else {
const handleXY = getHandlePosition(node, handle, handle.position, true);
res.push({ ...handle, ...handleXY });
function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, distance: number): InternalNodeBase[] {
const nodes: InternalNodeBase[] = [];
const rect = {
x: position.x - distance,
y: position.y - distance,
width: distance * 2,
height: distance * 2,
};
for (const node of nodeLookup.values()) {
if (getOverlappingArea(rect, nodeToRect(node)) > 0) {
nodes.push(node);
}
return res;
}, []);
return [handles, excludedHandle];
}
return nodes;
}
// this distance is used for the area around the user pointer
// while doing a connection for finding the closest nodes
const ADDITIONAL_DISTANCE = 250;
export function getClosestHandle(
pos: XYPosition,
position: XYPosition,
connectionRadius: number,
handleLookup: Map<string, Handle>
nodeLookup: NodeLookup,
fromHandle: { nodeId: string; type: HandleType; id?: string | null }
): Handle | null {
let closestHandles: Handle[] = [];
let minDistance = Infinity;
for (const handle of handleLookup.values()) {
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
if (distance <= connectionRadius) {
const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE);
for (const node of closeNodes) {
const allHandles = [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])];
for (const handle of allHandles) {
// if the handle is the same as the fromHandle we skip it
if (fromHandle.nodeId === handle.nodeId && fromHandle.type === handle.type && fromHandle.id === handle.id) {
continue;
}
// determine absolute position of the handle
const { x, y } = getHandlePosition(node, handle, handle.position, true);
const distance = Math.sqrt(Math.pow(x - position.x, 2) + Math.pow(y - position.y, 2));
if (distance > connectionRadius) {
continue;
}
if (distance < minDistance) {
closestHandles = [handle];
closestHandles = [{ ...handle, x, y }];
minDistance = distance;
} else if (distance === minDistance) {
// when multiple handles are on the same distance we collect all of them
closestHandles.push(handle);
closestHandles.push({ ...handle, x, y });
}
minDistance = distance;
}
}
if (!closestHandles.length) {
return null;
}
// when multiple handles overlay each other we prefer the opposite handle
if (closestHandles.length > 1) {
const oppositeHandleType = fromHandle.type === 'source' ? 'target' : 'source';
return closestHandles.find((handle) => handle.type === oppositeHandleType) ?? closestHandles[0];
}
return closestHandles.length === 1
? closestHandles[0]
: // if multiple handles are layouted on top of each other we take the one with type = target because it's more likely that the user wants to connect to this one
closestHandles.find((handle) => handle.type === 'target') || closestHandles[0];
return closestHandles[0];
}
type GetHandleLookupParams = {
nodeLookup: NodeLookup;
nodeId: string;
handleId: string | null;
handleType: HandleType;
};
export function getHandleLookup({
nodeLookup,
nodeId,
handleId,
handleType,
}: GetHandleLookupParams): [Map<string, Handle>, Handle] {
const connectionHandles: Map<string, Handle> = new Map();
const currentHandle = { nodeId, handleId, handleType };
let matchingHandle: Handle | null = null;
for (const node of nodeLookup.values()) {
if (node.internals.handleBounds) {
const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
matchingHandle = matchingHandle ? matchingHandle : excludedSource ?? excludedTarget;
[...sourceHandles, ...targetHandles].forEach((handle) =>
connectionHandles.set(`${handle.nodeId}-${handle.type}-${handle.id}`, handle)
);
}
export function getHandle(
nodeId: string,
handleType: HandleType,
handleId: string | null,
nodeLookup: NodeLookup,
connectionMode: ConnectionMode,
withAbsolutePosition = false
): Handle | null {
const node = nodeLookup.get(nodeId);
if (!node) {
return null;
}
// if the user only works with handles that are type="source" + connectionMode="loose"
// it happens that we can't find a matching handle. The reason for this is, that the
// edge don't know about the handles and always assumes that there is source and a target.
// In this case we need to find the matching handle by switching the handleType
if (!matchingHandle) {
const node = nodeLookup.get(nodeId);
if (node?.internals.handleBounds) {
currentHandle.handleType = handleType === 'source' ? 'target' : 'source';
const [, excluded] = getHandles(node, node.internals.handleBounds, currentHandle.handleType, currentHandle);
matchingHandle = excluded;
}
}
const handles =
connectionMode === 'strict'
? node.internals.handleBounds?.[handleType]
: [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])];
const handle = (handleId ? handles?.find((h) => h.id === handleId) : handles?.[0]) ?? null;
return [connectionHandles, matchingHandle!];
return handle && withAbsolutePosition
? { ...handle, ...getHandlePosition(node, handle, handle.position, true) }
: handle;
}
export function getHandleType(

943
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff