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 AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
|
||||||
import DevTools from '../examples/DevTools';
|
import DevTools from '../examples/DevTools';
|
||||||
import Redux from '../examples/Redux';
|
import Redux from '../examples/Redux';
|
||||||
|
import MovingHandles from '../examples/MovingHandles';
|
||||||
|
|
||||||
export interface IRoute {
|
export interface IRoute {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -206,6 +207,11 @@ const routes: IRoute[] = [
|
|||||||
path: 'multi-setnodes',
|
path: 'multi-setnodes',
|
||||||
component: MultiSetNodes,
|
component: MultiSetNodes,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Moving Handles',
|
||||||
|
path: 'moving-handles',
|
||||||
|
component: MovingHandles,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Multi Flows',
|
name: 'Multi Flows',
|
||||||
path: 'multiflows',
|
path: 'multiflows',
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import { Handle, NodeProps, Position, ReactFlowState, useStore } from '@xyflow/react';
|
import { Handle, NodeProps, Position, useConnection } from '@xyflow/react';
|
||||||
|
|
||||||
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionStartHandle?.nodeId;
|
|
||||||
|
|
||||||
export default function CustomNode({ id }: NodeProps) {
|
export default function CustomNode({ id }: NodeProps) {
|
||||||
const connectionNodeId = useStore(connectionNodeIdSelector);
|
const connection = useConnection();
|
||||||
const isConnecting = !!connectionNodeId;
|
const isTarget = connection.inProgress && connection.fromNode.id !== id;
|
||||||
const isTarget = connectionNodeId && connectionNodeId !== id;
|
|
||||||
|
|
||||||
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
|
|
||||||
const label = isTarget ? 'Drop here' : 'Drag to connect';
|
const label = isTarget ? 'Drop here' : 'Drag to connect';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,17 +14,13 @@ export default function CustomNode({ id }: NodeProps) {
|
|||||||
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
|
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isConnecting && (
|
{/* 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/ */}
|
||||||
<Handle className="customHandle" style={{ zIndex: 2 }} position={Position.Right} type="source" />
|
{/* 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}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useCallback } from 'react';
|
import { EdgeProps, getStraightPath, useInternalNode } from '@xyflow/react';
|
||||||
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
|
|
||||||
|
|
||||||
import { getEdgeParams } from './utils.js';
|
import { getEdgeParams } from './utils.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
import { useCallback } from 'react';
|
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 CustomNode from './CustomNode';
|
||||||
import FloatingEdge from './FloatingEdge';
|
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 xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h);
|
||||||
const yy1 = (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 xx3 = a * xx1;
|
||||||
const yy3 = a * yy1;
|
const yy3 = a * yy1;
|
||||||
const x = w * (xx3 + yy3) + x2;
|
const x = w * (xx3 + yy3) + x2;
|
||||||
const y = h * (-xx3 + yy3) + y2;
|
const y = h * (-xx3 + yy3) + y2;
|
||||||
|
|
||||||
return { x, y };
|
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);
|
||||||
@@ -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();
|
const { getIntersectingNodes } = useSvelteFlow();
|
||||||
|
|
||||||
function onNodeDrag({ detail: { targetNode } }) {
|
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) => {
|
$nodes.forEach((n) => {
|
||||||
n.class = intersections.includes(n.id) ? 'highlight' : '';
|
n.class = intersections.includes(n.id) ? 'highlight' : '';
|
||||||
});
|
});
|
||||||
$nodes = $nodes;
|
$nodes = $nodes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ export const initialNodes: Node[] = [
|
|||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
data: { label: 'Node 2' },
|
data: { label: 'Node 2' },
|
||||||
position: { x: 0, y: 150 }
|
position: { x: 0, y: 150 },
|
||||||
|
parentId: '1'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
# @xyflow/react
|
# @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
|
## 12.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/react",
|
"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.",
|
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function BackgroundComponent({
|
|||||||
// only used for lines and cross
|
// only used for lines and cross
|
||||||
size,
|
size,
|
||||||
lineWidth = 1,
|
lineWidth = 1,
|
||||||
offset = 2,
|
offset = 0,
|
||||||
color,
|
color,
|
||||||
bgColor,
|
bgColor,
|
||||||
style,
|
style,
|
||||||
@@ -39,13 +39,11 @@ function BackgroundComponent({
|
|||||||
const gapXY: [number, number] = Array.isArray(gap) ? gap : [gap, gap];
|
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 scaledGap: [number, number] = [gapXY[0] * transform[2] || 1, gapXY[1] * transform[2] || 1];
|
||||||
const scaledSize = patternSize * transform[2];
|
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 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 : ''}`;
|
const _patternId = `${patternId}${id ? id : ''}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -69,10 +67,10 @@ function BackgroundComponent({
|
|||||||
width={scaledGap[0]}
|
width={scaledGap[0]}
|
||||||
height={scaledGap[1]}
|
height={scaledGap[1]}
|
||||||
patternUnits="userSpaceOnUse"
|
patternUnits="userSpaceOnUse"
|
||||||
patternTransform={`translate(-${patternOffset[0]},-${patternOffset[1]})`}
|
patternTransform={`translate(-${scaledOffset[0]},-${scaledOffset[1]})`}
|
||||||
>
|
>
|
||||||
{isDots ? (
|
{isDots ? (
|
||||||
<DotPattern radius={scaledSize / offset} className={patternClassName} />
|
<DotPattern radius={scaledSize / 2} className={patternClassName} />
|
||||||
) : (
|
) : (
|
||||||
<LinePattern
|
<LinePattern
|
||||||
dimensions={patternDimensions}
|
dimensions={patternDimensions}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export type BackgroundProps = {
|
|||||||
/** Size of a single pattern element */
|
/** Size of a single pattern element */
|
||||||
size?: number;
|
size?: number;
|
||||||
/** Offset of the pattern */
|
/** Offset of the pattern */
|
||||||
offset?: number;
|
offset?: number | [number, number];
|
||||||
/** Line width of the Line pattern */
|
/** Line width of the Line pattern */
|
||||||
lineWidth?: number;
|
lineWidth?: number;
|
||||||
/** Variant of the pattern
|
/** Variant of the pattern
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
|
|||||||
queue.reset();
|
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.
|
// it back to false.
|
||||||
setShouldFlush(false);
|
setShouldFlush(false);
|
||||||
}, [shouldFlush]);
|
}, [shouldFlush]);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Reconnectable edges have a anchors around their handles to reconnect the edge.
|
// 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 { EdgeAnchor } from '../Edges/EdgeAnchor';
|
||||||
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
import type { EdgeWrapperProps, Edge } from '../../types/edges';
|
||||||
@@ -9,8 +9,6 @@ type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = {
|
|||||||
edge: EdgeType;
|
edge: EdgeType;
|
||||||
isReconnectable: boolean | 'source' | 'target';
|
isReconnectable: boolean | 'source' | 'target';
|
||||||
reconnectRadius: EdgeWrapperProps['reconnectRadius'];
|
reconnectRadius: EdgeWrapperProps['reconnectRadius'];
|
||||||
sourceHandleId: Edge['sourceHandle'];
|
|
||||||
targetHandleId: Edge['targetHandle'];
|
|
||||||
onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
|
onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
|
||||||
onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
|
onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
|
||||||
onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
|
onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
|
||||||
@@ -22,8 +20,6 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
isReconnectable,
|
isReconnectable,
|
||||||
reconnectRadius,
|
reconnectRadius,
|
||||||
edge,
|
edge,
|
||||||
targetHandleId,
|
|
||||||
sourceHandleId,
|
|
||||||
sourceX,
|
sourceX,
|
||||||
sourceY,
|
sourceY,
|
||||||
targetX,
|
targetX,
|
||||||
@@ -38,7 +34,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
}: EdgeUpdateAnchorsProps<EdgeType>) {
|
}: EdgeUpdateAnchorsProps<EdgeType>) {
|
||||||
const store = useStoreApi();
|
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
|
// avoid triggering edge updater if mouse btn is not left
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return;
|
return;
|
||||||
@@ -59,18 +58,14 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
panBy,
|
panBy,
|
||||||
updateConnection,
|
updateConnection,
|
||||||
} = store.getState();
|
} = store.getState();
|
||||||
const nodeId = isSourceHandle ? edge.target : edge.source;
|
const isTarget = oppositeHandle.type === 'target';
|
||||||
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
|
|
||||||
const handleType = isSourceHandle ? 'target' : 'source';
|
|
||||||
|
|
||||||
const isTarget = isSourceHandle;
|
|
||||||
|
|
||||||
setReconnecting(true);
|
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);
|
setReconnecting(false);
|
||||||
onReconnectEnd?.(evt, edge, handleType);
|
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
|
||||||
@@ -80,11 +75,11 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
connectionMode,
|
connectionMode,
|
||||||
connectionRadius,
|
connectionRadius,
|
||||||
domNode,
|
domNode,
|
||||||
handleId,
|
handleId: oppositeHandle.id,
|
||||||
nodeId,
|
nodeId: oppositeHandle.nodeId,
|
||||||
nodeLookup,
|
nodeLookup,
|
||||||
isTarget,
|
isTarget,
|
||||||
edgeUpdaterType: handleType,
|
edgeUpdaterType: oppositeHandle.type,
|
||||||
lib,
|
lib,
|
||||||
flowId,
|
flowId,
|
||||||
cancelConnection,
|
cancelConnection,
|
||||||
@@ -101,15 +96,15 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onReconnectSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
|
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 =>
|
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 onReconnectMouseEnter = () => setUpdateHover(true);
|
||||||
const onReconnectMouseOut = () => setUpdateHover(false);
|
const onReconnectMouseOut = () => setUpdateHover(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isReconnectable === 'source' || isReconnectable === true) && (
|
{(isReconnectable === true || isReconnectable === 'source') && (
|
||||||
<EdgeAnchor
|
<EdgeAnchor
|
||||||
position={sourcePosition}
|
position={sourcePosition}
|
||||||
centerX={sourceX}
|
centerX={sourceX}
|
||||||
@@ -121,7 +116,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
|||||||
type="source"
|
type="source"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(isReconnectable === 'target' || isReconnectable === true) && (
|
{(isReconnectable === true || isReconnectable === 'target') && (
|
||||||
<EdgeAnchor
|
<EdgeAnchor
|
||||||
position={targetPosition}
|
position={targetPosition}
|
||||||
centerX={targetX}
|
centerX={targetX}
|
||||||
|
|||||||
@@ -255,8 +255,6 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
|
|||||||
targetPosition={targetPosition}
|
targetPosition={targetPosition}
|
||||||
setUpdateHover={setUpdateHover}
|
setUpdateHover={setUpdateHover}
|
||||||
setReconnecting={setReconnecting}
|
setReconnecting={setReconnecting}
|
||||||
sourceHandleId={edge.sourceHandle}
|
|
||||||
targetHandleId={edge.targetHandle}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import {
|
|||||||
type HandleType,
|
type HandleType,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
OnConnect,
|
OnConnect,
|
||||||
|
ConnectionState,
|
||||||
|
Optional,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||||
@@ -159,6 +161,8 @@ function HandleComponent(
|
|||||||
isValidConnection: isValidConnectionStore,
|
isValidConnection: isValidConnectionStore,
|
||||||
lib,
|
lib,
|
||||||
rfId: flowId,
|
rfId: flowId,
|
||||||
|
nodeLookup,
|
||||||
|
connection: connectionState,
|
||||||
} = store.getState();
|
} = store.getState();
|
||||||
|
|
||||||
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
|
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
|
||||||
@@ -187,13 +191,17 @@ function HandleComponent(
|
|||||||
flowId,
|
flowId,
|
||||||
doc,
|
doc,
|
||||||
lib,
|
lib,
|
||||||
|
nodeLookup,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isValid && connection) {
|
if (isValid && connection) {
|
||||||
onConnectExtended(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 });
|
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`)
|
* 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`).
|
* 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 onPointerDown = (event: ReactPointerEvent): void => {
|
||||||
const { resetSelectedElements, domNode, edgeLookup } = store.getState();
|
const { resetSelectedElements, domNode, edgeLookup } = store.getState();
|
||||||
containerBounds.current = domNode?.getBoundingClientRect();
|
containerBounds.current = domNode?.getBoundingClientRect();
|
||||||
(event.target as Element)?.setPointerCapture?.(event.pointerId);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!elementsSelectable ||
|
!elementsSelectable ||
|
||||||
@@ -132,6 +131,8 @@ export function Pane({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(event.target as Element)?.setPointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
selectionStarted.current = true;
|
selectionStarted.current = true;
|
||||||
selectionInProgress.current = false;
|
selectionInProgress.current = false;
|
||||||
edgeIdLookup.current = new Map();
|
edgeIdLookup.current = new Map();
|
||||||
@@ -252,9 +253,11 @@ export function Pane({
|
|||||||
selectionStarted.current = false;
|
selectionStarted.current = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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)}
|
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||||
onContextMenu={wrapHandler(onContextMenu, container)}
|
onContextMenu={wrapHandler(onContextMenu, container)}
|
||||||
onWheel={wrapHandler(onWheel, container)}
|
onWheel={wrapHandler(onWheel, container)}
|
||||||
|
|||||||
@@ -4,17 +4,34 @@ import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
|
|||||||
import { useStore } from './useStore';
|
import { useStore } from './useStore';
|
||||||
import type { InternalNode, Node, ReactFlowStore } from '../types';
|
import type { InternalNode, Node, ReactFlowStore } from '../types';
|
||||||
|
|
||||||
const selector = (s: ReactFlowStore) => {
|
function storeSelector(s: ReactFlowStore) {
|
||||||
return s.connection.inProgress
|
return s.connection.inProgress
|
||||||
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
|
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
|
||||||
: { ...s.connection };
|
: { ...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.
|
* Hook for accessing the connection state.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
* @returns ConnectionState
|
* @returns ConnectionState
|
||||||
*/
|
*/
|
||||||
export function useConnection<NodeType extends Node = Node>(): ConnectionState<InternalNode<NodeType>> {
|
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
|
||||||
return useStore(selector, shallow) as 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);
|
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 EdgeAddChange,
|
||||||
type EdgeReplaceChange,
|
type EdgeReplaceChange,
|
||||||
type KeyCode,
|
type KeyCode,
|
||||||
|
type ConnectionState,
|
||||||
|
type FinalConnectionState,
|
||||||
|
type ConnectionInProgress,
|
||||||
|
type NoConnection,
|
||||||
} from '@xyflow/system';
|
} 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
|
// system utils
|
||||||
export {
|
export {
|
||||||
type GetBezierPathParams,
|
type GetBezierPathParams,
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const createStore = ({
|
|||||||
// Every node gets registerd at a ResizeObserver. Whenever a node
|
// Every node gets registerd at a ResizeObserver. Whenever a node
|
||||||
// changes its dimensions, this function is called to measure the
|
// changes its dimensions, this function is called to measure the
|
||||||
// new dimensions and update the nodes.
|
// new dimensions and update the nodes.
|
||||||
updateNodeInternals: (updates) => {
|
updateNodeInternals: (updates, params = { triggerFitView: true }) => {
|
||||||
const {
|
const {
|
||||||
triggerNodeChanges,
|
triggerNodeChanges,
|
||||||
nodeLookup,
|
nodeLookup,
|
||||||
@@ -105,23 +105,28 @@ const createStore = ({
|
|||||||
|
|
||||||
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
|
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
|
||||||
|
|
||||||
// we call fitView once initially after all dimensions are set
|
if (params.triggerFitView) {
|
||||||
let nextFitViewDone = fitViewDone;
|
// we call fitView once initially after all dimensions are set
|
||||||
|
let nextFitViewDone = fitViewDone;
|
||||||
|
|
||||||
if (!fitViewDone && fitViewOnInit) {
|
if (!fitViewDone && fitViewOnInit) {
|
||||||
nextFitViewDone = fitViewSync({
|
nextFitViewDone = fitViewSync({
|
||||||
...fitViewOnInitOptions,
|
...fitViewOnInitOptions,
|
||||||
nodes: fitViewOnInitOptions?.nodes,
|
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 (changes?.length > 0) {
|
||||||
if (debug) {
|
if (debug) {
|
||||||
console.log('React Flow: trigger node changes', changes);
|
console.log('React Flow: trigger node changes', changes);
|
||||||
@@ -276,7 +281,7 @@ const createStore = ({
|
|||||||
const { nodeLookup } = get();
|
const { nodeLookup } = get();
|
||||||
|
|
||||||
for (const [, node] of nodeLookup) {
|
for (const [, node] of nodeLookup) {
|
||||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
const positionAbsolute = clampPosition(node.internals.positionAbsolute, nodeExtent);
|
||||||
|
|
||||||
nodeLookup.set(node.id, {
|
nodeLookup.set(node.id, {
|
||||||
...node,
|
...node,
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import type {
|
|||||||
EdgePosition,
|
EdgePosition,
|
||||||
StepPathOptions,
|
StepPathOptions,
|
||||||
OnError,
|
OnError,
|
||||||
|
ConnectionState,
|
||||||
|
FinalConnectionState,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { EdgeTypes, InternalNode, Node } from '.';
|
import { EdgeTypes, InternalNode, Node } from '.';
|
||||||
@@ -78,7 +80,12 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
|
|||||||
onMouseLeave?: EdgeMouseHandler<EdgeType>;
|
onMouseLeave?: EdgeMouseHandler<EdgeType>;
|
||||||
reconnectRadius?: number;
|
reconnectRadius?: number;
|
||||||
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
|
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;
|
rfId?: string;
|
||||||
edgeTypes?: EdgeTypes;
|
edgeTypes?: EdgeTypes;
|
||||||
onError?: OnError;
|
onError?: OnError;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* 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 '.';
|
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
|
||||||
|
|
||||||
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
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
|
* @returns the bounds of the given nodes
|
||||||
*/
|
*/
|
||||||
getNodesBounds: (nodes: (NodeType | string)[]) => Rect;
|
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<
|
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[];
|
nodes: NodeType[];
|
||||||
nodeLookup: NodeLookup<InternalNode<NodeType>>;
|
nodeLookup: NodeLookup<InternalNode<NodeType>>;
|
||||||
parentLookup: ParentLookup<InternalNode<NodeType>>;
|
parentLookup: ParentLookup<InternalNode<NodeType>>;
|
||||||
edges: Edge[];
|
edges: EdgeType[];
|
||||||
edgeLookup: EdgeLookup<EdgeType>;
|
edgeLookup: EdgeLookup<EdgeType>;
|
||||||
connectionLookup: ConnectionLookup;
|
connectionLookup: ConnectionLookup;
|
||||||
onNodesChange: OnNodesChange<NodeType> | null;
|
onNodesChange: OnNodesChange<NodeType> | null;
|
||||||
@@ -152,7 +152,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
|||||||
setNodes: (nodes: NodeType[]) => void;
|
setNodes: (nodes: NodeType[]) => void;
|
||||||
setEdges: (edges: EdgeType[]) => void;
|
setEdges: (edges: EdgeType[]) => void;
|
||||||
setDefaultNodesAndEdges: (nodes?: NodeType[], 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;
|
updateNodePositions: UpdateNodePositions;
|
||||||
resetSelectedElements: () => void;
|
resetSelectedElements: () => void;
|
||||||
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => 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
|
// By storing a map of changes for each element, we can a quick lookup as we
|
||||||
// iterate over the elements array!
|
// iterate over the elements array!
|
||||||
const changesMap = new Map<any, any[]>();
|
const changesMap = new Map<any, any[]>();
|
||||||
|
const addItemChanges: any[] = [];
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
if (change.type === 'add') {
|
if (change.type === 'add') {
|
||||||
updatedElements.push(change.item);
|
addItemChanges.push(change);
|
||||||
continue;
|
continue;
|
||||||
} else if (change.type === 'remove' || change.type === 'replace') {
|
} else if (change.type === 'remove' || change.type === 'replace') {
|
||||||
// For a 'remove' change we can safely ignore any other changes queued for
|
// 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);
|
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;
|
return updatedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +250,7 @@ export function getElementsDiffChanges({
|
|||||||
const changes: any[] = [];
|
const changes: any[] = [];
|
||||||
const itemsLookup = new Map<string, any>(items.map((item) => [item.id, item]));
|
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 lookupItem = lookup.get(item.id);
|
||||||
const storeItem = lookupItem?.internals?.userNode ?? lookupItem;
|
const storeItem = lookupItem?.internals?.userNode ?? lookupItem;
|
||||||
|
|
||||||
@@ -246,7 +259,7 @@ export function getElementsDiffChanges({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (storeItem === undefined) {
|
if (storeItem === undefined) {
|
||||||
changes.push({ item: item, type: 'add' });
|
changes.push({ item: item, type: 'add', index });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,44 @@
|
|||||||
# @xyflow/svelte
|
# @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
|
## 0.1.13
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/svelte",
|
"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.",
|
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"svelte",
|
"svelte",
|
||||||
@@ -43,8 +43,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@svelte-put/shortcut": "^3.1.0",
|
"@svelte-put/shortcut": "^3.1.0",
|
||||||
"@xyflow/system": "workspace:*",
|
"@xyflow/system": "workspace:*",
|
||||||
"classcat": "^5.0.4",
|
"classcat": "^5.0.4"
|
||||||
"svelte-preprocess": "^5.1.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.1.1",
|
"@sveltejs/adapter-auto": "^3.1.1",
|
||||||
@@ -69,6 +68,7 @@
|
|||||||
"svelte": "^4.2.12",
|
"svelte": "^4.2.12",
|
||||||
"svelte-check": "^3.6.7",
|
"svelte-check": "^3.6.7",
|
||||||
"svelte-eslint-parser": "^0.33.1",
|
"svelte-eslint-parser": "^0.33.1",
|
||||||
|
"svelte-preprocess": "^5.1.3",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "5.4.2"
|
"typescript": "5.4.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
export let type: $$Props['type'] = 'source';
|
export let type: $$Props['type'] = 'source';
|
||||||
export let position: $$Props['position'] = Position.Top;
|
export let position: $$Props['position'] = Position.Top;
|
||||||
export let style: $$Props['style'] = undefined;
|
export let style: $$Props['style'] = undefined;
|
||||||
export let isConnectable: $$Props['isConnectable'] = undefined;
|
|
||||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||||
export let onconnect: $$Props['onconnect'] = undefined;
|
export let onconnect: $$Props['onconnect'] = undefined;
|
||||||
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
export let ondisconnect: $$Props['ondisconnect'] = undefined;
|
||||||
@@ -29,13 +28,16 @@
|
|||||||
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
|
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
|
||||||
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
|
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
|
||||||
|
|
||||||
|
let isConnectableProp: $$Props['isConnectable'] = undefined;
|
||||||
|
export { isConnectableProp as isConnectable };
|
||||||
|
|
||||||
let className: $$Props['class'] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
$: isTarget = type === 'target';
|
$: isTarget = type === 'target';
|
||||||
const nodeId = getContext<string>('svelteflow__node_id');
|
const nodeId = getContext<string>('svelteflow__node_id');
|
||||||
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
|
const connectable = getContext<Writable<boolean>>('svelteflow__node_connectable');
|
||||||
$: isConnectable = isConnectable !== undefined ? isConnectable : $connectable;
|
$: isConnectable = isConnectableProp !== undefined ? isConnectableProp : $connectable;
|
||||||
|
|
||||||
$: handleId = id || null;
|
$: handleId = id || null;
|
||||||
|
|
||||||
@@ -99,8 +101,8 @@
|
|||||||
handleType: startParams.handleType
|
handleType: startParams.handleType
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onConnectEnd: (event) => {
|
onConnectEnd: (event, connectionState) => {
|
||||||
$onConnectEndAction?.(event);
|
$onConnectEndAction?.(event, connectionState);
|
||||||
},
|
},
|
||||||
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
|
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
|
||||||
getFromHandle: () => $connection.fromHandle
|
getFromHandle: () => $connection.fromHandle
|
||||||
|
|||||||
@@ -92,7 +92,6 @@
|
|||||||
|
|
||||||
function onPointerDown(event: PointerEvent) {
|
function onPointerDown(event: PointerEvent) {
|
||||||
containerBounds = container.getBoundingClientRect();
|
containerBounds = container.getBoundingClientRect();
|
||||||
(event.target as Element)?.setPointerCapture?.(event.pointerId);
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!elementsSelectable ||
|
!elementsSelectable ||
|
||||||
@@ -104,6 +103,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(event.target as Element)?.setPointerCapture?.(event.pointerId);
|
||||||
|
|
||||||
const { x, y } = getEventPosition(event, containerBounds);
|
const { x, y } = getEventPosition(event, containerBounds);
|
||||||
|
|
||||||
unselectNodesAndEdges();
|
unselectNodesAndEdges();
|
||||||
@@ -211,7 +212,7 @@
|
|||||||
<div
|
<div
|
||||||
bind:this={container}
|
bind:this={container}
|
||||||
class="svelte-flow__pane"
|
class="svelte-flow__pane"
|
||||||
class:draggable={panOnDrag}
|
class:draggable={panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0))}
|
||||||
class:dragging={$dragging}
|
class:dragging={$dragging}
|
||||||
class:selection={isSelecting}
|
class:selection={isSelecting}
|
||||||
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
on:click={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
getBoundsOfBoxes,
|
getBoundsOfBoxes,
|
||||||
boxToRect,
|
boxToRect,
|
||||||
isInternalNodeBase
|
isInternalNodeBase
|
||||||
|
evaluateAbsolutePosition,
|
||||||
|
type HandleType,
|
||||||
|
type HandleConnection
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { useStore } from '$lib/store';
|
import { useStore } from '$lib/store';
|
||||||
@@ -242,6 +245,22 @@ export function useSvelteFlow(): {
|
|||||||
* @returns the bounds of the given nodes
|
* @returns the bounds of the given nodes
|
||||||
*/
|
*/
|
||||||
getNodesBounds: (nodes: (Node | InternalNode | string)[]) => Rect;
|
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 {
|
const {
|
||||||
zoomIn,
|
zoomIn,
|
||||||
@@ -260,15 +279,32 @@ export function useSvelteFlow(): {
|
|||||||
domNode,
|
domNode,
|
||||||
nodeLookup,
|
nodeLookup,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
edgeLookup
|
edgeLookup,
|
||||||
|
connectionLookup,
|
||||||
} = useStore();
|
} = useStore();
|
||||||
|
|
||||||
const getNodeRect = (nodeOrRect: Node | { id: Node['id'] }): Rect | null => {
|
const getNodeRect = (node: Node | { id: Node['id'] }): Rect | null => {
|
||||||
const node =
|
const $nodeLookup = get(nodeLookup);
|
||||||
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
|
const nodeToUse = isNode(node) ? node : $nodeLookup.get(node.id)!;
|
||||||
? nodeOrRect
|
const position = nodeToUse.parentId
|
||||||
: get(nodeLookup).get(nodeOrRect.id);
|
? evaluateAbsolutePosition(
|
||||||
return node ? nodeToRect(node) : null;
|
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 = (
|
const updateNode = (
|
||||||
@@ -519,7 +555,6 @@ export function useSvelteFlow(): {
|
|||||||
|
|
||||||
nodes.update((nds) => nds);
|
nodes.update((nds) => nds);
|
||||||
},
|
},
|
||||||
viewport,
|
|
||||||
getNodesBounds: (nodes) => {
|
getNodesBounds: (nodes) => {
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
return { x: 0, y: 0, width: 0, height: 0 };
|
return { x: 0, y: 0, width: 0, height: 0 };
|
||||||
@@ -547,6 +582,13 @@ export function useSvelteFlow(): {
|
|||||||
|
|
||||||
return boxToRect(box);
|
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[];
|
function getElements(lookup: Map<string, InternalNode>, ids: string[]): Node[];
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# @xyflow/system
|
# @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
|
## 0.0.37
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xyflow/system",
|
"name": "@xyflow/system",
|
||||||
"version": "0.0.37",
|
"version": "0.0.40",
|
||||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"node-based UI",
|
"node-based UI",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const errorMessages = {
|
|||||||
{ id, sourceHandle, targetHandle }: { id: string; sourceHandle: string | null; targetHandle: string | null }
|
{ id, sourceHandle, targetHandle }: { id: string; sourceHandle: string | null; targetHandle: string | null }
|
||||||
) =>
|
) =>
|
||||||
`Couldn't create edge for ${handleType} handle id: "${
|
`Couldn't create edge for ${handleType} handle id: "${
|
||||||
!sourceHandle ? sourceHandle : targetHandle
|
handleType === 'source' ? sourceHandle : targetHandle
|
||||||
}", edge id: ${id}.`,
|
}", edge id: ${id}.`,
|
||||||
error010: () => 'Handle: No node id found. Make sure to only use a Handle inside a custom Node.',
|
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".`,
|
error011: (edgeType: string) => `Edge type "${edgeType}" not found. Using fallback type "default".`,
|
||||||
|
|||||||
@@ -69,16 +69,16 @@
|
|||||||
.xy-flow__pane {
|
.xy-flow__pane {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
&.selection {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.draggable {
|
&.draggable {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
&.dragging {
|
&.dragging {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.selection {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xy-flow__edges {
|
.xy-flow .xy-flow__edges {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type NodeRemoveChange = {
|
|||||||
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
|
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
|
||||||
item: NodeType;
|
item: NodeType;
|
||||||
type: 'add';
|
type: 'add';
|
||||||
|
index?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
|
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
|
||||||
@@ -57,6 +58,7 @@ export type EdgeRemoveChange = NodeRemoveChange;
|
|||||||
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
|
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||||
item: EdgeType;
|
item: EdgeType;
|
||||||
type: 'add';
|
type: 'add';
|
||||||
|
index?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
|
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 OnConnectStart = (event: MouseEvent | TouchEvent, params: OnConnectStartParams) => void;
|
||||||
export type OnConnect = (connection: Connection) => 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;
|
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
|
||||||
|
|
||||||
@@ -173,6 +173,11 @@ export type ConnectionState<NodeType extends InternalNodeBase = InternalNodeBase
|
|||||||
| ConnectionInProgress<NodeType>
|
| ConnectionInProgress<NodeType>
|
||||||
| NoConnection;
|
| NoConnection;
|
||||||
|
|
||||||
|
export type FinalConnectionState<NodeType extends InternalNodeBase = InternalNodeBase> = Omit<
|
||||||
|
ConnectionState<NodeType>,
|
||||||
|
'inProgress'
|
||||||
|
>;
|
||||||
|
|
||||||
export type UpdateConnection<NodeType extends InternalNodeBase = InternalNodeBase> = (
|
export type UpdateConnection<NodeType extends InternalNodeBase = InternalNodeBase> = (
|
||||||
params: ConnectionState<NodeType>
|
params: ConnectionState<NodeType>
|
||||||
) => void;
|
) => void;
|
||||||
|
|||||||
@@ -196,21 +196,22 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
|||||||
|
|
||||||
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
||||||
|
|
||||||
for (const [, node] of nodes) {
|
for (const node of nodes.values()) {
|
||||||
const { measured, selectable = true, hidden = false } = node;
|
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) {
|
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||||
continue;
|
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 overlappingArea = getOverlappingArea(paneRect, nodeToRect(node));
|
||||||
const notInitialized = width === null || height === null;
|
const area = (width ?? 0) * (height ?? 0);
|
||||||
|
|
||||||
const partiallyVisible = partially && overlappingArea > 0;
|
const partiallyVisible = partially && overlappingArea > 0;
|
||||||
const area = (width ?? 0) * (height ?? 0);
|
const forceInitialRender = !node.internals.handleBounds;
|
||||||
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
|
const isVisible = forceInitialRender || partiallyVisible || overlappingArea >= area;
|
||||||
|
|
||||||
if (isVisible || node.dragging) {
|
if (isVisible || node.dragging) {
|
||||||
visibleNodes.push(node);
|
visibleNodes.push(node);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type Connection,
|
type Connection,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
|
import { getClosestHandle, isConnectionValid, getHandleType, getHandle } from './utils';
|
||||||
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
|
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
|
||||||
|
|
||||||
const alwaysValid = () => true;
|
const alwaysValid = () => true;
|
||||||
@@ -61,19 +61,17 @@ function onPointerDown(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromHandleInternal = getHandle(nodeId, handleType, handleId, nodeLookup, connectionMode);
|
||||||
|
if (!fromHandleInternal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let position = getEventPosition(event, containerBounds);
|
let position = getEventPosition(event, containerBounds);
|
||||||
let autoPanStarted = false;
|
let autoPanStarted = false;
|
||||||
let connection: Connection | null = null;
|
let connection: Connection | null = null;
|
||||||
let isValid: boolean | null = false;
|
let isValid: boolean | null = false;
|
||||||
let handleDomNode: Element | null = null;
|
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
|
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
|
||||||
function autoPan(): void {
|
function autoPan(): void {
|
||||||
if (!autoPanOnConnect || !containerBounds) {
|
if (!autoPanOnConnect || !containerBounds) {
|
||||||
@@ -128,7 +126,8 @@ function onPointerDown(
|
|||||||
closestHandle = getClosestHandle(
|
closestHandle = getClosestHandle(
|
||||||
pointToRendererPoint(position, transform, false, [1, 1]),
|
pointToRendererPoint(position, transform, false, [1, 1]),
|
||||||
connectionRadius,
|
connectionRadius,
|
||||||
handleLookup
|
nodeLookup,
|
||||||
|
fromHandle
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!autoPanStarted) {
|
if (!autoPanStarted) {
|
||||||
@@ -146,7 +145,7 @@ function onPointerDown(
|
|||||||
doc,
|
doc,
|
||||||
lib,
|
lib,
|
||||||
flowId,
|
flowId,
|
||||||
handleLookup,
|
nodeLookup,
|
||||||
});
|
});
|
||||||
|
|
||||||
handleDomNode = result.handleDomNode;
|
handleDomNode = result.handleDomNode;
|
||||||
@@ -175,7 +174,9 @@ function onPointerDown(
|
|||||||
newConnection.toHandle &&
|
newConnection.toHandle &&
|
||||||
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
||||||
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -191,10 +192,16 @@ function onPointerDown(
|
|||||||
|
|
||||||
// it's important to get a fresh reference from the store here
|
// it's important to get a fresh reference from the store here
|
||||||
// in order to get the latest state of onConnectEnd
|
// 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) {
|
if (edgeUpdaterType) {
|
||||||
onReconnectEnd?.(event);
|
onReconnectEnd?.(event, finalConnectionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelConnection();
|
cancelConnection();
|
||||||
@@ -231,7 +238,7 @@ function isValidHandle(
|
|||||||
lib,
|
lib,
|
||||||
flowId,
|
flowId,
|
||||||
isValidConnection = alwaysValid,
|
isValidConnection = alwaysValid,
|
||||||
handleLookup,
|
nodeLookup,
|
||||||
}: IsValidParams
|
}: IsValidParams
|
||||||
) {
|
) {
|
||||||
const isTarget = fromType === 'target';
|
const isTarget = fromType === 'target';
|
||||||
@@ -259,7 +266,7 @@ function isValidHandle(
|
|||||||
const connectable = handleToCheck.classList.contains('connectable');
|
const connectable = handleToCheck.classList.contains('connectable');
|
||||||
const connectableEnd = handleToCheck.classList.contains('connectableend');
|
const connectableEnd = handleToCheck.classList.contains('connectableend');
|
||||||
|
|
||||||
if (!handleNodeId) {
|
if (!handleNodeId || !handleType) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,13 +289,7 @@ function isValidHandle(
|
|||||||
|
|
||||||
result.isValid = isValid && isValidConnection(connection);
|
result.isValid = isValid && isValidConnection(connection);
|
||||||
|
|
||||||
const toHandle = handleLookup?.get(`${handleNodeId}-${handleType}-${handleId}`);
|
result.toHandle = getHandle(handleNodeId, handleType, handleId, nodeLookup, connectionMode, false);
|
||||||
|
|
||||||
if (toHandle) {
|
|
||||||
result.toHandle = {
|
|
||||||
...toHandle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
type UpdateConnection,
|
type UpdateConnection,
|
||||||
type IsValidConnection,
|
type IsValidConnection,
|
||||||
NodeLookup,
|
NodeLookup,
|
||||||
|
ConnectionState,
|
||||||
|
FinalConnectionState,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type OnPointerDownParams = {
|
export type OnPointerDownParams = {
|
||||||
@@ -32,7 +34,7 @@ export type OnPointerDownParams = {
|
|||||||
onConnect?: OnConnect;
|
onConnect?: OnConnect;
|
||||||
onConnectEnd?: OnConnectEnd;
|
onConnectEnd?: OnConnectEnd;
|
||||||
isValidConnection?: IsValidConnection;
|
isValidConnection?: IsValidConnection;
|
||||||
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
|
onReconnectEnd?: (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => void;
|
||||||
getTransform: () => Transform;
|
getTransform: () => Transform;
|
||||||
getFromHandle: () => Handle | null;
|
getFromHandle: () => Handle | null;
|
||||||
autoPanSpeed?: number;
|
autoPanSpeed?: number;
|
||||||
@@ -48,7 +50,7 @@ export type IsValidParams = {
|
|||||||
doc: Document | ShadowRoot;
|
doc: Document | ShadowRoot;
|
||||||
lib: string;
|
lib: string;
|
||||||
flowId: string | null;
|
flowId: string | null;
|
||||||
handleLookup?: Map<string, Handle>;
|
nodeLookup: NodeLookup;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type XYHandleInstance = {
|
export type XYHandleInstance = {
|
||||||
|
|||||||
@@ -1,109 +1,100 @@
|
|||||||
import { getHandlePosition } from '../utils';
|
import { getHandlePosition, getOverlappingArea, nodeToRect } from '../utils';
|
||||||
import {
|
import type { HandleType, XYPosition, Handle, InternalNodeBase, NodeLookup, ConnectionMode } from '../types';
|
||||||
type HandleType,
|
|
||||||
type NodeHandleBounds,
|
|
||||||
type XYPosition,
|
|
||||||
type Handle,
|
|
||||||
InternalNodeBase,
|
|
||||||
NodeLookup,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
// this functions collects all handles and adds an absolute position
|
function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, distance: number): InternalNodeBase[] {
|
||||||
// so that we can later find the closest handle to the mouse position
|
const nodes: InternalNodeBase[] = [];
|
||||||
function getHandles(
|
const rect = {
|
||||||
node: InternalNodeBase,
|
x: position.x - distance,
|
||||||
handleBounds: NodeHandleBounds,
|
y: position.y - distance,
|
||||||
type: HandleType,
|
width: distance * 2,
|
||||||
currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
|
height: distance * 2,
|
||||||
): [Handle[], Handle | null] {
|
};
|
||||||
let excludedHandle = null;
|
|
||||||
const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
|
for (const node of nodeLookup.values()) {
|
||||||
if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
|
if (getOverlappingArea(rect, nodeToRect(node)) > 0) {
|
||||||
excludedHandle = handle;
|
nodes.push(node);
|
||||||
} else {
|
|
||||||
const handleXY = getHandlePosition(node, handle, handle.position, true);
|
|
||||||
res.push({ ...handle, ...handleXY });
|
|
||||||
}
|
}
|
||||||
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(
|
export function getClosestHandle(
|
||||||
pos: XYPosition,
|
position: XYPosition,
|
||||||
connectionRadius: number,
|
connectionRadius: number,
|
||||||
handleLookup: Map<string, Handle>
|
nodeLookup: NodeLookup,
|
||||||
|
fromHandle: { nodeId: string; type: HandleType; id?: string | null }
|
||||||
): Handle | null {
|
): Handle | null {
|
||||||
let closestHandles: Handle[] = [];
|
let closestHandles: Handle[] = [];
|
||||||
let minDistance = Infinity;
|
let minDistance = Infinity;
|
||||||
|
|
||||||
for (const handle of handleLookup.values()) {
|
const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE);
|
||||||
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
|
|
||||||
if (distance <= connectionRadius) {
|
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) {
|
if (distance < minDistance) {
|
||||||
closestHandles = [handle];
|
closestHandles = [{ ...handle, x, y }];
|
||||||
|
minDistance = distance;
|
||||||
} else if (distance === minDistance) {
|
} else if (distance === minDistance) {
|
||||||
// when multiple handles are on the same distance we collect all of them
|
// 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) {
|
if (!closestHandles.length) {
|
||||||
return null;
|
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
|
return closestHandles[0];
|
||||||
? 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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetHandleLookupParams = {
|
export function getHandle(
|
||||||
nodeLookup: NodeLookup;
|
nodeId: string,
|
||||||
nodeId: string;
|
handleType: HandleType,
|
||||||
handleId: string | null;
|
handleId: string | null,
|
||||||
handleType: HandleType;
|
nodeLookup: NodeLookup,
|
||||||
};
|
connectionMode: ConnectionMode,
|
||||||
|
withAbsolutePosition = false
|
||||||
export function getHandleLookup({
|
): Handle | null {
|
||||||
nodeLookup,
|
const node = nodeLookup.get(nodeId);
|
||||||
nodeId,
|
if (!node) {
|
||||||
handleId,
|
return null;
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the user only works with handles that are type="source" + connectionMode="loose"
|
const handles =
|
||||||
// it happens that we can't find a matching handle. The reason for this is, that the
|
connectionMode === 'strict'
|
||||||
// edge don't know about the handles and always assumes that there is source and a target.
|
? node.internals.handleBounds?.[handleType]
|
||||||
// In this case we need to find the matching handle by switching the handleType
|
: [...(node.internals.handleBounds?.source ?? []), ...(node.internals.handleBounds?.target ?? [])];
|
||||||
if (!matchingHandle) {
|
const handle = (handleId ? handles?.find((h) => h.id === handleId) : handles?.[0]) ?? null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [connectionHandles, matchingHandle!];
|
return handle && withAbsolutePosition
|
||||||
|
? { ...handle, ...getHandlePosition(node, handle, handle.position, true) }
|
||||||
|
: handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHandleType(
|
export function getHandleType(
|
||||||
|
|||||||
Generated
+725
-218
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user