Merge branch 'main' into nodesbounds
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
116
examples/react/src/examples/MovingHandles/index.tsx
Normal file
116
examples/react/src/examples/MovingHandles/index.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -255,8 +255,6 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
||||
targetPosition={targetPosition}
|
||||
setUpdateHover={setUpdateHover}
|
||||
setReconnecting={setReconnecting}
|
||||
sourceHandleId={edge.sourceHandle}
|
||||
targetHandleId={edge.targetHandle}
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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`).
|
||||
*/
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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() ?? []
|
||||
),
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -25,6 +25,6 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeInternals(updates));
|
||||
requestAnimationFrame(() => updateNodeInternals(updates, { triggerFitView: false }));
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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".`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
943
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user