Merge pull request #5187 from xyflow/migrate/svelte5-diff

implemented new fitView logic/merged master
This commit is contained in:
Peter Kogo
2025-04-09 10:54:20 +02:00
committed by GitHub
131 changed files with 5502 additions and 2295 deletions

View File

@@ -38,7 +38,7 @@
"cypress": "13.6.6",
"cypress-real-events": "1.12.0",
"start-server-and-test": "^2.0.2",
"typescript": "5.2.2",
"typescript": "5.4.5",
"vite": "4.5.0"
}
}

View File

@@ -1,5 +1,6 @@
import Basic from '../examples/Basic';
import Backgrounds from '../examples/Backgrounds';
import BrokenNodes from '../examples/BrokenNodes';
import ColorMode from '../examples/ColorMode';
import ClickDistance from '../examples/ClickDistance';
import ControlledUncontrolled from '../examples/ControlledUncontrolled';
@@ -77,6 +78,11 @@ const routes: IRoute[] = [
path: 'backgrounds',
component: Backgrounds,
},
{
name: 'Broken Nodes',
path: 'broken-nodes',
component: BrokenNodes,
},
{
name: 'Color Mode',
path: 'color-mode',

View File

@@ -56,8 +56,18 @@ const initialEdges: Edge[] = [
const defaultEdgeOptions = {};
const BasicFlow = () => {
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
useReactFlow();
const {
addNodes,
setNodes,
getNodes,
setEdges,
getEdges,
deleteElements,
updateNodeData,
toObject,
setViewport,
fitView,
} = useReactFlow();
const updatePos = () => {
setNodes((nodes) =>
@@ -104,6 +114,7 @@ const BasicFlow = () => {
]);
setEdges([{ id: 'a-b', source: 'a', target: 'b' }]);
fitView();
};
const onUpdateNode = () => {
@@ -117,6 +128,7 @@ const BasicFlow = () => {
position: { x: Math.random() * 300, y: Math.random() * 300 },
className: 'light',
});
fitView();
};
return (
@@ -134,6 +146,9 @@ const BasicFlow = () => {
minZoom={0.2}
maxZoom={4}
fitView
fitViewOptions={{
padding: { top: '100px', left: '0%', right: '10%', bottom: 0.1 },
}}
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
elevateEdgesOnSelect

View File

@@ -0,0 +1,80 @@
import { useCallback, useState } from 'react';
import { ReactFlow, addEdge, Node, Connection, Edge, OnNodeDrag } from '@xyflow/react';
const nodesInit: Node[] = [
{
id: '1a',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
ariaLabel: 'Input Node 1',
},
{
id: '2a',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
ariaLabel: 'Default Node 2',
},
{
id: '3a',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
className: 'light',
},
{
id: '4a',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
className: 'light',
},
];
const edgesInit: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const onNodesChange = () => {};
const onEdgesChange = () => {};
const BasicFlow = () => {
const [nodes, setNodes] = useState(nodesInit);
const [edges, setEdges] = useState(edgesInit);
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
const onNodeDrag: OnNodeDrag = useCallback((e, node) => {
if (isNaN(node.position.x) || isNaN(node.position.y)) {
console.log('received NaN', node.position);
}
setNodes((nds) => {
return nds.map((item) => {
if (item.id === node.id) {
return {
...item,
position: {
x: node.position.x,
y: node.position.y,
},
};
}
return item;
});
});
}, []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeDrag={onNodeDrag}
></ReactFlow>
);
};
export default BasicFlow;

View File

@@ -12,6 +12,8 @@ import {
Controls,
Background,
Panel,
ReactFlowProvider,
useReactFlow,
} from '@xyflow/react';
import { getNodesAndEdges } from './utils';
@@ -22,6 +24,7 @@ const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(25, 25);
const StressFlow = () => {
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const { fitView } = useReactFlow();
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
@@ -191,12 +194,13 @@ const StressFlow = () => {
return {
...n,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
x: Math.random() * window.innerWidth * 4,
y: Math.random() * window.innerHeight * 4,
},
};
});
});
fitView();
};
const updateElements = () => {
@@ -240,4 +244,10 @@ const StressFlow = () => {
);
};
export default StressFlow;
export default function StressFlowProvider() {
return (
<ReactFlowProvider>
<StressFlow />
</ReactFlowProvider>
);
}

View File

@@ -10,7 +10,6 @@ import {
useEdgesState,
useOnSelectionChange,
OnSelectionChangeParams,
OnSelectionChangeFunc,
} from '@xyflow/react';
const initialNodes: Node[] = [

View File

@@ -12,20 +12,20 @@
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.1",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"prettier": "^3.4.2",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.4",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^3.5.1",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.19.5",
"svelte-check": "^4.1.4",
"svelte": "^5.25.8",
"svelte-check": "^4.1.5",
"tslib": "^2.8.1",
"typescript": "^5.7.3",
"vite": "^6.0.11"
"typescript": "^5.8.3",
"vite": "^6.2.5"
},
"type": "module",
"dependencies": {

View File

@@ -25,20 +25,15 @@
"@changesets/changelog-github": "^0.4.7",
"@changesets/cli": "^2.25.0",
"@playwright/test": "^1.44.1",
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"concurrently": "^7.6.0",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "latest",
"prettier": "^2.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"rollup": "^4.18.0",
"turbo": "^2.0.3",
"typescript": "5.1.3"
"typescript": "5.4.5"
},
"packageManager": "pnpm@9.2.0"
}

View File

@@ -1,5 +1,158 @@
# @xyflow/react
## 12.5.5
### Patch Changes
- [#5172](https://github.com/xyflow/xyflow/pull/5172) [`e6139a00`](https://github.com/xyflow/xyflow/commit/e6139a00d4414ba2c1d3e500cdfa67d7e66e655a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useNodesData`, `useReactFlow`, `isNode` and `isEdge`
- [#5165](https://github.com/xyflow/xyflow/pull/5165) [`d536abea`](https://github.com/xyflow/xyflow/commit/d536abea9240bad7f5c1064efc0a4713ebef87d1) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useViewport`, `useUpdateNodeInternals`, `useOnSelectionChange`, `useNodesInitialized` hooks and `UseOnSelectionChangeOptions`, `UseNodesInitializedOptions` types
- [#5171](https://github.com/xyflow/xyflow/pull/5171) [`62d87409`](https://github.com/xyflow/xyflow/commit/62d874097337a022bebe54b559834c0d582f435e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ReactFlowProps`
- [#5154](https://github.com/xyflow/xyflow/pull/5154) [`d0237166`](https://github.com/xyflow/xyflow/commit/d02371662669aab91cd2ac7c45b412491c3377bd) Thanks [@ibagov](https://github.com/ibagov)! - Improve TSDoc comments for `onNodesChange`
- [#5174](https://github.com/xyflow/xyflow/pull/5174) [`ae585d13`](https://github.com/xyflow/xyflow/commit/ae585d136e34c9e9ad9d45f75c059bc5367e73ae) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `BaseEdgeProps`
- [#5159](https://github.com/xyflow/xyflow/pull/5159) [`0c1436d6`](https://github.com/xyflow/xyflow/commit/0c1436d6a371240cfa0adecea573c44fd42df7b3) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useConnection` hook
- [#5167](https://github.com/xyflow/xyflow/pull/5167) [`934ea42d`](https://github.com/xyflow/xyflow/commit/934ea42d9af6027a3164e1196a78140bdd05d347) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useEdges`, `useInternalNode`, `useNodes` and `useNodeId` hooks
- [#5163](https://github.com/xyflow/xyflow/pull/5163) [`ab800054`](https://github.com/xyflow/xyflow/commit/ab800054a50c68f9c27dfad2b0d3833b782f4797) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useNodesState` and `useEdgesState` hook
- [#5160](https://github.com/xyflow/xyflow/pull/5160) [`b357f43d`](https://github.com/xyflow/xyflow/commit/b357f43dfa262205059ac9714198196b4aaf8870) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseHandleConnectionsParams` and `useHandleConnections` hook
- [#5162](https://github.com/xyflow/xyflow/pull/5162) [`29f4aeb2`](https://github.com/xyflow/xyflow/commit/29f4aeb260df0a5d83e775c7c2ed788f997006a7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseNodeConnectionsParams` and `useNodeConnections` hook
- [#5164](https://github.com/xyflow/xyflow/pull/5164) [`09021550`](https://github.com/xyflow/xyflow/commit/09021550dc72ac240fcbb6adb3cf530d91575f79) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseOnViewportChangeOptions` and `useOnViewportChange` hook
- [#5166](https://github.com/xyflow/xyflow/pull/5166) [`701ad17e`](https://github.com/xyflow/xyflow/commit/701ad17ed3f523389e7d0bf9a006bef249645c3d) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `useStore` hook
- [#5170](https://github.com/xyflow/xyflow/pull/5170) [`eb2a33c6`](https://github.com/xyflow/xyflow/commit/eb2a33c6dfb0629e851ab3a0f2cd70eca92efc42) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `interface GetSimpleBezierPathParams` and `getSimpleBezierPath`
- [#5161](https://github.com/xyflow/xyflow/pull/5161) [`c4efe749`](https://github.com/xyflow/xyflow/commit/c4efe749208e854dc9ced5bdd00933269d5b4382) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type UseKeyPressOptions` and `useKeyPress` hook
- Updated dependencies [[`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e), [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd), [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03), [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a), [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b)]:
- @xyflow/system@0.0.55
## 12.5.4
### Patch Changes
- [#5134](https://github.com/xyflow/xyflow/pull/5134) [`7acab1e1`](https://github.com/xyflow/xyflow/commit/7acab1e123944c296180fbf826a3fd488963608f) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Adjust `@default` TSDoc tags for `BackgroundProps`
- [#5135](https://github.com/xyflow/xyflow/pull/5135) [`754a1671`](https://github.com/xyflow/xyflow/commit/754a167134f22fa00a139de4c7e10aaaa1953ac8) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `NodeResizerProps` and `ResizeControlProps`
- [#5140](https://github.com/xyflow/xyflow/pull/5140) [`82e6860e`](https://github.com/xyflow/xyflow/commit/82e6860e1354b8bb8047399b7773fd090be206d7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `MiniMapProps` and `PanelProps`
- [#5147](https://github.com/xyflow/xyflow/pull/5147) [`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a) Thanks [@moklick](https://github.com/moklick)! - Pass dimensions to final resize change event
- [#5143](https://github.com/xyflow/xyflow/pull/5143) [`b1e1cc11`](https://github.com/xyflow/xyflow/commit/b1e1cc1125d106cf1521a1524286404483e38f30) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ReactFlowInstance` and `GeneralHelpers`
- [#5138](https://github.com/xyflow/xyflow/pull/5138) [`d4eb8d52`](https://github.com/xyflow/xyflow/commit/d4eb8d52d0e26e9534ec5fc347211ce91d1ddd32) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `ViewportHelperFunctions` and `NodeToolbarProps`
- [#5144](https://github.com/xyflow/xyflow/pull/5144) [`5a1ce56e`](https://github.com/xyflow/xyflow/commit/5a1ce56e8ce83f01e01ef531d90c52181c3e3a1a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Compare `nodeStrokeWidth` with `number`, not with `string` within `MiniMap`
- [#5141](https://github.com/xyflow/xyflow/pull/5141) [`06cf4c10`](https://github.com/xyflow/xyflow/commit/06cf4c10f5d8a43f57ee0fde19d9a3fe1044cf48) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `Edge`, `BaseEdgeProps` and `ConnectionLineComponentProps`
- Updated dependencies [[`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a), [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be), [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe), [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654), [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761), [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e)]:
- @xyflow/system@0.0.54
## 12.5.3
### Patch Changes
- [#5132](https://github.com/xyflow/xyflow/pull/5132) [`75ab8942`](https://github.com/xyflow/xyflow/commit/75ab89420e3cd0fdc34baf06eabdc50113d4de7c) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working when onNodesChange is not defined.
## 12.5.2
### Patch Changes
- [#5124](https://github.com/xyflow/xyflow/pull/5124) [`b76f7f9e`](https://github.com/xyflow/xyflow/commit/b76f7f9eb4841f139b1468b8eda0430ddd19a1ae) Thanks [@bjornosal](https://github.com/bjornosal)! - Export NodeConnection type
- [#5127](https://github.com/xyflow/xyflow/pull/5127) [`3079c2c9`](https://github.com/xyflow/xyflow/commit/3079c2c911426f54e8d295083ddbe97ed3aad201) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix `fitView` not working when returning early in `onNodesChange`.
## 12.5.1
### Patch Changes
- [#5120](https://github.com/xyflow/xyflow/pull/5120) [`6dfea686`](https://github.com/xyflow/xyflow/commit/6dfea6863a3cbd91f932bf54a6dba549bd248bd5) Thanks [@moklick](https://github.com/moklick)! - Handle fitView for uncontrolled flows
## 12.5.0
### Minor Changes
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`acba901d`](https://github.com/xyflow/xyflow/commit/acba901d861aa84cb5beba60b24fff4cfde7ada6) Thanks [@peterkogo](https://github.com/peterkogo)! - You can now express paddings in fitViewOptions as pixels ('30px'), as viewport percentages ('20%') and define different paddings for each side.
### Patch Changes
- [#5109](https://github.com/xyflow/xyflow/pull/5109) [`0cdda42c`](https://github.com/xyflow/xyflow/commit/0cdda42cdd1cd43d43d43c44e54b7b9f7a716ca9) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `BackgroundProps`
- [#5059](https://github.com/xyflow/xyflow/pull/5059) [`065ff89d`](https://github.com/xyflow/xyflow/commit/065ff89d10488f9c76c56870511e45eaed299778) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Prevent onPaneClick when connection is in progress. Closes [#5057](https://github.com/xyflow/xyflow/issues/5057)
- [#5110](https://github.com/xyflow/xyflow/pull/5110) [`7eb6eb07`](https://github.com/xyflow/xyflow/commit/7eb6eb0709e451d7628bfdbc3ced89b3bb57b626) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `EdgeLabelOptions` and `BaseEdgeProps`
- [#5113](https://github.com/xyflow/xyflow/pull/5113) [`bce8542d`](https://github.com/xyflow/xyflow/commit/bce8542df19c33f3cd9225f483435e8a7aa4ed94) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `ControlProps`
- [#5116](https://github.com/xyflow/xyflow/pull/5116) [`58942154`](https://github.com/xyflow/xyflow/commit/589421542386906ec49d8469cac551b8f7ea1c47) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `NodeToolbarProps`
- [#5114](https://github.com/xyflow/xyflow/pull/5114) [`ba2bfbb4`](https://github.com/xyflow/xyflow/commit/ba2bfbb49aafac979f94b0136bb408faea12d5c6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - feat: export `EdgeLabelRendererProps`
- [#5107](https://github.com/xyflow/xyflow/pull/5107) [`c5a8c237`](https://github.com/xyflow/xyflow/commit/c5a8c23773e5985be6b37abacdac743911be8c09) Thanks [@moklick](https://github.com/moklick)! - Add TSDoc annotations for exported edges
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5115](https://github.com/xyflow/xyflow/pull/5115) [`c2154557`](https://github.com/xyflow/xyflow/commit/c215455735385ef5e12e4130164b9d01f9c18aa2) Thanks [@dimaMachina](https://github.com/dimaMachina)! - fix: improve TSDoc comments for `EdgeLabelOptions` and `EdgeTextProps`
- [#5093](https://github.com/xyflow/xyflow/pull/5093) [`65825e89`](https://github.com/xyflow/xyflow/commit/65825e89a6e2e7591087eb41ac89da4da7095f8f) Thanks [@moklick](https://github.com/moklick)! - Hidden nodes are not displayed in the mini map anymore
- [#5090](https://github.com/xyflow/xyflow/pull/5090) [`8da1748a`](https://github.com/xyflow/xyflow/commit/8da1748a6ad5cdde9f03737ff786bd29c9c968de) Thanks [@moklick](https://github.com/moklick)! - Release key even when an inout field is focused
- Updated dependencies [[`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293), [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe), [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1)]:
- @xyflow/system@0.0.53
## 12.4.4
### Patch Changes
- [#5052](https://github.com/xyflow/xyflow/pull/5052) [`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90) Thanks [@moklick](https://github.com/moklick)! - Show an error if user drags uninitialized node
- [#5042](https://github.com/xyflow/xyflow/pull/5042) [`2fe0e850`](https://github.com/xyflow/xyflow/commit/2fe0e850a8c415c6a3113796a2c5c80e7cad2376) Thanks [@moklick](https://github.com/moklick)! - Allow click connections when target sets `isConnectableStart`
- [#5047](https://github.com/xyflow/xyflow/pull/5047) [`b3bf5693`](https://github.com/xyflow/xyflow/commit/b3bf5693c659069cea90bf1cb215ae65d06c5509) Thanks [@moklick](https://github.com/moklick)! - Pass generics to OnSelectionChangeFunc so that users can type it correctly
- [#5053](https://github.com/xyflow/xyflow/pull/5053) [`25fb45b5`](https://github.com/xyflow/xyflow/commit/25fb45b5e9d6da391b9aff652b8e6e34eaf757fc) Thanks [@moklick](https://github.com/moklick)! - Remove incorrect deprecation warning
- [#5033](https://github.com/xyflow/xyflow/pull/5033) [`7b4a81fb`](https://github.com/xyflow/xyflow/commit/7b4a81fb6b3d88f8ee7b4f070aef7ac3b962d5a6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - lint: use `React.JSX` type instead of the deprecated global `JSX` namespace
- [#5043](https://github.com/xyflow/xyflow/pull/5043) [`0292ad20`](https://github.com/xyflow/xyflow/commit/0292ad20109a3b2518dc686a82e100a0a6964fb8) Thanks [@moklick](https://github.com/moklick)! - Use current expandParent value on drag to be able to update it while dragging
- [#5032](https://github.com/xyflow/xyflow/pull/5032) [`5867bba8`](https://github.com/xyflow/xyflow/commit/5867bba8050d07378a45a2026557c4bce7bda239) Thanks [@dimaMachina](https://github.com/dimaMachina)! - lint: remove unnecessary type assertions
- Updated dependencies [[`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90)]:
- @xyflow/system@0.0.52
## 12.4.3
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- [#4991](https://github.com/xyflow/xyflow/pull/4991) [`ea54d9bc`](https://github.com/xyflow/xyflow/commit/ea54d9bcb197d02d248ef3e4eaabc033a43d966a) Thanks [@waynetee](https://github.com/waynetee)! - Fix viewport shifting on node focus
- [#5013](https://github.com/xyflow/xyflow/pull/5013) [`cde899c5`](https://github.com/xyflow/xyflow/commit/cde899c5be9715c4ff2cc331ea93821102604c62) Thanks [@moklick](https://github.com/moklick)! - Pass `NodeType` type argument from `ReactFlowProps` to `connectionLineComponent` property.
- [#5008](https://github.com/xyflow/xyflow/pull/5008) [`12d859fe`](https://github.com/xyflow/xyflow/commit/12d859fe297593d44cf8493a4d6bf2c664b9139c) Thanks [@moklick](https://github.com/moklick)! - Add package.json to exports
- [#5012](https://github.com/xyflow/xyflow/pull/5012) [`4d3f19e8`](https://github.com/xyflow/xyflow/commit/4d3f19e88b984ce6743970560d7367d174500f32) Thanks [@moklick](https://github.com/moklick)! - Add snapGrid option to screenToFlowPosition and set snapToGrid to false
- [#5003](https://github.com/xyflow/xyflow/pull/5003) [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - repair lint command
- [#4991](https://github.com/xyflow/xyflow/pull/4991) [`4c62f19b`](https://github.com/xyflow/xyflow/commit/4c62f19b3afac4b3db84b14e2c36f8c9e0a96116) Thanks [@waynetee](https://github.com/waynetee)! - Prevent viewport shift after using Tab
- Updated dependencies [[`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919), [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234), [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7)]:
- @xyflow/system@0.0.51
## 12.4.2
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/react",
"version": "12.4.2",
"version": "12.5.5",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [
"react",
@@ -28,6 +28,7 @@
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"node": {
"types": "./dist/esm/index.d.ts",
@@ -84,7 +85,7 @@
"postcss-nested": "^6.0.0",
"postcss-rename": "^0.6.1",
"react": "^18.2.0",
"typescript": "5.1.3"
"typescript": "5.4.5"
},
"rollup": {
"globals": {

View File

@@ -90,4 +90,57 @@ function BackgroundComponent({
BackgroundComponent.displayName = 'Background';
/**
* The `<Background />` component makes it convenient to render different types of backgrounds common in node-based UIs. It comes with three variants: lines, dots and cross.
*
* @example
*
* A simple example of how to use the Background component.
*
* ```tsx
* import { useState } from 'react';
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background color="#ccc" variant={BackgroundVariant.Dots} />
* </ReactFlow>
* );
* }
* ```
*
* @example
*
* In this example you can see how to combine multiple backgrounds
*
* ```tsx
* import { ReactFlow, Background, BackgroundVariant } from '@xyflow/react';
* import '@xyflow/react/dist/style.css';
*
* export default function Flow() {
* return (
* <ReactFlow defaultNodes={[...]} defaultEdges={[...]}>
* <Background
* id="1"
* gap={10}
* color="#f1f1f1"
* variant={BackgroundVariant.Lines}
* />
* <Background
* id="2"
* gap={100}
* color="#ccc"
* variant={BackgroundVariant.Lines}
* />
* </ReactFlow>
* );
* }
* ```
*
* @remarks
*
* When combining multiple <Background /> components its important to give each of them a unique id prop!
*
*/
export const Background = memo(BackgroundComponent);

View File

@@ -1,34 +1,60 @@
import { CSSProperties } from 'react';
/**
* The three variants are exported as an enum for convenience. You can either import
* the enum and use it like `BackgroundVariant.Lines` or you can use the raw string
* value directly.
* @public
*/
export enum BackgroundVariant {
Lines = 'lines',
Dots = 'dots',
Cross = 'cross',
}
/**
* @expand
*/
export type BackgroundProps = {
/** When multiple backgrounds are present on the page, each one should have a unique id. */
id?: string;
/** Color of the pattern */
/** Color of the pattern. */
color?: string;
/** Color of the background */
/** Color of the background. */
bgColor?: string;
/** Class applied to the container */
/** Class applied to the container. */
className?: string;
/** Class applied to the pattern */
/** Class applied to the pattern. */
patternClassName?: string;
/** Gap between repetitions of the pattern */
/**
* The gap between patterns. Passing in a tuple allows you to control the x and y gap
* independently.
* @default 20
*/
gap?: number | [number, number];
/** Size of a single pattern element */
/**
* The radius of each dot or the size of each rectangle if `BackgroundVariant.Dots` or
* `BackgroundVariant.Cross` is used. This defaults to 1 or 6 respectively, or ignored if
* `BackgroundVariant.Lines` is used.
*/
size?: number;
/** Offset of the pattern */
/**
* Offset of the pattern.
* @default 0
*/
offset?: number | [number, number];
/** Line width of the Line pattern */
/**
* The stroke thickness used when drawing the pattern.
* @default 1
*/
lineWidth?: number;
/** Variant of the pattern
/**
* Variant of the pattern.
* @default BackgroundVariant.Dots
* @example BackgroundVariant.Lines, BackgroundVariant.Dots, BackgroundVariant.Cross
* 'lines', 'dots', 'cross'
*/
variant?: BackgroundVariant;
/** Style applied to the container */
/** Style applied to the container. */
style?: CSSProperties;
};

View File

@@ -2,6 +2,29 @@ import cc from 'classcat';
import type { ControlButtonProps } from './types';
/**
* You can add buttons to the control panel by using the `<ControlButton />` component
* and pass it as a child to the [`<Controls />`](/api-reference/components/controls) component.
*
* @public
* @example
*```jsx
*import { MagicWand } from '@radix-ui/react-icons'
*import { ReactFlow, Controls, ControlButton } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls>
* <ControlButton onClick={() => alert('Something magical just happened. ✨')}>
* <MagicWand />
* </ControlButton>
* </Controls>
* </ReactFlow>
* )
*}
*```
*/
export function ControlButton({ children, className, ...rest }: ControlButtonProps) {
return (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>

View File

@@ -125,4 +125,25 @@ function ControlsComponent({
ControlsComponent.displayName = 'Controls';
/**
* The `<Controls />` component renders a small panel that contains convenient
* buttons to zoom in, zoom out, fit the view, and lock the viewport.
*
* @public
* @example
*```tsx
*import { ReactFlow, Controls } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]} edges={[...]}>
* <Controls />
* </ReactFlow>
* )
*}
*```
*
* @remarks To extend or customise the controls, you can use the [`<ControlButton />`](/api-reference/components/control-button) component
*
*/
export const Controls = memo(ControlsComponent);

View File

@@ -3,24 +3,46 @@ import type { PanelPosition } from '@xyflow/system';
import type { FitViewOptions } from '../../types';
/**
* @expand
*/
export type ControlProps = {
/** Show button for zoom in/out */
/**
* Whether or not to show the zoom in and zoom out buttons. These buttons will adjust the viewport
* zoom by a fixed amount each press.
* @default true
*/
showZoom?: boolean;
/** Show button for fit view */
/**
* Whether or not to show the fit view button. By default, this button will adjust the viewport so
* that all nodes are visible at once.
* @default true
*/
showFitView?: boolean;
/** Show button for toggling interactivity */
/**
* Show button for toggling interactivity
* @default true
*/
showInteractive?: boolean;
/** Options being used when fit view button is clicked */
/**
* Customise the options for the fit view button. These are the same options you would pass to the
* fitView function.
*/
fitViewOptions?: FitViewOptions;
/** Callback when zoom in button is clicked */
/** Called in addition the default zoom behavior when the zoom in button is clicked. */
onZoomIn?: () => void;
/** Callback when zoom out button is clicked */
/** Called in addition the default zoom behavior when the zoom out button is clicked. */
onZoomOut?: () => void;
/** Callback when fit view button is clicked */
/**
* Called when the fit view button is clicked. When this is not provided, the viewport will be
* adjusted so that all nodes are visible.
*/
onFitView?: () => void;
/** Callback when interactivity is toggled */
/** Called when the interactive (lock) button is clicked. */
onInteractiveChange?: (interactiveStatus: boolean) => void;
/** Position of the controls on the pane
/**
* Position of the controls on the pane
* @default PanelPosition.BottomLeft
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
@@ -28,10 +50,19 @@ export type ControlProps = {
children?: ReactNode;
/** Style applied to container */
style?: React.CSSProperties;
/** ClassName applied to container */
/** Class name applied to container */
className?: string;
/**
* @default 'React Flow controls'
*/
'aria-label'?: string;
/**
* @default 'vertical'
*/
orientation?: 'horizontal' | 'vertical';
};
/**
* @expand
*/
export type ControlButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;

View File

@@ -15,6 +15,8 @@ import type { MiniMapProps } from './types';
const defaultWidth = 200;
const defaultHeight = 150;
const filterHidden = (node: Node) => !node.hidden;
const selector = (s: ReactFlowState) => {
const viewBB: Rect = {
x: -s.transform[0] / s.transform[2],
@@ -25,7 +27,10 @@ const selector = (s: ReactFlowState) => {
return {
viewBB,
boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB,
boundingRect:
s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { filter: filterHidden }), viewBB)
: viewBB,
rfId: s.rfId,
panZoom: s.panZoom,
translateExtent: s.translateExtent,
@@ -44,8 +49,10 @@ function MiniMapComponent<NodeType extends Node = Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent,
bgColor,
maskColor,
@@ -118,7 +125,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
const onSvgNodeClick = onNodeClick
? useCallback((event: MouseEvent, nodeId: string) => {
const node = store.getState().nodeLookup.get(nodeId)!;
const node: NodeType = store.getState().nodeLookup.get(nodeId)!.internals.userNode;
onNodeClick(event, node);
}, [])
: undefined;
@@ -136,7 +143,7 @@ function MiniMapComponent<NodeType extends Node = Node>({
typeof maskStrokeWidth === 'number' ? maskStrokeWidth * viewScale : undefined,
'--xy-minimap-node-background-color-props': typeof nodeColor === 'string' ? nodeColor : undefined,
'--xy-minimap-node-stroke-color-props': typeof nodeStrokeColor === 'string' ? nodeStrokeColor : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'string' ? nodeStrokeWidth : undefined,
'--xy-minimap-node-stroke-width-props': typeof nodeStrokeWidth === 'number' ? nodeStrokeWidth : undefined,
} as CSSProperties
}
className={cc(['react-flow__minimap', className])}
@@ -176,4 +183,24 @@ function MiniMapComponent<NodeType extends Node = Node>({
MiniMapComponent.displayName = 'MiniMap';
/**
* The `<MiniMap />` component can be used to render an overview of your flow. It
* renders each node as an SVG element and visualizes where the current viewport is
* in relation to the rest of the flow.
*
* @public
* @example
*
* ```jsx
*import { ReactFlow, MiniMap } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[...]]} edges={[...]]}>
* <MiniMap nodeStrokeWidth={3} />
* </ReactFlow>
* );
*}
*```
*/
export const MiniMap = memo(MiniMapComponent) as typeof MiniMapComponent;

View File

@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import { MiniMapNode } from './MiniMapNode';
import type { ReactFlowState, Node, InternalNode } from '../../types';
import type { ReactFlowState, Node } from '../../types';
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any;
@@ -21,8 +21,10 @@ function MiniMapNodes<NodeType extends Node>({
nodeClassName = '',
nodeBorderRadius = 5,
nodeStrokeWidth,
// We need to rename the prop to be `CapitalCase` so that JSX will render it as
// a component properly.
/*
* We need to rename the prop to be `CapitalCase` so that JSX will render it as
* a component properly.
*/
nodeComponent: NodeComponent = MiniMapNode,
onClick,
}: MiniMapNodesProps<NodeType>) {
@@ -36,11 +38,13 @@ function MiniMapNodes<NodeType extends Node>({
return (
<>
{nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
/*
* The split of responsibilities between MiniMapNodes and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For more details, see a similar commit in `NodeRenderer/index.tsx`.
*/
<NodeComponentWrapper<NodeType>
key={nodeId}
id={nodeId}
@@ -80,8 +84,9 @@ function NodeComponentWrapperInner<NodeType extends Node>({
shapeRendering: string;
}) {
const { node, x, y, width, height } = useStore((s) => {
const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
const { x, y } = node.internals.positionAbsolute;
const { internals } = s.nodeLookup.get(id)!;
const node = internals.userNode as NodeType;
const { x, y } = internals.positionAbsolute;
const { width, height } = getNodeDimensions(node);
return {

View File

@@ -6,47 +6,97 @@ import type { Node } from '../../types';
export type GetMiniMapNodeAttribute<NodeType extends Node = Node> = (node: NodeType) => string;
/**
* @expand
*/
export type MiniMapProps<NodeType extends Node = Node> = Omit<HTMLAttributes<SVGSVGElement>, 'onClick'> & {
/** Color of nodes on minimap */
/**
* Color of nodes on minimap.
* @default "#e2e2e2"
*/
nodeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** Stroke color of nodes on minimap */
/**
* Stroke color of nodes on minimap.
* @default "transparent"
*/
nodeStrokeColor?: string | GetMiniMapNodeAttribute<NodeType>;
/** ClassName applied to nodes on minimap */
/**
* Class name applied to nodes on minimap.
* @default ""
*/
nodeClassName?: string | GetMiniMapNodeAttribute<NodeType>;
/** Border radius of nodes on minimap */
/**
* Border radius of nodes on minimap.
* @default 5
*/
nodeBorderRadius?: number;
/** Stroke width of nodes on minimap */
/**
* Stroke width of nodes on minimap.
* @default 2
*/
nodeStrokeWidth?: number;
/** Component used to render nodes on minimap */
/**
* A custom component to render the nodes in the minimap. This component must render an SVG
* element!
*/
nodeComponent?: ComponentType<MiniMapNodeProps>;
/** Background color of minimap */
/** Background color of minimap. */
bgColor?: string;
/** Color of mask representing viewport */
/**
* The color of the mask that covers the portion of the minimap not currently visible in the
* viewport.
* @default "rgba(240, 240, 240, 0.6)"
*/
maskColor?: string;
/** Stroke color of mask representing viewport */
/**
* Stroke color of mask representing viewport.
* @default transparent
*/
maskStrokeColor?: string;
/** Stroke width of mask representing viewport */
/**
* Stroke width of mask representing viewport.
* @default 1
*/
maskStrokeWidth?: number;
/** Position of minimap on pane
/**
* Position of minimap on pane.
* @default PanelPosition.BottomRight
* @example PanelPosition.TopLeft, PanelPosition.TopRight,
* PanelPosition.BottomLeft, PanelPosition.BottomRight
*/
position?: PanelPosition;
/** Callback caled when minimap is clicked*/
/** Callback called when minimap is clicked. */
onClick?: (event: MouseEvent, position: XYPosition) => void;
/** Callback called when node on minimap is clicked */
/** Callback called when node on minimap is clicked. */
onNodeClick?: (event: MouseEvent, node: NodeType) => void;
/** If true, viewport is pannable via mini map component */
/**
* Determines whether you can pan the viewport by dragging inside the minimap.
* @default false
*/
pannable?: boolean;
/** If true, viewport is zoomable via mini map component */
/**
* Determines whether you can zoom the viewport by scrolling inside the minimap.
* @default false
*/
zoomable?: boolean;
/** The aria-label attribute */
/**
* There is no text inside the minimap for a screen reader to use as an accessible name, so it's
* important we provide one to make the minimap accessible. The default is sufficient, but you may
* want to replace it with something more relevant to your app or product.
* @default "React Flow mini map"
*/
ariaLabel?: string | null;
/** Invert direction when panning the minimap viewport */
/** Invert direction when panning the minimap viewport. */
inversePan?: boolean;
/** Step size for zooming in/out on minimap */
/**
* Step size for zooming in/out on minimap.
* @default 10
*/
zoomStep?: number;
/** Offset the viewport on the minmap, acts like a padding */
/**
* Offset the viewport on the minimap, acts like a padding.
* @default 5
*/
offsetScale?: number;
};
@@ -57,6 +107,12 @@ export type MiniMapNodes<NodeType extends Node = Node> = Pick<
onClick?: (event: MouseEvent, nodeId: string) => void;
};
/**
* The props that are passed to the MiniMapNode component
*
* @public
* @expand
*/
export type MiniMapNodeProps = {
id: string;
x: number;

View File

@@ -74,8 +74,8 @@ function ResizeControl({
if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width!;
const height = change.height ?? node.measured.height!;
const width = change.width ?? node.measured.width ?? 0;
const height = change.height ?? node.measured.height ?? 0;
const child: ParentExpandChild = {
id: node.id,
@@ -99,8 +99,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at
// 0,0 when node origin is 0,0 and to width, height if it's 1,1
/*
* when the parent was expanded by the child node, its position will be clamped at
* 0,0 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
}
@@ -140,11 +142,15 @@ function ResizeControl({
triggerNodeChanges(changes);
},
onEnd: () => {
onEnd: ({ width, height }) => {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
dimensions: {
width,
height,
},
};
store.getState().triggerNodeChanges([dimensionChange]);
},
@@ -201,4 +207,9 @@ export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
}
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
export const NodeResizeControl = memo(ResizeControl);

View File

@@ -3,6 +3,30 @@ import { ResizeControlVariant, XY_RESIZER_HANDLE_POSITIONS, XY_RESIZER_LINE_POSI
import { NodeResizeControl } from './NodeResizeControl';
import type { NodeResizerProps } from './types';
/**
* The `<NodeResizer />` component can be used to add a resize functionality to your
* nodes. It renders draggable controls around the node to resize in all directions.
* @public
*
* @example
*```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeResizer } from '@xyflow/react';
*
*function ResizableNode({ data }) {
* return (
* <>
* <NodeResizer minWidth={100} minHeight={30} />
* <Handle type="target" position={Position.Left} />
* <div style={{ padding: 10 }}>{data.label}</div>
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(ResizableNode);
*```
*/
export function NodeResizer({
nodeId,
isVisible = true,

View File

@@ -9,43 +9,68 @@ import type {
OnResizeEnd,
} from '@xyflow/system';
/**
* @expand
*/
export type NodeResizerProps = {
/** Id of the node it is resizing
/**
* Id of the node it is resizing.
* @remarks optional if used inside custom node
*/
nodeId?: string;
/** Color of the resize handle */
/** Color of the resize handle. */
color?: string;
/** ClassName applied to handle */
/** Class name applied to handle. */
handleClassName?: string;
/** Style applied to handle */
/** Style applied to handle. */
handleStyle?: CSSProperties;
/** ClassName applied to line */
/** Class name applied to line. */
lineClassName?: string;
/** Style applied to line */
/** Style applied to line. */
lineStyle?: CSSProperties;
/** Are the controls visible */
/**
* Are the controls visible.
* @default true
*/
isVisible?: boolean;
/** Minimum width of node */
/**
* Minimum width of node.
* @default 10
*/
minWidth?: number;
/** Minimum height of node */
/**
* Minimum height of node.
* @default 10
*/
minHeight?: number;
/** Maximum width of node */
/**
* Maximum width of node.
* @default Number.MAX_VALUE
*/
maxWidth?: number;
/** Maximum height of node */
/**
* Maximum height of node.
* @default Number.MAX_VALUE
*/
maxHeight?: number;
/** Keep aspect ratio when resizing */
/**
* Keep aspect ratio when resizing.
* @default false
*/
keepAspectRatio?: boolean;
/** Callback to determine if node should resize */
/** Callback to determine if node should resize. */
shouldResize?: ShouldResize;
/** Callback called when resizing starts */
/** Callback called when resizing starts. */
onResizeStart?: OnResizeStart;
/** Callback called when resizing */
/** Callback called when resizing. */
onResize?: OnResize;
/** Callback called when resizing ends */
/** Callback called when resizing ends. */
onResizeEnd?: OnResizeEnd;
};
/**
* @expand
*/
export type ResizeControlProps = Pick<
NodeResizerProps,
| 'nodeId'
@@ -60,12 +85,15 @@ export type ResizeControlProps = Pick<
| 'onResize'
| 'onResizeEnd'
> & {
/** Position of the control
/**
* Position of the control.
* @example ControlPosition.TopLeft, ControlPosition.TopRight,
* ControlPosition.BottomLeft, ControlPosition.BottomRight
*/
position?: ControlPosition;
/** Variant of the control
/**
* Variant of the control.
* @default "handle"
* @example ResizeControlVariant.Handle, ResizeControlVariant.Line
*/
variant?: ResizeControlVariant;
@@ -74,6 +102,9 @@ export type ResizeControlProps = Pick<
children?: ReactNode;
};
/**
* @expand
*/
export type ResizeControlLineProps = ResizeControlProps & {
position?: ControlLinePosition;
};

View File

@@ -38,6 +38,41 @@ const storeSelector = (state: ReactFlowState) => ({
selectedNodesCount: state.nodes.filter((node) => node.selected).length,
});
/**
* This component can render a toolbar or tooltip to one side of a custom node. This
* toolbar doesn't scale with the viewport so that the content is always visible.
*
* @public
* @example
* ```jsx
*import { memo } from 'react';
*import { Handle, Position, NodeToolbar } from '@xyflow/react';
*
*function CustomNode({ data }) {
* return (
* <>
* <NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition}>
* <button>delete</button>
* <button>copy</button>
* <button>expand</button>
* </NodeToolbar>
*
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*
*export default memo(CustomNode);
*```
* @remarks By default, the toolbar is only visible when a node is selected. If multiple
* nodes are selected it will not be visible to prevent overlapping toolbars or
* clutter. You can override this behavior by setting the `isVisible` prop to `true`.
*/
export function NodeToolbar({
nodeId,
children,
@@ -74,7 +109,7 @@ export function NodeToolbar({
const isActive =
typeof isVisible === 'boolean'
? isVisible
: nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1;
: nodes.size === 1 && nodes.values().next().value?.selected && selectedNodesCount === 1;
if (!isActive || !nodes.size) {
return null;

View File

@@ -1,19 +1,31 @@
import type { HTMLAttributes } from 'react';
import type { Position, Align } from '@xyflow/system';
/**
* @expand
*/
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
/** Id of the node, or array of ids the toolbar should be displayed at */
/**
* By passing in an array of node id's you can render a single tooltip for a group or collection
* of nodes.
*/
nodeId?: string | string[];
/** If true, node toolbar is visible even if node is not selected */
/** If `true`, node toolbar is visible even if node is not selected. */
isVisible?: boolean;
/** Position of the toolbar relative to the node
* @example Position.TopLeft, Position.TopRight,
* Position.BottomLeft, Position.BottomRight
/**
* Position of the toolbar relative to the node.
* @default Position.Top
* @example Position.TopLeft, Position.TopRight, Position.BottomLeft, Position.BottomRight
*/
position?: Position;
/** Offset the toolbar from the node */
/**
* The space between the node and the toolbar, measured in pixels.
* @default 10
*/
offset?: number;
/** Align the toolbar relative to the node
/**
* Align the toolbar relative to the node.
* @default "center"
* @example Align.Start, Align.Center, Align.End
*/
align?: Align;

View File

@@ -28,33 +28,49 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const store = useStoreApi<NodeType, EdgeType>();
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup } = store.getState();
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState();
// This is essentially an `Array.reduce` in imperative clothing. Processing
// this queue is a relatively hot path so we'd like to avoid the overhead of
// array methods where we can.
let next = nodes as NodeType[];
/*
* This is essentially an `Array.reduce` in imperative clothing. Processing
* this queue is a relatively hot path so we'd like to avoid the overhead of
* array methods where we can.
*/
let next = nodes;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}
if (hasDefaultNodes) {
setNodes(next);
} else if (onNodesChange) {
onNodesChange(
getElementsDiffChanges({
items: next,
lookup: nodeLookup,
}) as NodeChange<NodeType>[]
);
} else {
// When a controlled flow is used we need to collect the changes
const changes = getElementsDiffChanges({
items: next,
lookup: nodeLookup,
}) as NodeChange<NodeType>[];
// We only want to fire onNodesChange if there are changes to the nodes
if (changes.length > 0) {
onNodesChange?.(changes);
} else if (fitViewQueued) {
// If there are no changes to the nodes, we still need to call setNodes
// to trigger a re-render and fitView.
window.requestAnimationFrame(() => {
const { fitViewQueued, nodes, setNodes } = store.getState();
if (fitViewQueued) {
setNodes(nodes);
}
});
}
}
}, []);
const nodeQueue = useQueue<NodeType>(nodeQueueHandler);
const edgeQueueHandler = useCallback((queueItems: QueueItem<EdgeType>[]) => {
const { edges = [], setEdges, hasDefaultEdges, onEdgesChange, edgeLookup } = store.getState();
let next = edges as EdgeType[];
let next = edges;
for (const payload of queueItems) {
next = typeof payload === 'function' ? payload(next) : payload;
}

View File

@@ -12,21 +12,27 @@ import { Queue, QueueItem } from './types';
* @returns a Queue object
*/
export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
// Because we're using a ref above, we need some way to let React know when to
// actually process the queue. We increment this number any time we mutate the
// queue, creating a new state to trigger the layout effect below.
// Using a boolean dirty flag here instead would lead to issues related to
// automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
/*
* Because we're using a ref above, we need some way to let React know when to
* actually process the queue. We increment this number any time we mutate the
* queue, creating a new state to trigger the layout effect below.
* Using a boolean dirty flag here instead would lead to issues related to
* automatic batching. (https://github.com/xyflow/xyflow/issues/4779)
*/
const [serial, setSerial] = useState(BigInt(0));
// A reference of all the batched updates to process before the next render. We
// want a reference here so multiple synchronous calls to `setNodes` etc can be
// batched together.
/*
* A reference of all the batched updates to process before the next render. We
* want a reference here so multiple synchronous calls to `setNodes` etc can be
* batched together.
*/
const [queue] = useState(() => createQueue<T>(() => setSerial(n => n + BigInt(1))));
// Layout effects are guaranteed to run before the next render which means we
// shouldn't run into any issues with stale state or weird issues that come from
// rendering things one frame later than expected (we used to use `setTimeout`).
/*
* Layout effects are guaranteed to run before the next render which means we
* shouldn't run into any issues with stale state or weird issues that come from
* rendering things one frame later than expected (we used to use `setTimeout`).
*/
useIsomorphicLayoutEffect(() => {
const queueItems = queue.get();

View File

@@ -11,12 +11,12 @@ import {
import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
import type { ConnectionLineComponent, Node, ReactFlowState } from '../../types';
import { useConnection } from '../../hooks/useConnection';
type ConnectionLineWrapperProps = {
type ConnectionLineWrapperProps<NodeType extends Node = Node> = {
type: ConnectionLineType;
component?: ConnectionLineComponent;
component?: ConnectionLineComponent<NodeType>;
containerStyle?: CSSProperties;
style?: CSSProperties;
};
@@ -29,7 +29,12 @@ const selector = (s: ReactFlowState) => ({
height: s.height,
});
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
export function ConnectionLineWrapper<NodeType extends Node = Node>({
containerStyle,
style,
type,
component,
}: ConnectionLineWrapperProps<NodeType>) {
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress);
@@ -45,21 +50,27 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container"
>
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
<ConnectionLine<NodeType> style={style} type={type} CustomComponent={component} isValid={isValid} />
</g>
</svg>
);
}
type ConnectionLineProps = {
type ConnectionLineProps<NodeType extends Node = Node> = {
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
CustomComponent?: ConnectionLineComponent<NodeType>;
isValid: boolean | null;
};
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection();
const ConnectionLine = <NodeType extends Node = Node>({
style,
type = ConnectionLineType.Bezier,
CustomComponent,
isValid,
}: ConnectionLineProps<NodeType>) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } =
useConnection<NodeType>();
if (!inProgress) {
return;

View File

@@ -6,7 +6,52 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__edgelabel-renderer');
export function EdgeLabelRenderer({ children }: { children: ReactNode }) {
export type EdgeLabelRendererProps = {
children: ReactNode
}
/**
* Edges are SVG-based. If you want to render more complex labels you can use the
* `<EdgeLabelRenderer />` component to access a div based renderer. This component
* is a portal that renders the label in a `<div />` that is positioned on top of
* the edges. You can see an example usage of the component in the
* [edge label renderer example](/examples/edges/edge-label-renderer).
* @public
*
* @example
* ```jsx
* import React from 'react';
* import { getBezierPath, EdgeLabelRenderer, BaseEdge } from '@xyflow/react';
*
* export function CustomEdge({ id, data, ...props }) {
* const [edgePath, labelX, labelY] = getBezierPath(props);
*
* return (
* <>
* <BaseEdge id={id} path={edgePath} />
* <EdgeLabelRenderer>
* <div
* style={{
* position: 'absolute',
* transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
* background: '#ffcc00',
* padding: 10,
* }}
* className="nodrag nopan"
* >
* {data.label}
* </div>
* </EdgeLabelRenderer>
* </>
* );
* };
* ```
*
* @remarks The `<EdgeLabelRenderer />` has no pointer events by default. If you want to
* add mouse interactions you need to set the style `pointerEvents: all` and add
* the `nopan` class on the label or the element you want to interact with.
*/
export function EdgeLabelRenderer({ children }: EdgeLabelRendererProps) {
const edgeLabelRenderer = useStore(selector);
if (!edgeLabelRenderer) {

View File

@@ -1,4 +1,4 @@
import { useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import { useState, useMemo, useRef, type KeyboardEvent, useCallback, JSX } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
@@ -136,28 +136,28 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
const onEdgeDoubleClick = onDoubleClick
? (event: React.MouseEvent) => {
onDoubleClick(event, { ...edge });
}
onDoubleClick(event, { ...edge });
}
: undefined;
const onEdgeContextMenu = onContextMenu
? (event: React.MouseEvent) => {
onContextMenu(event, { ...edge });
}
onContextMenu(event, { ...edge });
}
: undefined;
const onEdgeMouseEnter = onMouseEnter
? (event: React.MouseEvent) => {
onMouseEnter(event, { ...edge });
}
onMouseEnter(event, { ...edge });
}
: undefined;
const onEdgeMouseMove = onMouseMove
? (event: React.MouseEvent) => {
onMouseMove(event, { ...edge });
}
onMouseMove(event, { ...edge });
}
: undefined;
const onEdgeMouseLeave = onMouseLeave
? (event: React.MouseEvent) => {
onMouseLeave(event, { ...edge });
}
onMouseLeave(event, { ...edge });
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -4,6 +4,33 @@ import cc from 'classcat';
import { EdgeText } from './EdgeText';
import type { BaseEdgeProps } from '../../types';
/**
* The `<BaseEdge />` component gets used internally for all the edges. It can be
* used inside a custom edge and handles the invisible helper edge and the edge label
* for you.
*
* @public
* @example
* ```jsx
*import { BaseEdge } from '@xyflow/react';
*
*export function CustomEdge({ sourceX, sourceY, targetX, targetY, ...props }) {
* const [edgePath] = getStraightPath({
* sourceX,
* sourceY,
* targetX,
* targetY,
* });
*
* return <BaseEdge path={edgePath} {...props} />;
*}
*```
*
* @remarks If you want to use an edge marker with the [`<BaseEdge />`](/api-reference/components/base-edge) component,
* you can pass the `markerStart` or `markerEnd` props passed to your custom edge
* through to the [`<BaseEdge />`](/api-reference/components/base-edge) component.
* You can see all the props passed to a custom edge by looking at the [`EdgeProps`](/api-reference/types/edge-props) type.
*/
export function BaseEdge({
path,
labelX,

View File

@@ -61,7 +61,34 @@ function createBezierEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a bezier curve.
*
* @public
* @example
*
* ```tsx
* import { BezierEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <BezierEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const BezierEdge = createBezierEdge({ isInternal: false });
/**
* @internal
*/
const BezierEdgeInternal = createBezierEdge({ isInternal: true });
BezierEdge.displayName = 'BezierEdge';

View File

@@ -27,6 +27,9 @@ export interface EdgeAnchorProps extends SVGAttributes<SVGGElement> {
const EdgeUpdaterClassName = 'react-flow__edgeupdater';
/**
* @internal
*/
export function EdgeAnchor({
position,
centerX,

View File

@@ -8,9 +8,9 @@ function EdgeTextComponent({
x,
y,
label,
labelStyle = {},
labelStyle,
labelShowBg = true,
labelBgStyle = {},
labelBgStyle,
labelBgPadding = [2, 4],
labelBgBorderRadius = 2,
children,
@@ -34,7 +34,7 @@ function EdgeTextComponent({
}
}, [label]);
if (typeof label === 'undefined' || !label) {
if (!label) {
return null;
}
@@ -73,4 +73,30 @@ function EdgeTextComponent({
EdgeTextComponent.displayName = 'EdgeText';
/**
* You can use the `<EdgeText />` component as a helper component to display text
* within your custom edges.
*
* @public
*
* @example
* ```jsx
* import { EdgeText } from '@xyflow/react';
*
* export function CustomEdgeLabel({ label }) {
* return (
* <EdgeText
* x={100}
* y={100}
* label={label}
* labelStyle={{ fill: 'white' }}
* labelShowBg
* labelBgStyle={{ fill: 'red' }}
* labelBgPadding={[2, 4]}
* labelBgBorderRadius={2}
* />
* );
* }
*```
*/
export const EdgeText = memo(EdgeTextComponent);

View File

@@ -7,9 +7,11 @@ import type { SimpleBezierEdgeProps } from '../../types';
export interface GetSimpleBezierPathParams {
sourceX: number;
sourceY: number;
/** @default Position.Bottom */
sourcePosition?: Position;
targetX: number;
targetY: number;
/** @default Position.Top */
targetPosition?: Position;
}
@@ -29,6 +31,19 @@ function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number]
return [x1, 0.5 * (y1 + y2)];
}
/**
* The `getSimpleBezierPath` util returns everything you need to render a simple
* bezier edge between two nodes.
* @public
* @returns
* - `path`: the path to use in an SVG `<path>` element.
* - `labelX`: the `x` position you can use to render a label for this edge.
* - `labelY`: the `y` position you can use to render a label for this edge.
* - `offsetX`: the absolute difference between the source `x` position and the `x` position of the
* middle of this path.
* - `offsetY`: the absolute difference between the source `y` position and the `y` position of the
* middle of this path.
*/
export function getSimpleBezierPath({
sourceX,
sourceY,
@@ -80,8 +95,8 @@ function createSimpleBezierEdge(params: { isInternal: boolean }) {
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
sourcePosition,
targetPosition,
label,
labelStyle,
labelShowBg,

View File

@@ -62,7 +62,34 @@ function createSmoothStepEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a smooth step edge.
*
* @public
* @example
*
* ```tsx
* import { SmoothStepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <SmoothStepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const SmoothStepEdge = createSmoothStepEdge({ isInternal: false });
/**
* @internal
*/
const SmoothStepEdgeInternal = createSmoothStepEdge({ isInternal: true });
SmoothStepEdge.displayName = 'SmoothStepEdge';

View File

@@ -21,7 +21,34 @@ function createStepEdge(params: { isInternal: boolean }) {
});
}
/**
* Component that can be used inside a custom edge to render a step edge.
*
* @public
* @example
*
* ```tsx
* import { StepEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }) {
* return (
* <StepEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* sourcePosition={sourcePosition}
* targetPosition={targetPosition}
* />
* );
* }
* ```
*/
const StepEdge = createStepEdge({ isInternal: false });
/**
* @internal
*/
const StepEdgeInternal = createStepEdge({ isInternal: true });
StepEdge.displayName = 'StepEdge';

View File

@@ -50,7 +50,32 @@ function createStraightEdge(params: { isInternal: boolean }) {
);
}
/**
* Component that can be used inside a custom edge to render a straight line.
*
* @public
* @example
*
* ```tsx
* import { StraightEdge } from '@xyflow/react';
*
* function CustomEdge({ sourceX, sourceY, targetX, targetY }) {
* return (
* <StraightEdge
* sourceX={sourceX}
* sourceY={sourceY}
* targetX={targetX}
* targetY={targetY}
* />
* );
* }
* ```
*/
const StraightEdge = createStraightEdge({ isInternal: false });
/**
* @internal
*/
const StraightEdgeInternal = createStraightEdge({ isInternal: true });
StraightEdge.displayName = 'StraightEdge';

View File

@@ -1,6 +1,8 @@
// We distinguish between internal and exported edges
// The internal edges are used directly like custom edges and always get an id, source and target props
// If you import an edge from the library, the id is optional and source and target are not used at all
/*
* We distinguish between internal and exported edges
* The internal edges are used directly like custom edges and always get an id, source and target props
* If you import an edge from the library, the id is optional and source and target are not used at all
*/
export { SimpleBezierEdge, SimpleBezierEdgeInternal } from './SimpleBezierEdge';
export { SmoothStepEdge, SmoothStepEdgeInternal } from './SmoothStepEdge';

View File

@@ -28,10 +28,14 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import { type ReactFlowState } from '../../types';
import { fixedForwardRef } from '../../utils';
export interface HandleProps extends HandlePropsSystem, Omit<HTMLAttributes<HTMLDivElement>, 'id'> {
/** Callback called when connection is made */
onConnect?: OnConnect;
}
/**
* @expand
*/
export type HandleProps = HandlePropsSystem &
Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
/** Callback called when connection is made */
onConnect?: OnConnect;
};
const selector = (s: ReactFlowState) => ({
connectOnClick: s.connectOnClick,
@@ -42,9 +46,7 @@ const selector = (s: ReactFlowState) => ({
const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
const { fromHandle, toHandle, isValid } = connection;
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
return {
@@ -56,6 +58,7 @@ const connectingSelector =
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!fromHandle,
clickConnectionInProcess: !!clickHandle,
valid: connectingTo && isValid,
};
};
@@ -83,11 +86,15 @@ function HandleComponent(
const store = useStoreApi();
const nodeId = useNodeId();
const { connectOnClick, noPanClassName, rfId } = useStore(selector, shallow);
const { connectingFrom, connectingTo, clickConnecting, isPossibleEndHandle, connectionInProcess, valid } = useStore(
connectingSelector(nodeId, handleId, type),
shallow
);
const {
connectingFrom,
connectingTo,
clickConnecting,
isPossibleEndHandle,
connectionInProcess,
clickConnectionInProcess,
valid,
} = useStore(connectingSelector(nodeId, handleId, type), shallow);
if (!nodeId) {
store.getState().onError?.('010', errorMessages['error010']());
}
@@ -228,12 +235,14 @@ function HandleComponent(
connectingfrom: connectingFrom,
connectingto: connectingTo,
valid,
// shows where you can start a connection from
// and where you can end it while connecting
/*
* shows where you can start a connection from
* and where you can end it while connecting
*/
connectionindicator:
isConnectable &&
(!connectionInProcess || isPossibleEndHandle) &&
(connectionInProcess ? isConnectableEnd : isConnectableStart),
(connectionInProcess || clickConnectionInProcess ? isConnectableEnd : isConnectableStart),
},
])}
onMouseDown={onPointerDown}
@@ -248,6 +257,28 @@ function HandleComponent(
}
/**
* The Handle component is a UI element that is used to connect nodes.
* The `<Handle />` component is used in your [custom nodes](/learn/customization/custom-nodes)
* to define connection points.
*
*@public
*
*@example
*
*```jsx
*import { Handle, Position } from '@xyflow/react';
*
*export function CustomNode({ data }) {
* return (
* <>
* <div style={{ padding: '10px 20px' }}>
* {data.label}
* </div>
*
* <Handle type="target" position={Position.Left} />
* <Handle type="source" position={Position.Right} />
* </>
* );
*};
*```
*/
export const Handle = memo(fixedForwardRef(HandleComponent));

View File

@@ -37,7 +37,6 @@ export function NodeWrapper<NodeType extends Node>({
disableKeyboardA11y,
rfId,
nodeTypes,
nodeExtent,
nodeClickDistance,
onError,
}: NodeWrapperProps<NodeType>) {
@@ -109,8 +108,10 @@ export function NodeWrapper<NodeType extends Node>({
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
/*
* this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
* here we only need to call it when selectNodesOnDrag=false
*/
handleNodeClick({
id,
store,

View File

@@ -49,8 +49,10 @@ export function useNodeObserver({
useEffect(() => {
if (nodeRef.current) {
// when the user programmatically changes the source or handle position, we need to update the internals
// to make sure the edges are updated correctly
/*
* when the user programmatically changes the source or handle position, we need to update the internals
* to make sure the edges are updated correctly
*/
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;

View File

@@ -23,9 +23,9 @@ export const builtinNodeTypes: NodeTypes = {
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
node: InternalNode<NodeType>
): {
width: number | string | undefined;
height: number | string | undefined;
} {
width: number | string | undefined;
height: number | string | undefined;
} {
if (node.internals.handleBounds === undefined) {
return {
width: node.width ?? node.initialWidth ?? node.style?.width,

View File

@@ -4,10 +4,12 @@ import { errorMessages } from '@xyflow/system';
import type { ReactFlowState } from '../../types';
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
/*
* this handler is called by
* 1. the click handler when node is not draggable or selectNodesOnDrag = false
* or
* 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
*/
export function handleNodeClick({
id,
store,

View File

@@ -61,9 +61,9 @@ export function NodesSelection<NodeType extends Node>({
const onContextMenu = onSelectionContextMenu
? (event: MouseEvent) => {
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
const selectedNodes = store.getState().nodes.filter((n) => n.selected);
onSelectionContextMenu(event, selectedNodes);
}
: undefined;
const onKeyDown = (event: KeyboardEvent) => {

View File

@@ -1,20 +1,48 @@
import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';
import { HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import type { PanelPosition } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
/**
* @expand
*/
export type PanelProps = HTMLAttributes<HTMLDivElement> & {
/** Set position of the panel
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
/**
* The position of the panel.
* @default "top-left"
*/
position?: PanelPosition;
children: ReactNode;
};
const selector = (s: ReactFlowState) => (s.userSelectionActive ? 'none' : 'all');
/**
* The `<Panel />` component helps you position content above the viewport.
* It is used internally by the [`<MiniMap />`](/api-reference/components/minimap)
* and [`<Controls />`](/api-reference/components/controls) components.
*
* @public
*
* @example
* ```jsx
*import { ReactFlow, Background, Panel } from '@xyflow/react';
*
*export default function Flow() {
* return (
* <ReactFlow nodes={[]} fitView>
* <Panel position="top-left">top-left</Panel>
* <Panel position="top-center">top-center</Panel>
* <Panel position="top-right">top-right</Panel>
* <Panel position="bottom-left">bottom-left</Panel>
* <Panel position="bottom-center">bottom-center</Panel>
* <Panel position="bottom-right">bottom-right</Panel>
* </ReactFlow>
* );
*}
*```
*/
export const Panel = forwardRef<HTMLDivElement, PanelProps>(
({ position = 'top-left', children, className, style, ...rest }, ref) => {
const pointerEvents = useStore(selector);
@@ -32,3 +60,5 @@ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
);
}
);
Panel.displayName = 'Panel';

View File

@@ -19,6 +19,40 @@ export type ReactFlowProviderProps = {
children: ReactNode;
};
/**
* The `<ReactFlowProvider />` component is a [context provider](https://react.dev/learn/passing-data-deeply-with-context#)
* that makes it possible to access a flow's internal state outside of the
* [`<ReactFlow />`](/api-reference/react-flow) component. Many of the hooks we
* provide rely on this component to work.
* @public
*
* @example
* ```tsx
*import { ReactFlow, ReactFlowProvider, useNodes } from '@xyflow/react'
*
*export default function Flow() {
* return (
* <ReactFlowProvider>
* <ReactFlow nodes={...} edges={...} />
* <Sidebar />
* </ReactFlowProvider>
* );
*}
*
*function Sidebar() {
* // This hook will only work if the component it's used in is a child of a
* // <ReactFlowProvider />.
* const nodes = useNodes()
*
* return <aside>do something with nodes</aside>;
*}
*```
*
* @remarks If you're using a router and want your flow's state to persist across routes,
* it's vital that you place the `<ReactFlowProvider />` component _outside_ of
* your router. If you have multiple flows on the same page you will need to use a separate
* `<ReactFlowProvider />` for each flow.
*/
export function ReactFlowProvider({
initialNodes: nodes,
initialEdges: edges,

View File

@@ -10,8 +10,8 @@ import { shallow } from 'zustand/shallow';
import { useStore, useStoreApi } from '../../hooks/useStore';
import type { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
type SelectionListenerProps = {
onSelectionChange?: OnSelectionChangeFunc;
type SelectionListenerProps<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
};
const selector = (s: ReactFlowState) => {
@@ -44,12 +44,14 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
);
}
function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
const store = useStoreApi();
function SelectionListenerInner<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => {
const params = { nodes: selectedNodes, edges: selectedEdges };
const params = { nodes: selectedNodes as NodeType[], edges: selectedEdges as EdgeType[] };
onSelectionChange?.(params);
store.getState().onSelectionChangeHandlers.forEach((fn) => fn(params));
@@ -60,11 +62,13 @@ function SelectionListenerInner({ onSelectionChange }: SelectionListenerProps) {
const changeSelector = (s: ReactFlowState) => !!s.onSelectionChangeHandlers;
export function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
export function SelectionListener<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onSelectionChange,
}: SelectionListenerProps<NodeType, EdgeType>) {
const storeHasSelectionChangeHandlers = useStore(changeSelector);
if (onSelectionChange || storeHasSelectionChangeHandlers) {
return <SelectionListenerInner onSelectionChange={onSelectionChange} />;
return <SelectionListenerInner<NodeType, EdgeType> onSelectionChange={onSelectionChange} />;
}
return null;

View File

@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
import type { Node, Edge, ReactFlowState, ReactFlowProps, FitViewOptions } from '../../types';
import { defaultNodeOrigin } from '../../container/ReactFlow/init-values';
// these fields exist in the global store and we need to keep them up to date
// These fields exist in the global store, and we need to keep them up to date
const reactFlowFieldsToTrack = [
'nodes',
'edges',
@@ -94,9 +94,11 @@ const selector = (s: ReactFlowState) => ({
});
const initPrevValues = {
// these are values that are also passed directly to other components
// than the StoreUpdater. We can reduce the number of setStore calls
// by setting the same values here as prev fields.
/*
* these are values that are also passed directly to other components
* than the StoreUpdater. We can reduce the number of setStore calls
* by setting the same values here as prev fields.
*/
translateExtent: infiniteExtent,
nodeOrigin: defaultNodeOrigin,
minZoom: 0.5,
@@ -152,8 +154,8 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
else if (fieldName === 'nodeExtent') setNodeExtent(fieldValue as CoordinateExtent);
else if (fieldName === 'paneClickDistance') setPaneClickDistance(fieldValue as number);
// Renamed fields
else if (fieldName === 'fitView') store.setState({ fitViewOnInit: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOnInitOptions: fieldValue as FitViewOptions });
else if (fieldName === 'fitView') store.setState({ fitViewQueued: fieldValue as boolean });
else if (fieldName === 'fitViewOptions') store.setState({ fitViewOptions: fieldValue as FitViewOptions });
// General case
else store.setState({ [fieldName]: fieldValue });
}

View File

@@ -6,6 +6,31 @@ import type { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.domNode?.querySelector('.react-flow__viewport-portal');
/**
* The `<ViewportPortal />` component can be used to add components to the same viewport
* of the flow where nodes and edges are rendered. This is useful when you want to render
* your own components that are adhere to the same coordinate system as the nodes & edges
* and are also affected by zooming and panning
* @public
* @example
*
* ```jsx
*import React from 'react';
*import { ViewportPortal } from '@xyflow/react';
*
*export default function () {
* return (
* <ViewportPortal>
* <div
* style={{ transform: 'translate(100px, 100px)', position: 'absolute' }}
* >
* This div is positioned at [100, 100] on the flow.
* </div>
* </ViewportPortal>
* );
*}
*```
*/
export function ViewportPortal({ children }: { children: ReactNode }) {
const viewPortalDiv = useStore(selector);

View File

@@ -42,9 +42,11 @@ const Marker = ({
);
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
/*
* when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
* when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
* that we can then use for creating our unique marker ids
*/
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const edges = useStore((s) => s.edges);
const defaultEdgeOptions = useStore((s) => s.defaultEdgeOptions);

View File

@@ -170,7 +170,7 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
disableKeyboardA11y={disableKeyboardA11y}
rfId={rfId}
/>
<ConnectionLineWrapper
<ConnectionLineWrapper<NodeType>
style={connectionLineStyle}
type={connectionLineType}
component={connectionLineComponent}

View File

@@ -44,29 +44,31 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
<div className="react-flow__nodes" style={containerStyle}>
{nodeIds.map((nodeId) => {
return (
// The split of responsibilities between NodeRenderer and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre dragging a single node, that node gets
// updated multiple times per second. If `NodeRenderer` were to update
// every time, it would have to re-run the `nodes.map()` loop every
// time. This gets pricey with hundreds of nodes, especially if every
// loop cycle does more than just rendering a JSX element!
//
// As a result of this choice, we took the following implementation
// decisions:
// - NodeRenderer subscribes *only* to node IDs and therefore
// rerender *only* when visible nodes are added or removed.
// - NodeRenderer performs all operations the result of which can be
// shared between nodes (such as creating the `ResizeObserver`
// instance, or subscribing to `selector`). This means extra prop
// drilling into `NodeComponentWrapper`, but it means we need to run
// these operations only once instead of once per node.
// - Any operations that youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
/*
* The split of responsibilities between NodeRenderer and
* NodeComponentWrapper may appear weird. However, its designed to
* minimize the cost of updates when individual nodes change.
*
* For example, when youre dragging a single node, that node gets
* updated multiple times per second. If `NodeRenderer` were to update
* every time, it would have to re-run the `nodes.map()` loop every
* time. This gets pricey with hundreds of nodes, especially if every
* loop cycle does more than just rendering a JSX element!
*
* As a result of this choice, we took the following implementation
* decisions:
* - NodeRenderer subscribes *only* to node IDs and therefore
* rerender *only* when visible nodes are added or removed.
* - NodeRenderer performs all operations the result of which can be
* shared between nodes (such as creating the `ResizeObserver`
* instance, or subscribing to `selector`). This means extra prop
* drilling into `NodeComponentWrapper`, but it means we need to run
* these operations only once instead of once per node.
* - Any operations that youd normally write inside `nodes.map` are
* moved into `NodeComponentWrapper`. This ensures they are
* memorized so if `NodeRenderer` *has* to rerender, it only
* needs to regenerate the list of nodes, nothing else.
*/
<NodeWrapper<NodeType>
key={nodeId}
id={nodeId}

View File

@@ -61,6 +61,7 @@ const wrapHandler = (
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
connectionInProgress: s.connection.inProgress,
dragging: s.paneDragging,
});
@@ -81,7 +82,7 @@ export function Pane({
children,
}: PaneProps) {
const store = useStoreApi();
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
const { userSelectionActive, elementsSelectable, dragging, connectionInProgress } = useStore(selector, shallow);
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
const container = useRef<HTMLDivElement | null>(null);
@@ -96,7 +97,8 @@ export function Pane({
const onClick = (event: ReactMouseEvent) => {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress.current) {
// We also prevent click events when a connection is in progress
if (selectionInProgress.current || connectionInProgress) {
selectionInProgress.current = false;
return;
}
@@ -233,8 +235,10 @@ export function Pane({
(event.target as Partial<Element>)?.releasePointerCapture?.(event.pointerId);
const { userSelectionRect } = store.getState();
// We only want to trigger click functions when in selection mode if
// the user did not move the mouse.
/*
* We only want to trigger click functions when in selection mode if
* the user did not move the mouse.
*/
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
onClick?.(event);
}
@@ -246,8 +250,10 @@ export function Pane({
});
onSelectionEnd?.(event);
// If the user kept holding the selectionKey during the selection,
// we need to reset the selectionInProgress, so the next click event is not prevented
/*
* If the user kept holding the selectionKey during the selection,
* we need to reset the selectionInProgress, so the next click event is not prevented
*/
if (selectionKeyPressed || selectionOnDrag) {
selectionInProgress.current = false;
}

View File

@@ -31,8 +31,10 @@ export function Wrapper({
const isWrapped = useContext(StoreContext);
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
/*
* we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
* https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
*/
return <>{children}</>;
}

View File

@@ -1,4 +1,4 @@
import { ForwardedRef, type CSSProperties } from 'react';
import { ForwardedRef, useCallback, type CSSProperties } from 'react';
import cc from 'classcat';
import { ConnectionLineType, PanOnScrollMode, SelectionMode, infiniteExtent, isMacOs } from '@xyflow/system';
@@ -144,6 +144,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
height,
colorMode = 'light',
debug,
onScroll,
...rest
}: ReactFlowProps<NodeType, EdgeType>,
ref: ForwardedRef<HTMLDivElement>
@@ -151,10 +152,20 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
const rfId = id || '1';
const colorModeClassName = useColorModeClass(colorMode);
// Undo scroll events, preventing viewport from shifting when nodes outside of it are focused
const wrapperOnScroll = useCallback(
(e: React.UIEvent<HTMLDivElement>) => {
e.currentTarget.scrollTo({ top: 0, left: 0, behavior: 'instant' });
onScroll?.(e);
},
[onScroll]
);
return (
<div
data-testid="rf__wrapper"
{...rest}
onScroll={wrapperOnScroll}
style={{ ...style, ...wrapperStyle }}
ref={ref}
className={cc(['react-flow', className, colorModeClassName])}
@@ -292,7 +303,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
paneClickDistance={paneClickDistance}
debug={debug}
/>
<SelectionListener onSelectionChange={onSelectionChange} />
<SelectionListener<NodeType, EdgeType> onSelectionChange={onSelectionChange} />
{children}
<Attribution proOptions={proOptions} position={attributionPosition} />
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
@@ -301,4 +312,24 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
);
}
/**
* The `<ReactFlow />` component is the heart of your React Flow application.
* It renders your nodes and edges and handles user interaction
*
* @public
*
* @example
* ```tsx
*import { ReactFlow } from '@xyflow/react'
*
*export default function Flow() {
* return (<ReactFlow
* nodes={...}
* edges={...}
* onNodesChange={...}
* ...
* />);
*}
*```
*/
export default fixedForwardRef(ReactFlow);

View File

@@ -95,7 +95,7 @@ export function ZoomPane({
},
});
const { x, y, zoom } = panZoom.current!.getViewport();
const { x, y, zoom } = panZoom.current.getViewport();
store.setState({
panZoom: panZoom.current,

View File

@@ -4,6 +4,34 @@ export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;
/**
* You can use this hook to get the id of the node it is used inside. It is useful
* if you need the node's id deeper in the render tree but don't want to manually
* drill down the id as a prop.
*
* @public
* @returns The id for a node in the flow.
*
* @example
*```jsx
*import { useNodeId } from '@xyflow/react';
*
*export default function CustomNode() {
* return (
* <div>
* <span>This node has an id of </span>
* <NodeIdDisplay />
* </div>
* );
*}
*
*function NodeIdDisplay() {
* const nodeId = useNodeId();
*
* return <span>{nodeId}</span>;
*}
*```
*/
export const useNodeId = (): string | null => {
const nodeId = useContext(NodeIdContext);
return nodeId;

View File

@@ -24,9 +24,32 @@ function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionSt
return storeSelector;
}
/**
* Hook for accessing the connection state.
* The `useConnection` hook returns the current connection when there is an active
* connection interaction. If no connection interaction is active, it returns null
* for every property. A typical use case for this hook is to colorize handles
* based on a certain condition (e.g. if the connection is valid or not).
*
* @public
* @param connectionSelector - An optional selector function used to extract a slice of the
* `ConnectionState` data. Using a selector can prevent component re-renders where data you don't
* otherwise care about might change. If a selector is not provided, the entire `ConnectionState`
* object is returned unchanged.
* @example
*
* ```tsx
*import { useConnection } from '@xyflow/react';
*
*function App() {
* const connection = useConnection();
*
* return (
* <div> {connection ? `Someone is trying to make a connection from ${connection.fromNode} to this one.` : 'There are currently no incoming connections!'}
*
* </div>
* );
* }
* ```
*
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(

View File

@@ -6,10 +6,22 @@ import type { Edge, ReactFlowState } from '../types';
const edgesSelector = (state: ReactFlowState) => state.edges;
/**
* Hook for getting the current edges from the store.
* This hook returns an array of the current edges. Components that use this hook
* will re-render **whenever any edge changes**.
*
* @public
* @returns An array of edges
* @returns An array of all edges currently in the flow.
*
* @example
* ```tsx
*import { useEdges } from '@xyflow/react';
*
*export default function () {
* const edges = useEdges();
*
* return <div>There are currently {edges.length} edges!</div>;
*}
*```
*/
export function useEdges<EdgeType extends Edge = Edge>(): EdgeType[] {
const edges = useStore(edgesSelector, shallow) as EdgeType[];

View File

@@ -10,11 +10,16 @@ import {
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
type useHandleConnectionsParams = {
type UseHandleConnectionsParams = {
/** What type of handle connections do you want to observe? */
type: HandleType;
/** The handle id (this is only needed if the node has multiple handles of the same type). */
id?: string | null;
/** If node id is not provided, the node id from the `NodeIdContext` is used. */
nodeId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void;
};
@@ -23,12 +28,7 @@ type useHandleConnectionsParams = {
*
* @public
* @deprecated Use `useNodeConnections` instead.
* @param param.type - handle type 'source' or 'target'
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with handle connections
* @returns An array with handle connections.
*/
export function useHandleConnections({
type,
@@ -36,7 +36,7 @@ export function useHandleConnections({
nodeId,
onConnect,
onDisconnect,
}: useHandleConnectionsParams): HandleConnection[] {
}: UseHandleConnectionsParams): HandleConnection[] {
console.warn(
'[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections'
);

View File

@@ -5,11 +5,31 @@ import { useStore } from './useStore';
import type { InternalNode, Node } from '../types';
/**
* Hook for getting an internal node by id
* This hook returns the internal representation of a specific node.
* Components that use this hook will re-render **whenever the node changes**,
* including when a node is selected or moved.
*
* @public
* @param id - id of the node
* @returns array with visible node ids
* @param id - The ID of a node you want to observe.
* @returns The `InternalNode` object for the node with the given ID.
*
* @example
* ```tsx
*import { useInternalNode } from '@xyflow/react';
*
*export default function () {
* const internalNode = useInternalNode('node-1');
* const absolutePosition = internalNode.internals.positionAbsolute;
*
* return (
* <div>
* The absolute position of the node is at:
* <p>x: {absolutePosition.x}</p>
* <p>y: {absolutePosition.y}</p>
* </div>
* );
*}
*```
*/
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
const node = useStore(

View File

@@ -6,25 +6,58 @@ type PressedKeys = Set<string>;
type KeyOrCode = 'key' | 'code';
export type UseKeyPressOptions = {
/**
* Listen to key presses on a specific element.
* @default document
*/
target?: Window | Document | HTMLElement | ShadowRoot | null;
/**
* You can use this flag to prevent triggering the key press hook when an input field is focused.
* @default true
*/
actInsideInputWithModifier?: boolean;
preventDefault?: boolean;
};
const defaultDoc = typeof document !== 'undefined' ? document : null;
/**
* Hook for handling key events.
* This hook lets you listen for specific key codes and tells you whether they are
* currently pressed or not.
*
* @public
* @param param.keyCode - The key code (string or array of strings) to use
* @param param.options - Options
* @returns boolean
* @param options - Options
*
* @example
* ```tsx
*import { useKeyPress } from '@xyflow/react';
*
*export default function () {
* const spacePressed = useKeyPress('Space');
* const cmdAndSPressed = useKeyPress(['Meta+s', 'Strg+s']);
*
* return (
* <div>
* {spacePressed && <p>Space pressed!</p>}
* {cmdAndSPressed && <p>Cmd + S pressed!</p>}
* </div>
* );
*}
*```
*/
export function useKeyPress(
// the keycode can be a string 'a' or an array of strings ['a', 'a+d']
// a string means a single key 'a' or a combination when '+' is used 'a+d'
// an array means different possibilites. Explainer: ['a', 'd+s'] here the
// user can use the single key 'a' or the combination 'd' + 's'
/**
* The key code (string or array of strings) specifies which key(s) should trigger
* an action.
*
* A **string** can represent:
* - A **single key**, e.g. `'a'`
* - A **key combination**, using `'+'` to separate keys, e.g. `'a+d'`
*
* An **array of strings** represents **multiple possible key inputs**. For example, `['a', 'd+s']`
* means the user can press either the single key `'a'` or the combination of `'d'` and `'s'`.
* @default null
*/
keyCode: KeyCode | null = null,
options: UseKeyPressOptions = { target: defaultDoc, actInsideInputWithModifier: true }
): boolean {
@@ -36,20 +69,24 @@ export function useKeyPress(
// we need to remember the pressed keys in order to support combinations
const pressedKeys = useRef<PressedKeys>(new Set([]));
// keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
// keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
// used to check if we store event.code or event.key. When the code is in the list of keysToWatch
// we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
// and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
// we can't find it in the list of keysToWatch.
/*
* keyCodes = array with single keys [['a']] or key combinations [['a', 's']]
* keysToWatch = array with all keys flattened ['a', 'd', 'ShiftLeft']
* used to check if we store event.code or event.key. When the code is in the list of keysToWatch
* we use the code otherwise the key. Explainer: When you press the left "command" key, the code is "MetaLeft"
* and the key is "Meta". We want users to be able to pass keys and codes so we assume that the key is meant when
* we can't find it in the list of keysToWatch.
*/
const [keyCodes, keysToWatch] = useMemo<[Array<Keys>, Keys]>(() => {
if (keyCode !== null) {
const keyCodeArr = Array.isArray(keyCode) ? keyCode : [keyCode];
const keys = keyCodeArr
.filter((kc) => typeof kc === 'string')
// we first replace all '+' with '\n' which we will use to split the keys on
// then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
// in the end we simply split on '\n' to get the key array
/*
* we first replace all '+' with '\n' which we will use to split the keys on
* then we replace '\n\n' with '\n+', this way we can also support the combination 'key++'
* in the end we simply split on '\n' to get the key array
*/
.map((kc) => kc.replace('+', '\n').replace('\n\n', '\n+').split('\n'));
const keysFlat = keys.reduce((res: Keys, item) => res.concat(...item), []);
@@ -64,7 +101,7 @@ export function useKeyPress(
if (keyCode !== null) {
const downHandler = (event: KeyboardEvent) => {
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey;
modifierPressed.current = event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
@@ -76,19 +113,18 @@ export function useKeyPress(
pressedKeys.current.add(event[keyOrCode]);
if (isMatchingKey(keyCodes, pressedKeys.current, false)) {
event.preventDefault();
const target = (event.composedPath?.()?.[0] || event.target) as Element | null;
const isInteractiveElement = target?.nodeName === 'BUTTON' || target?.nodeName === 'A';
if (options.preventDefault !== false && (modifierPressed.current || !isInteractiveElement)) {
event.preventDefault();
}
setKeyPressed(true);
}
};
const upHandler = (event: KeyboardEvent) => {
const preventAction =
(!modifierPressed.current || (modifierPressed.current && !options.actInsideInputWithModifier)) &&
isInputDOMNode(event);
if (preventAction) {
return false;
}
const keyOrCode = useKeyOrCode(event.code, keysToWatch);
if (isMatchingKey(keyCodes, pressedKeys.current, true)) {
@@ -133,12 +169,16 @@ export function useKeyPress(
function isMatchingKey(keyCodes: Array<Keys>, pressedKeys: PressedKeys, isUp: boolean): boolean {
return (
keyCodes
// we only want to compare same sizes of keyCode definitions
// and pressed keys. When the user specified 'Meta' as a key somewhere
// this would also be truthy without this filter when user presses 'Meta' + 'r'
/*
* we only want to compare same sizes of keyCode definitions
* and pressed keys. When the user specified 'Meta' as a key somewhere
* this would also be truthy without this filter when user presses 'Meta' + 'r'
*/
.filter((keys) => isUp || keys.length === pressedKeys.size)
// since we want to support multiple possibilities only one of the
// combinations need to be part of the pressed keys
/*
* since we want to support multiple possibilities only one of the
* combinations need to be part of the pressed keys
*/
.some((keys) => keys.every((k) => pressedKeys.has(k)))
);
}

View File

@@ -22,8 +22,10 @@ export function useMoveSelectedNodes() {
const nodeUpdates = new Map();
const isSelected = selectedAndDraggable(nodesDraggable);
// by default a node moves 5px on each key press
// if snap grid is enabled, we use that for the velocity
/*
* by default a node moves 5px on each key press
* if snap grid is enabled, we use that for the velocity
*/
const xVelo = snapToGrid ? snapGrid[0] : 5;
const yVelo = snapToGrid ? snapGrid[1] : 5;

View File

@@ -14,23 +14,39 @@ import { useNodeId } from '../contexts/NodeIdContext';
const error014 = errorMessages['error014']();
type UseNodeConnectionsParams = {
/** ID of the node, filled in automatically if used inside custom node. */
id?: string;
/** What type of handle connections do you want to observe? */
handleType?: HandleType;
/** Filter by handle id (this is only needed if the node has multiple handles of the same type). */
handleId?: string;
/** Gets called when a connection is established. */
onConnect?: (connections: Connection[]) => void;
/** Gets called when a connection is removed. */
onDisconnect?: (connections: Connection[]) => void;
};
/**
* Hook to retrieve all edges connected to a node. Can be filtered by handle type and id.
* This hook returns an array of connections on a specific node, handle type ('source', 'target') or handle ID.
*
* @public
* @param param.id - node id - optional if called inside a custom node
* @param param.handleType - filter by handle type 'source' or 'target'
* @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type)
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections
* @returns An array with connections.
*
* @example
* ```jsx
*import { useNodeConnections } from '@xyflow/react';
*
*export default function () {
* const connections = useNodeConnections({
* handleType: 'target',
* handleId: 'my-handle',
* });
*
* return (
* <div>There are currently {connections.length} incoming connections!</div>
* );
*}
*```
*/
export function useNodeConnections({
id,
@@ -57,7 +73,7 @@ export function useNodeConnections({
);
useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
// @todo discuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);

View File

@@ -6,10 +6,23 @@ import type { Node, ReactFlowState } from '../types';
const nodesSelector = (state: ReactFlowState) => state.nodes;
/**
* Hook for getting the current nodes from the store.
* This hook returns an array of the current nodes. Components that use this hook
* will re-render **whenever any node changes**, including when a node is selected
* or moved.
*
* @public
* @returns An array of nodes
* @returns An array of all nodes currently in the flow.
*
* @example
* ```jsx
*import { useNodes } from '@xyflow/react';
*
*export default function() {
* const nodes = useNodes();
*
* return <div>There are currently {nodes.length} nodes!</div>;
*}
*```
*/
export function useNodes<NodeType extends Node = Node>(): NodeType[] {
const nodes = useStore(nodesSelector, shallow) as NodeType[];

View File

@@ -5,17 +5,31 @@ import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
/**
* Hook for receiving data of one or multiple nodes
* This hook lets you subscribe to changes of a specific nodes `data` object.
*
* @public
* @param nodeId - The id (or ids) of the node to get the data from
* @param guard - Optional guard function to narrow down the node type
* @returns An object (or array of object) with {id, type, data} representing each node
* @returns An object (or array of object) with `id`, `type`, `data` representing each node.
*
* @example
*```jsx
*import { useNodesData } from '@xyflow/react';
*
*export default function() {
* const nodeData = useNodesData('nodeId-1');
* const nodesData = useNodesData(['nodeId-1', 'nodeId-2']);
*
* return null;
*}
*```
*/
export function useNodesData<NodeType extends Node = Node>(
/** The id of the node to get the data from. */
nodeId: string
): Pick<NodeType, 'id' | 'type' | 'data'> | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): Pick<NodeType, 'id' | 'type' | 'data'>[];
export function useNodesData<NodeType extends Node = Node>(
/** The ids of the nodes to get the data from. */
nodeIds: string[]
): Pick<NodeType, 'id' | 'type' | 'data'>[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const nodesData = useStore(

View File

@@ -4,15 +4,58 @@ import { applyNodeChanges, applyEdgeChanges } from '../utils/changes';
import type { Node, Edge, OnNodesChange, OnEdgesChange } from '../types';
/**
* Hook for managing the state of nodes - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialNodes
* @returns an array [nodes, setNodes, onNodesChange]
* @returns
* - `nodes`: The current array of nodes. You might pass this directly to the `nodes` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
* - `setNodes`: A function that you can use to update the nodes. You can pass it a new array of
* nodes or a callback that receives the current array of nodes and returns a new array of nodes.
* This is the same as the second element of the tuple returned by React's `useState` hook.
* - `onNodesChange`: A handy callback that can take an array of `NodeChanges` and update the nodes
* state accordingly. You'll typically pass this directly to the `onNodesChange` prop of your
* `<ReactFlow />` component.
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useNodesState<NodeType extends Node>(
initialNodes: NodeType[]
): [NodeType[], Dispatch<SetStateAction<NodeType[]>>, OnNodesChange<NodeType>] {
): [
//
nodes: NodeType[],
setNodes: Dispatch<SetStateAction<NodeType[]>>,
onNodesChange: OnNodesChange<NodeType>
] {
const [nodes, setNodes] = useState(initialNodes);
const onNodesChange: OnNodesChange<NodeType> = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
@@ -23,15 +66,60 @@ export function useNodesState<NodeType extends Node>(
}
/**
* Hook for managing the state of edges - should only be used for prototyping / simple use cases.
* This hook makes it easy to prototype a controlled flow where you manage the
* state of nodes and edges outside the `ReactFlowInstance`. You can think of it
* like React's `useState` hook with an additional helper callback.
*
* @public
* @param initialEdges
* @returns an array [edges, setEdges, onEdgesChange]
* @returns
* - `edges`: The current array of edges. You might pass this directly to the `edges` prop of your
* `<ReactFlow />` component, or you may want to manipulate it first to perform some layouting,
* for example.
*
* - `setEdges`: A function that you can use to update the edges. You can pass it a new array of
* edges or a callback that receives the current array of edges and returns a new array of edges.
* This is the same as the second element of the tuple returned by React's `useState` hook.
*
* - `onEdgesChange`: A handy callback that can take an array of `EdgeChanges` and update the edges
* state accordingly. You'll typically pass this directly to the `onEdgesChange` prop of your
* `<ReactFlow />` component.
* @example
*
*```tsx
*import { ReactFlow, useNodesState, useEdgesState } from '@xyflow/react';
*
*const initialNodes = [];
*const initialEdges = [];
*
*export default function () {
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
*
* return (
* <ReactFlow
* nodes={nodes}
* edges={edges}
* onNodesChange={onNodesChange}
* onEdgesChange={onEdgesChange}
* />
* );
*}
*```
*
* @remarks This hook was created to make prototyping easier and our documentation
* examples clearer. Although it is OK to use this hook in production, in
* practice you may want to use a more sophisticated state management solution
* like Zustand {@link https://reactflow.dev/docs/guides/state-management/} instead.
*
*/
export function useEdgesState<EdgeType extends Edge = Edge>(
initialEdges: EdgeType[]
): [EdgeType[], Dispatch<SetStateAction<EdgeType[]>>, OnEdgesChange<EdgeType>] {
): [
//
edges: EdgeType[],
setEdges: Dispatch<SetStateAction<EdgeType[]>>,
onEdgesChange: OnEdgesChange<EdgeType>
] {
const [edges, setEdges] = useState(initialEdges);
const onEdgesChange: OnEdgesChange<EdgeType> = useCallback(
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),

View File

@@ -1,8 +1,10 @@
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
import { nodeHasDimensions } from '@xyflow/system';
import { useStore } from './useStore';
import type { ReactFlowState } from '../types';
export type UseNodesInitializedOptions = {
/** @default false */
includeHiddenNodes?: boolean;
};
@@ -22,18 +24,44 @@ const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) =>
return true;
};
const defaultOptions = {
includeHiddenNodes: false,
};
/**
* Hook which returns true when all nodes are initialized.
* This hook tells you whether all the nodes in a flow have been measured and given
*a width and height. When you add a node to the flow, this hook will return
*`false` and then `true` again once the node has been measured.
*
* @public
* @param options.includeHiddenNodes - defaults to false
* @returns boolean indicating whether all nodes are initialized
* @returns Whether or not the nodes have been initialized by the `<ReactFlow />` component and
* given a width and height.
*
* @example
* ```jsx
*import { useReactFlow, useNodesInitialized } from '@xyflow/react';
*import { useEffect, useState } from 'react';
*
*const options = {
* includeHiddenNodes: false,
*};
*
*export default function useLayout() {
* const { getNodes } = useReactFlow();
* const nodesInitialized = useNodesInitialized(options);
* const [layoutedNodes, setLayoutedNodes] = useState(getNodes());
*
* useEffect(() => {
* if (nodesInitialized) {
* setLayoutedNodes(yourLayoutingFunction(getNodes()));
* }
* }, [nodesInitialized]);
*
* return layoutedNodes;
*}
*```
*/
export function useNodesInitialized(options: UseNodesInitializedOptions = defaultOptions): boolean {
export function useNodesInitialized(
options: UseNodesInitializedOptions = {
includeHiddenNodes: false,
}
): boolean {
const initialized = useStore(selector(options));
return initialized;

View File

@@ -1,20 +1,53 @@
import { useEffect } from 'react';
import { useStoreApi } from './useStore';
import type { OnSelectionChangeFunc } from '../types';
import type { OnSelectionChangeFunc, Node, Edge } from '../types';
export type UseOnSelectionChangeOptions = {
onChange: OnSelectionChangeFunc;
export type UseOnSelectionChangeOptions<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/** The handler to register. */
onChange: OnSelectionChangeFunc<NodeType, EdgeType>;
};
/**
* Hook for registering an onSelectionChange handler.
* This hook lets you listen for changes to both node and edge selection. As the
*name implies, the callback you provide will be called whenever the selection of
*_either_ nodes or edges changes.
*
* @public
* @param params.onChange - The handler to register
* @example
* ```jsx
*import { useState } from 'react';
*import { ReactFlow, useOnSelectionChange } from '@xyflow/react';
*
*function SelectionDisplay() {
* const [selectedNodes, setSelectedNodes] = useState([]);
* const [selectedEdges, setSelectedEdges] = useState([]);
*
* // the passed handler has to be memoized, otherwise the hook will not work correctly
* const onChange = useCallback(({ nodes, edges }) => {
* setSelectedNodes(nodes.map((node) => node.id));
* setSelectedEdges(edges.map((edge) => edge.id));
* }, []);
*
* useOnSelectionChange({
* onChange,
* });
*
* return (
* <div>
* <p>Selected nodes: {selectedNodes.join(', ')}</p>
* <p>Selected edges: {selectedEdges.join(', ')}</p>
* </div>
* );
*}
*```
*
* @remarks You need to memoize the passed `onChange` handler, otherwise the hook will not work correctly.
*/
export function useOnSelectionChange({ onChange }: UseOnSelectionChangeOptions) {
const store = useStoreApi();
export function useOnSelectionChange<NodeType extends Node = Node, EdgeType extends Edge = Edge>({
onChange,
}: UseOnSelectionChangeOptions<NodeType, EdgeType>) {
const store = useStoreApi<NodeType, EdgeType>();
useEffect(() => {
const nextOnSelectionChangeHandlers = [...store.getState().onSelectionChangeHandlers, onChange];

View File

@@ -4,18 +4,35 @@ import type { OnViewportChange } from '@xyflow/system';
import { useStoreApi } from './useStore';
export type UseOnViewportChangeOptions = {
/** Gets called when the viewport starts changing. */
onStart?: OnViewportChange;
/** Gets called when the viewport changes. */
onChange?: OnViewportChange;
/** Gets called when the viewport stops changing. */
onEnd?: OnViewportChange;
};
/**
* Hook for registering an onViewportChange handler.
* The `useOnViewportChange` hook lets you listen for changes to the viewport such
* as panning and zooming. You can provide a callback for each phase of a viewport
* change: `onStart`, `onChange`, and `onEnd`.
*
* @public
* @param params.onStart - gets called when the viewport starts changing
* @param params.onChange - gets called when the viewport changes
* @param params.onEnd - gets called when the viewport stops changing
* @example
* ```jsx
*import { useCallback } from 'react';
*import { useOnViewportChange } from '@xyflow/react';
*
*function ViewportChangeLogger() {
* useOnViewportChange({
* onStart: (viewport: Viewport) => console.log('start', viewport),
* onChange: (viewport: Viewport) => console.log('change', viewport),
* onEnd: (viewport: Viewport) => console.log('end', viewport),
* });
*
* return null;
*}
*```
*/
export function useOnViewportChange({ onStart, onChange, onEnd }: UseOnViewportChangeOptions) {
const store = useStoreApi();

View File

@@ -15,15 +15,44 @@ import useViewportHelper from './useViewportHelper';
import { useStore, useStoreApi } from './useStore';
import { useBatchContext } from '../components/BatchProvider';
import { elementToRemoveChange, isEdge, isNode } from '../utils';
import type { ReactFlowInstance, Node, Edge, InternalNode, ReactFlowState, GeneralHelpers } from '../types';
import type {
ReactFlowInstance,
Node,
Edge,
InternalNode,
ReactFlowState,
GeneralHelpers,
FitViewOptions,
} from '../types';
const selector = (s: ReactFlowState) => !!s.panZoom;
/**
* Hook for accessing the ReactFlow instance.
* This hook returns a ReactFlowInstance that can be used to update nodes and edges, manipulate the viewport, or query the current state of the flow.
*
* @public
* @returns ReactFlowInstance
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { useReactFlow } from '@xyflow/react';
*
*export function NodeCounter() {
* const reactFlow = useReactFlow();
* const [count, setCount] = useState(0);
* const countNodes = useCallback(() => {
* setCount(reactFlow.getNodes().length);
* // you need to pass it as a dependency if you are using it with useEffect or useCallback
* // because at the first render, it's not initialized yet and some functions might not work.
* }, [reactFlow]);
*
* return (
* <div>
* <button onClick={countNodes}>Update count</button>
* <p>There are {count} nodes in the flow.</p>
* </div>
* );
*}
*```
*/
export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(): ReactFlowInstance<
NodeType,
@@ -72,7 +101,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setNodes((prevNodes) =>
prevNodes.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as NodeType) : nodeUpdate;
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
return options.replace && isNode(nextNode) ? (nextNode as NodeType) : { ...node, ...nextNode };
}
@@ -89,7 +118,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
setEdges((prevEdges) =>
prevEdges.map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge as EdgeType) : edgeUpdate;
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? (nextEdge as EdgeType) : { ...edge, ...nextEdge };
}
@@ -184,7 +213,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return (nodes || store.getState().nodes).filter((n) => {
const internalNode = store.getState().nodeLookup.get(n.id);
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
if (internalNode && !isRect && (n.id === nodeOrRect.id || !internalNode.internals.positionAbsolute)) {
return false;
}
@@ -248,6 +277,17 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
.connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`)
?.values() ?? []
),
fitView: async (options: FitViewOptions<NodeType> | undefined) => {
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.getState().fitViewResolver ?? Promise.withResolvers<boolean>();
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.setState({ fitViewQueued: true, fitViewOptions: options, fitViewResolver });
batchContext.nodeQueue.push((nodes) => [...nodes]);
return fitViewResolver.promise;
},
};
}, []);

View File

@@ -16,7 +16,7 @@ export function useResizeHandler(domNode: MutableRefObject<HTMLDivElement | null
if (!domNode.current) {
return false;
}
const size = getDimensions(domNode.current!);
const size = getDimensions(domNode.current);
if (size.height === 0 || size.width === 0) {
store.getState().onError?.('004', errorMessages['error004']());

View File

@@ -9,16 +9,27 @@ import type { Edge, Node, ReactFlowState } from '../types';
const zustandErrorMessage = errorMessages['error001']();
/**
* Hook for accessing the internal store. Should only be used in rare cases.
* This hook can be used to subscribe to internal state changes of the React Flow
* component. The `useStore` hook is re-exported from the [Zustand](https://github.com/pmndrs/zustand)
* state management library, so you should check out their docs for more details.
*
* @public
* @param selector
* @param equalityFn
* @returns The selected state slice
* @param selector - A selector function that returns a slice of the flow's internal state.
* Extracting or transforming just the state you need is a good practice to avoid unnecessary
* re-renders.
* @param equalityFn - A function to compare the previous and next value. This is incredibly useful
* for preventing unnecessary re-renders. Good sensible defaults are using `Object.is` or importing
* `zustand/shallow`, but you can be as granular as you like.
* @returns The selected state slice.
*
* @example
* const nodes = useStore((state: ReactFlowState<MyNodeType>) => state.nodes);
* ```ts
* const nodes = useStore((state) => state.nodes);
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStore<StateSlice = unknown>(
selector: (state: ReactFlowState) => StateSlice,
@@ -33,6 +44,19 @@ function useStore<StateSlice = unknown>(
return useZustandStore(store, selector, equalityFn);
}
/**
* In some cases, you might need to access the store directly. This hook returns the store object which can be used on demand to access the state or dispatch actions.
*
* @returns The store object.
* @example
* ```ts
* const store = useStoreApi();
* ```
*
* @remarks This hook should only be used if there is no other way to access the internal
* state. For many of the common use cases, there are dedicated hooks available
* such as {@link useReactFlow}, {@link useViewport}, etc.
*/
function useStoreApi<NodeType extends Node = Node, EdgeType extends Edge = Edge>() {
const store = useContext(StoreContext) as UseBoundStoreWithEqualityFn<
StoreApi<ReactFlowState<NodeType, EdgeType>>

View File

@@ -4,10 +4,49 @@ import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
/**
* Hook for updating node internals.
* When you programmatically add or remove handles to a node or update a node's
* handle position, you need to let React Flow know about it using this hook. This
* will update the internal dimensions of the node and properly reposition handles
* on the canvas if necessary.
*
* @public
* @returns function for updating node internals
* @returns Use this function to tell React Flow to update the internal state of one or more nodes
* that you have changed programmatically.
*
* @example
* ```jsx
*import { useCallback, useState } from 'react';
*import { Handle, useUpdateNodeInternals } from '@xyflow/react';
*
*export default function RandomHandleNode({ id }) {
* const updateNodeInternals = useUpdateNodeInternals();
* const [handleCount, setHandleCount] = useState(0);
* const randomizeHandleCount = useCallback(() => {
* setHandleCount(Math.floor(Math.random() * 10));
* updateNodeInternals(id);
* }, [id, updateNodeInternals]);
*
* return (
* <>
* {Array.from({ length: handleCount }).map((_, index) => (
* <Handle
* key={index}
* type="target"
* position="left"
* id={`handle-${index}`}
* />
* ))}
*
* <div>
* <button onClick={randomizeHandleCount}>Randomize handle count</button>
* <p>There are {handleCount} handles on this node.</p>
* </div>
* </>
* );
*}
*```
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useUpdateNodeInternals(): UpdateNodeInternals {
const store = useStoreApi();

View File

@@ -11,10 +11,33 @@ const viewportSelector = (state: ReactFlowState) => ({
});
/**
* Hook for getting the current viewport from the store.
* The `useViewport` hook is a convenient way to read the current state of the
* {@link Viewport} in a component. Components that use this hook
* will re-render **whenever the viewport changes**.
*
* @public
* @returns The current viewport
* @returns The current viewport.
*
* @example
*
*```jsx
*import { useViewport } from '@xyflow/react';
*
*export default function ViewportDisplay() {
* const { x, y, zoom } = useViewport();
*
* return (
* <div>
* <p>
* The viewport is currently at ({x}, {y}) and zoomed to {zoom}.
* </p>
* </div>
* );
*}
*```
*
* @remarks This hook can only be used in a component that is a child of a
*{@link ReactFlowProvider} or a {@link ReactFlow} component.
*/
export function useViewport(): Viewport {
const viewport = useStore(viewportSelector, shallow);

View File

@@ -2,11 +2,9 @@ import { useMemo } from 'react';
import {
pointToRendererPoint,
getViewportForBounds,
getFitViewNodes,
fitView,
type XYPosition,
rendererPointToPoint,
getDimensions,
SnapGrid,
} from '@xyflow/system';
import { useStoreApi } from '../hooks/useStore';
@@ -64,28 +62,6 @@ const useViewportHelper = (): ViewportHelperFunctions => {
const [x, y, zoom] = store.getState().transform;
return { x, y, zoom };
},
fitView: (options) => {
const { nodeLookup, minZoom, maxZoom, panZoom, domNode } = store.getState();
if (!panZoom || !domNode) {
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
const { width, height } = getDimensions(domNode);
return fitView(
{
nodes: fitViewNodes,
width,
height,
minZoom,
maxZoom,
panZoom,
},
options
);
},
setCenter: async (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState();
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom;
@@ -119,21 +95,25 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return Promise.resolve(true);
},
screenToFlowPosition: (clientPosition: XYPosition, options: { snapToGrid: boolean } = { snapToGrid: true }) => {
const { transform, snapGrid, domNode } = store.getState();
screenToFlowPosition: (
clientPosition: XYPosition,
options: { snapToGrid?: boolean; snapGrid?: SnapGrid } = {}
) => {
const { transform, snapGrid, snapToGrid, domNode } = store.getState();
if (!domNode) {
return clientPosition;
}
const { x: domX, y: domY } = domNode.getBoundingClientRect();
const correctedPosition = {
x: clientPosition.x - domX,
y: clientPosition.y - domY,
};
const _snapGrid = options.snapGrid ?? snapGrid;
const _snapToGrid = options.snapToGrid ?? snapToGrid;
return pointToRendererPoint(correctedPosition, transform, options.snapToGrid, snapGrid);
return pointToRendererPoint(correctedPosition, transform, _snapToGrid, _snapGrid);
},
flowToScreenPosition: (flowPosition: XYPosition) => {
const { transform, domNode } = store.getState();

View File

@@ -8,8 +8,8 @@ import type { Node, ReactFlowState } from '../types';
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
};

View File

@@ -9,7 +9,7 @@ export { SmoothStepEdge } from './components/Edges/SmoothStepEdge';
export { BaseEdge } from './components/Edges/BaseEdge';
export { ReactFlowProvider } from './components/ReactFlowProvider';
export { Panel, type PanelProps } from './components/Panel';
export { EdgeLabelRenderer } from './components/EdgeLabelRenderer';
export { EdgeLabelRenderer, type EdgeLabelRendererProps } from './components/EdgeLabelRenderer';
export { ViewportPortal } from './components/ViewportPortal';
export { useReactFlow } from './hooks/useReactFlow';
@@ -106,6 +106,7 @@ export {
type FinalConnectionState,
type ConnectionInProgress,
type NoConnection,
type NodeConnection,
} from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error

View File

@@ -1,7 +1,5 @@
import { createWithEqualityFn } from 'zustand/traditional';
import {
getFitViewNodes,
fitView as fitViewSystem,
adoptUserNodes,
updateAbsolutePositions,
panBy as panBySystem,
@@ -15,11 +13,12 @@ import {
initialConnection,
NodeOrigin,
CoordinateExtent,
fitViewport,
} from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
import getInitialState from './initialState';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types';
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types';
const createStore = ({
nodes,
@@ -42,25 +41,60 @@ const createStore = ({
nodeOrigin?: NodeOrigin;
nodeExtent?: CoordinateExtent;
}) =>
createWithEqualityFn<ReactFlowState>(
(set, get) => ({
createWithEqualityFn<ReactFlowState>((set, get) => {
async function resolveFitView() {
const { nodeLookup, panZoom, fitViewOptions, fitViewResolver, width, height, minZoom, maxZoom } = get();
if (!panZoom) {
return;
}
await fitViewport(
{
nodes: nodeLookup,
width,
height,
panZoom,
minZoom,
maxZoom,
},
fitViewOptions
);
fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
set({ fitViewResolver: null });
}
return {
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions:
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
adoptUserNodes(nodes, nodeLookup, parentLookup, {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued } = get();
/*
* setNodes() is called exclusively in response to user actions:
* - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
* - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
*
* When this happens, we take the note objects passed by the user and extend them with fields
* relevant for internal React Flow operations.
*/
const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin,
nodeExtent,
elevateNodesOnSelect,
checkEquality: true,
});
set({ nodes });
if (fitViewQueued && nodesInitialized) {
resolveFitView();
set({ nodes, fitViewQueued: false, fitViewOptions: undefined });
} else {
set({ nodes });
}
},
setEdges: (edges: Edge[]) => {
const { connectionLookup, edgeLookup } = get();
@@ -81,23 +115,14 @@ const createStore = ({
set({ hasDefaultEdges: true });
}
},
// 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, params = { triggerFitView: true }) => {
const {
triggerNodeChanges,
nodeLookup,
parentLookup,
fitViewOnInit,
fitViewDone,
fitViewOnInitOptions,
domNode,
nodeOrigin,
nodeExtent,
debug,
fitViewSync,
} = get();
/*
* 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) => {
const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug, fitViewQueued } =
get();
const { changes, updatedInternals } = updateNodeInternalsSystem(
updates,
@@ -114,23 +139,9 @@ const createStore = ({
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent });
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,
});
}
// 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 (fitViewQueued) {
resolveFitView();
set({ fitViewQueued: false, fitViewOptions: undefined });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
set({});
@@ -146,9 +157,12 @@ const createStore = ({
updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = [];
const changes = [];
const { nodeLookup, triggerNodeChanges } = get();
for (const [id, dragItem] of nodeDragItems) {
const expandParent = !!(dragItem?.expandParent && dragItem?.parentId && dragItem?.position);
// we are using the nodelookup to be sure to use the current expandParent and parentId value
const node = nodeLookup.get(id);
const expandParent = !!(node?.expandParent && node?.parentId && dragItem?.position);
const change: NodeChange = {
id,
@@ -162,14 +176,14 @@ const createStore = ({
dragging,
};
if (expandParent) {
if (expandParent && node.parentId) {
parentExpandChildren.push({
id,
parentId: dragItem.parentId!,
parentId: node.parentId,
rect: {
...dragItem.internals.positionAbsolute,
width: dragItem.measured.width!,
height: dragItem.measured.height!,
width: dragItem.measured.width ?? 0,
height: dragItem.measured.height ?? 0,
},
});
}
@@ -178,12 +192,12 @@ const createStore = ({
}
if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup, nodeOrigin } = get();
const { parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges);
}
get().triggerNodeChanges(changes);
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes) => {
const { onNodesChange, setNodes, nodes, hasDefaultNodes, debug } = get();
@@ -222,7 +236,7 @@ const createStore = ({
if (multiSelectionActive) {
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]);
triggerNodeChanges(nodeChanges);
return;
}
@@ -234,7 +248,7 @@ const createStore = ({
if (multiSelectionActive) {
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
triggerEdgeChanges(changedEdges as EdgeSelectionChange[]);
triggerEdgeChanges(changedEdges);
return;
}
@@ -248,8 +262,10 @@ const createStore = ({
const nodeChanges = nodesToUnselect.map((n) => {
const internalNode = nodeLookup.get(n.id);
if (internalNode) {
// we need to unselect the internal node that was selected previously before we
// send the change to the user to prevent it to be selected while dragging the new node
/*
* we need to unselect the internal node that was selected previously before we
* send the change to the user to prevent it to be selected while dragging the new node
*/
internalNode.selected = false;
}
@@ -257,8 +273,8 @@ const createStore = ({
});
const edgeChanges = edgesToUnselect.map((edge) => createSelectionChange(edge.id, false));
triggerNodeChanges(nodeChanges as NodeSelectionChange[]);
triggerEdgeChanges(edgeChanges as EdgeSelectionChange[]);
triggerNodeChanges(nodeChanges);
triggerEdgeChanges(edgeChanges);
},
setMinZoom: (minZoom) => {
const { panZoom, maxZoom } = get();
@@ -284,11 +300,11 @@ const createStore = ({
const { edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
const nodeChanges = nodes.reduce<NodeSelectionChange[]>(
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false) as NodeSelectionChange] : res),
(res, node) => (node.selected ? [...res, createSelectionChange(node.id, false)] : res),
[]
);
const edgeChanges = edges.reduce<EdgeSelectionChange[]>(
(res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false) as EdgeSelectionChange] : res),
(res, edge) => (edge.selected ? [...res, createSelectionChange(edge.id, false)] : res),
[]
);
@@ -321,52 +337,6 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
},
fitView: (options?: FitViewOptions): Promise<boolean> => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) {
return Promise.resolve(false);
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
return fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
panZoom,
minZoom,
maxZoom,
},
options
);
},
// we can't call an asnychronous function in updateNodeInternals
// for that we created this sync version of fitView
fitViewSync: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) {
return false;
}
const fitViewNodes = getFitViewNodes(nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
panZoom,
minZoom,
maxZoom,
},
options
);
return fitViewNodes.size > 0;
},
cancelConnection: () => {
set({
connection: { ...initialConnection },
@@ -377,8 +347,7 @@ const createStore = ({
},
reset: () => set({ ...getInitialState() }),
}),
Object.is
);
};
}, Object.is);
export { createStore };

View File

@@ -104,13 +104,14 @@ const getInitialState = ({
elementsSelectable: true,
elevateNodesOnSelect: true,
elevateEdgesOnSelect: false,
fitViewOnInit: false,
fitViewDone: false,
fitViewOnInitOptions: undefined,
selectNodesOnDrag: true,
multiSelectionActive: false,
fitViewQueued: fitView ?? false,
fitViewOptions: undefined,
fitViewResolver: null,
connection: { ...initialConnection },
connectionClickStartHandle: null,
connectOnClick: true,

View File

@@ -52,7 +52,9 @@ import type {
*/
export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends Edge = Edge>
extends Omit<HTMLAttributes<HTMLDivElement>, 'onError'> {
/** An array of nodes to render in a controlled flow.
/**
* An array of nodes to render in a controlled flow.
* @default []
* @example
* const nodes = [
* {
@@ -64,7 +66,9 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* ];
*/
nodes?: NodeType[];
/** An array of edges to render in a controlled flow.
/**
* An array of edges to render in a controlled flow.
* @default []
* @example
* const edges = [
* {
@@ -79,7 +83,8 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
defaultNodes?: NodeType[];
/** The initial edges to render in an uncontrolled flow. */
defaultEdges?: EdgeType[];
/** Defaults to be applied to all new edges that are added to the flow.
/**
* Defaults to be applied to all new edges that are added to the flow.
*
* Properties on a new edge will override these defaults if they exist.
* @example
@@ -99,57 +104,75 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* }
*/
defaultEdgeOptions?: DefaultEdgeOptions;
/** This event handler is called when a user clicks on a node */
/** This event handler is called when a user clicks on a node. */
onNodeClick?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user double clicks on a node */
/** This event handler is called when a user double-clicks on a node. */
onNodeDoubleClick?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user enters a node */
/** This event handler is called when mouse of a user enters a node. */
onNodeMouseEnter?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user moves over a node */
/** This event handler is called when mouse of a user moves over a node. */
onNodeMouseMove?: NodeMouseHandler<NodeType>;
/** This event handler is called when mouse of a user leaves a node */
/** This event handler is called when mouse of a user leaves a node. */
onNodeMouseLeave?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user right clicks on a node */
/** This event handler is called when a user right-clicks on a node. */
onNodeContextMenu?: NodeMouseHandler<NodeType>;
/** This event handler is called when a user starts to drag a node */
/** This event handler is called when a user starts to drag a node. */
onNodeDragStart?: OnNodeDrag<NodeType>;
/** This event handler is called when a user drags a node */
/** This event handler is called when a user drags a node. */
onNodeDrag?: OnNodeDrag<NodeType>;
/** This event handler is called when a user stops dragging a node */
/** This event handler is called when a user stops dragging a node. */
onNodeDragStop?: OnNodeDrag<NodeType>;
/** This event handler is called when a user clicks on an edge */
/** This event handler is called when a user clicks on an edge. */
onEdgeClick?: (event: ReactMouseEvent, edge: EdgeType) => void;
/** This event handler is called when a user right clicks on an edge */
/** This event handler is called when a user right-clicks on an edge. */
onEdgeContextMenu?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user enters an edge */
/** This event handler is called when mouse of a user enters an edge. */
onEdgeMouseEnter?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user moves over an edge */
/** This event handler is called when mouse of a user moves over an edge. */
onEdgeMouseMove?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when mouse of a user leaves an edge */
/** This event handler is called when mouse of a user leaves an edge. */
onEdgeMouseLeave?: EdgeMouseHandler<EdgeType>;
/** This event handler is called when a user double clicks on an edge */
/** This event handler is called when a user double-clicks on an edge. */
onEdgeDoubleClick?: EdgeMouseHandler<EdgeType>;
/**
* This handler is called when the source or target of a reconnectable edge is dragged from the
* current node. It will fire even if the edge's source or target do not end up changing.
*
* You can use the `reconnectEdge` utility to convert the connection to a new edge.
*/
onReconnect?: OnReconnect<EdgeType>;
/**
* This event fires when the user begins dragging the source or target of an editable edge.
*/
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
/**
* This event fires when the user releases the source or target of an editable edge. It is called
* even if an edge update does not occur.
*
*/
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
/** This event handler is called when a Node is updated
/**
* Use this event handler to add interactivity to a controlled flow.
* It is called on node drag, select, and move.
* @example // Use NodesState hook to create edges and get onNodesChange handler
* import ReactFlow, { useNodesState } from '@xyflow/react';
* const [edges, setNodes, onNodesChange] = useNodesState(initialNodes);
* const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
*
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
* @example // Use helper function to update edge
* @example // Use helper function to update node
* import ReactFlow, { applyNodeChanges } from '@xyflow/react';
*
* const onNodeChange = useCallback(
* (changes) => setNode((eds) => applyNodeChanges(changes, eds)),
* (changes) => setNode((nds) => applyNodeChanges(changes, nds)),
* [],
* );
*
* return (<ReactFlow onNodeChange={onNodeChange} {...rest} />)
*/
onNodesChange?: OnNodesChange<NodeType>;
/** This event handler is called when a Edge is updated
/**
* Use this event handler to add interactivity to a controlled flow. It is called on edge select
* and remove.
* @example // Use EdgesState hook to create edges and get onEdgesChange handler
* import ReactFlow, { useEdgesState } from '@xyflow/react';
* const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -166,24 +189,28 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onEdgesChange={onEdgesChange} {...rest} />)
*/
onEdgesChange?: OnEdgesChange<EdgeType>;
/** This event handler gets called when a Node is deleted */
/** This event handler gets called when a node is deleted. */
onNodesDelete?: OnNodesDelete<NodeType>;
/** This event handler gets called when a Edge is deleted */
/** This event handler gets called when an edge is deleted. */
onEdgesDelete?: OnEdgesDelete<EdgeType>;
/** This event handler gets called when a Node or Edge is deleted */
/** This event handler gets called when a node or edge is deleted. */
onDelete?: OnDelete<NodeType, EdgeType>;
/** This event handler gets called when a user starts to drag a selection box */
/** This event handler gets called when a user starts to drag a selection box. */
onSelectionDragStart?: SelectionDragHandler<NodeType>;
/** This event handler gets called when a user drags a selection box */
/** This event handler gets called when a user drags a selection box. */
onSelectionDrag?: SelectionDragHandler<NodeType>;
/** This event handler gets called when a user stops dragging a selection box */
/** This event handler gets called when a user stops dragging a selection box. */
onSelectionDragStop?: SelectionDragHandler<NodeType>;
onSelectionStart?: (event: ReactMouseEvent) => void;
onSelectionEnd?: (event: ReactMouseEvent) => void;
/**
* This event handler is called when a user right-clicks on a node selection.
*/
onSelectionContextMenu?: (event: ReactMouseEvent, nodes: NodeType[]) => void;
/** When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
/**
* When a connection line is completed and two nodes are connected by the user, this event fires with the new connection.
*
* You can use the addEdge utility to convert the connection to a complete edge.
* You can use the `addEdge` utility to convert the connection to a complete edge.
* @example // Use helper function to update edges onConnect
* import ReactFlow, { addEdge } from '@xyflow/react';
*
@@ -195,178 +222,260 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* return (<ReactFlow onConnect={onConnect} {...rest} />)
*/
onConnect?: OnConnect;
/** This event handler gets called when a user starts to drag a connection line */
/** This event handler gets called when a user starts to drag a connection line. */
onConnectStart?: OnConnectStart;
/** This event handler gets called when a user stops dragging a connection line */
/**
* This callback will fire regardless of whether a valid connection could be made or not. You can
* use the second `connectionState` parameter to have different behavior when a connection was
* unsuccessful.
*/
onConnectEnd?: OnConnectEnd;
onClickConnectStart?: OnConnectStart;
onClickConnectEnd?: OnConnectEnd;
/** This event handler gets called when a flow has finished initializing */
/**
* The `onInit` callback is called when the viewport is initialized. At this point you can use the
* instance to call methods like `fitView` or `zoomTo`.
*/
onInit?: OnInit<NodeType, EdgeType>;
/** This event handler is called while the user is either panning or zooming the viewport. */
onMove?: OnMove;
/** This event handler gets called when a user starts to pan or zoom the viewport */
/** This event handler is called when the user begins to pan or zoom the viewport. */
onMoveStart?: OnMoveStart;
/** This event handler gets called when a user stops panning or zooming the viewport */
/**
* This event handler is called when panning or zooming viewport movement stops.
* If the movement is not user-initiated, the event parameter will be `null`.
*/
onMoveEnd?: OnMoveEnd;
/** This event handler gets called when a user changes group of selected elements in the flow */
onSelectionChange?: OnSelectionChangeFunc;
/** This event handler gets called when user scroll inside the pane */
/** This event handler gets called when a user changes group of selected elements in the flow. */
onSelectionChange?: OnSelectionChangeFunc<NodeType, EdgeType>;
/** This event handler gets called when user scroll inside the pane. */
onPaneScroll?: (event?: WheelEvent) => void;
/** This event handler gets called when user clicks inside the pane */
/** This event handler gets called when user clicks inside the pane. */
onPaneClick?: (event: ReactMouseEvent) => void;
/** This event handler gets called when user right clicks inside the pane */
/** This event handler gets called when user right clicks inside the pane. */
onPaneContextMenu?: (event: ReactMouseEvent | MouseEvent) => void;
/** This event handler gets called when mouse enters the pane */
/** This event handler gets called when mouse enters the pane. */
onPaneMouseEnter?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse moves over the pane */
/** This event handler gets called when mouse moves over the pane. */
onPaneMouseMove?: (event: ReactMouseEvent) => void;
/** This event handler gets called when mouse leaves the pane */
/** This event handler gets called when mouse leaves the pane. */
onPaneMouseLeave?: (event: ReactMouseEvent) => void;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click.
* @default 0
*/
paneClickDistance?: number;
/** Distance that the mouse can move between mousedown/up that will trigger a click
/**
* Distance that the mouse can move between mousedown/up that will trigger a click.
* @default 0
*/
nodeClickDistance?: number;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
/**
* This handler is called before nodes or edges are deleted, allowing the deletion to be aborted
* by returning `false` or modified by returning updated nodes and edges.
*/
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
/** Custom node types to be available in a flow.
/**
* Custom node types to be available in a flow.
*
* React Flow matches a node's type to a component in the nodeTypes object.
* React Flow matches a node's type to a component in the `nodeTypes` object.
* @TODO check if @default is correct
* @default {
* input: InputNode,
* default: DefaultNode,
* output: OutputNode,
* group: GroupNode
* }
* @example
* import CustomNode from './CustomNode';
*
* const nodeTypes = { nameOfNodeType: CustomNode };
*/
nodeTypes?: NodeTypes;
/** Custom edge types to be available in a flow.
/**
* Custom edge types to be available in a flow.
*
* React Flow matches an edge's type to a component in the edgeTypes object.
* React Flow matches an edge's type to a component in the `edgeTypes` object.
* @TODO check if @default is correct
* @default {
* default: BezierEdge,
* straight: StraightEdge,
* step: StepEdge,
* smoothstep: SmoothStepEdge,
* simplebezier: SimpleBezier
* }
* @example
* import CustomEdge from './CustomEdge';
*
* const edgeTypes = { nameOfEdgeType: CustomEdge };
*/
edgeTypes?: EdgeTypes;
/** The type of edge path to use for connection lines.
/**
* The type of edge path to use for connection lines.
*
* Although created edges can be of any type, React Flow needs to know what type of path to render for the connection line before the edge is created!
* @default ConnectionLineType.Bezier
*/
connectionLineType?: ConnectionLineType;
/** Styles to be applied to the connection line */
/** Styles to be applied to the connection line. */
connectionLineStyle?: CSSProperties;
/** React Component to be used as a connection line */
connectionLineComponent?: ConnectionLineComponent;
/** Styles to be applied to the container of the connection line */
/** React Component to be used as a connection line. */
connectionLineComponent?: ConnectionLineComponent<NodeType>;
/** Styles to be applied to the container of the connection line. */
connectionLineContainerStyle?: CSSProperties;
/** 'strict' connection mode will only allow you to connect source handles to target handles.
*
* 'loose' connection mode will allow you to connect handles of any type to one another.
/**
* A loose connection mode will allow you to connect handles with differing types, including
* source-to-source connections. However, it does not support target-to-target connections. Strict
* mode allows only connections between source handles and target handles.
* @default 'strict'
*/
connectionMode?: ConnectionMode;
/** Pressing down this key deletes all selected nodes & edges.
/**
* If set, pressing the key or chord will delete any selected nodes and edges. Passing an array
* represents multiple keys that can be pressed.
*
* For example, `["Delete", "Backspace"]` will delete selected elements when either key is pressed.
* @default 'Backspace'
*/
deleteKeyCode?: KeyCode | null;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If set, holding this key will let you click and drag to draw a selection box around multiple
* nodes and edges. Passing an array represents multiple keys that can be pressed.
*
* By setting this prop to null you can disable this functionality.
* @default 'Space'
* For example, `["Shift", "Meta"]` will allow you to draw a selection box when either key is
* pressed.
* @default 'Shift'
*/
selectionKeyCode?: KeyCode | null;
/** Select multiple elements with a selection box, without pressing down selectionKey */
/**
* Select multiple elements with a selection box, without pressing down `selectionKey`.
* @default false
*/
selectionOnDrag?: boolean;
/** When set to "partial", when the user creates a selection box by click and dragging nodes that are only partially in the box are still selected.
/**
* When set to `"partial"`, when the user creates a selection box by click and dragging nodes that
* are only partially in the box are still selected.
* @default 'full'
*/
selectionMode?: SelectionMode;
/** If a key is set, you can pan the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can pan the viewport while that key is held down even if `panOnScroll`
* is set to `false`.
*
* By setting this prop to null you can disable this functionality.
* By setting this prop to `null` you can disable this functionality.
* @default 'Space'
*/
panActivationKeyCode?: KeyCode | null;
/** Pressing down this key you can select multiple elements by clicking.
* @default 'Meta' for macOS, "Ctrl" for other systems
/**
* Pressing down this key you can select multiple elements by clicking.
* @default "Meta" for macOS, "Control" for other systems
*/
multiSelectionKeyCode?: KeyCode | null;
/** If a key is set, you can zoom the viewport while that key is held down even if panOnScroll is set to false.
/**
* If a key is set, you can zoom the viewport while that key is held down even if `panOnScroll`
* is set to `false`.
*
* By setting this prop to null you can disable this functionality.
* @default 'Meta' for macOS, "Ctrl" for other systems
* */
* By setting this prop to `null` you can disable this functionality.
* @default "Meta" for macOS, "Control" for other systems
*
*/
zoomActivationKeyCode?: KeyCode | null;
/** Set this prop to make the flow snap to the grid */
/** When enabled, nodes will snap to the grid when dragged. */
snapToGrid?: boolean;
/** Grid all nodes will snap to
/**
* If `snapToGrid` is enabled, this prop configures the grid that nodes will snap to.
* @example [20, 20]
*/
snapGrid?: SnapGrid;
/** You can enable this optimisation to instruct Svelte Flow to only render nodes and edges that would be visible in the viewport.
/**
* You can enable this optimisation to instruct React Flow to only render nodes and edges that would be visible in the viewport.
*
* This might improve performance when you have a large number of nodes and edges but also adds an overhead.
* @default false
*/
onlyRenderVisibleElements?: boolean;
/** Controls if all nodes should be draggable
/**
* Controls whether all nodes should be draggable or not. Individual nodes can override this
* setting by setting their `draggable` prop. If you want to use the mouse handlers on
* non-draggable nodes, you need to add the `"nopan"` class to those nodes.
* @default true
*/
nodesDraggable?: boolean;
/** Controls if all nodes should be connectable to each other
/**
* Controls whether all nodes should be connectable or not. Individual nodes can override this
* setting by setting their `connectable` prop.
* @default true
*/
nodesConnectable?: boolean;
/** Controls if all nodes should be focusable
/**
* When `true`, focus between nodes can be cycled with the `Tab` key and selected with the `Enter`
* key. This option can be overridden by individual nodes by setting their `focusable` prop.
* @default true
*/
nodesFocusable?: boolean;
/** Defines nodes relative position to its coordinates
/**
* The origin of the node to use when placing it in the flow or looking up its `x` and `y`
* position. An origin of `[0, 0]` means that a node's top left corner will be placed at the `x`
* and `y` position.
* @default [0, 0]
* @example
* [0, 0] // default, top left
* [0.5, 0.5] // center
* [1, 1] // bottom right
*/
nodeOrigin?: NodeOrigin;
/** Controls if all edges should be focusable
/**
* When `true`, focus between edges can be cycled with the `Tab` key and selected with the `Enter`
* key. This option can be overridden by individual edges by setting their `focusable` prop.
* @default true
*/
edgesFocusable?: boolean;
/** Controls if all edges should be updateable
/**
* Whether edges can be updated once they are created. When both this prop is `true` and an
* `onReconnect` handler is provided, the user can drag an existing edge to a new source or
* target. Individual edges can override this value with their reconnectable property.
* @default true
*/
edgesReconnectable?: boolean;
/** Controls if all elements should (nodes & edges) be selectable
/**
* When `true`, elements (nodes and edges) can be selected by clicking on them. This option can be
* overridden by individual elements by setting their `selectable` prop.
* @default true
*/
elementsSelectable?: boolean;
/** If true, nodes get selected on drag
/**
* If `true`, nodes get selected on drag.
* @default true
*/
selectNodesOnDrag?: boolean;
/** Enableing this prop allows users to pan the viewport by clicking and dragging.
/**
* Enabling this prop allows users to pan the viewport by clicking and dragging.
*
* You can also set this prop to an array of numbers to limit which mouse buttons can activate panning.
* @default true
* @example [0, 2] // allows panning with the left and right mouse buttons
* [0, 1, 2, 3, 4] // allows panning with all mouse buttons
*/
panOnDrag?: boolean | number[];
/** Minimum zoom level
/**
* Minimum zoom level.
* @default 0.5
*/
minZoom?: number;
/** Maximum zoom level
/**
* Maximum zoom level.
* @default 2
*/
maxZoom?: number;
/** Controlled viewport to be used instead of internal one */
/**
* When you pass a `viewport` prop, it's controlled, and you also need to pass `onViewportChange`
* to handle internal changes.
*/
viewport?: Viewport;
/** Sets the initial position and zoom of the viewport.
*
* If a default viewport is provided but fitView is enabled, the default viewport will be ignored.
/**
* Sets the initial position and zoom of the viewport. If a default viewport is provided but
* `fitView` is enabled, the default viewport will be ignored.
* @default { x: 0, y: 0, zoom: 1 }
* @example
* const initialViewport = {
* zoom: 0.5,
@@ -375,59 +484,103 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
*/
defaultViewport?: Viewport;
/**
* Gets called when the viewport changes.
* Used when working with a controlled viewport for updating the user viewport state.
*/
onViewportChange?: (viewport: Viewport) => void;
/** By default the viewport extends infinitely. You can use this prop to set a boundary.
/**
* By default, the viewport extends infinitely. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @default [[-∞, -∞], [+∞, +∞]]
* @example [[-1000, -10000], [1000, 1000]]
*/
translateExtent?: CoordinateExtent;
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
/**
* Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
* @default true
*/
preventScrolling?: boolean;
/** By default nodes can be placed on an infinite flow. You can use this prop to set a boundary.
/**
* By default, nodes can be placed on an infinite flow. You can use this prop to set a boundary.
*
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
* @example [[-1000, -10000], [1000, 1000]]
*/
nodeExtent?: CoordinateExtent;
/** Color of edge markers
* @example "#b1b1b7"
/**
* Color of edge markers.
* @default '#b1b1b7'
*/
defaultMarkerColor?: string;
/** Controls if the viewport should zoom by scrolling inside the container */
/**
* Controls if the viewport should zoom by scrolling inside the container.
* @default true
*/
zoomOnScroll?: boolean;
/** Controls if the viewport should zoom by pinching on a touch screen */
/**
* Controls if the viewport should zoom by pinching on a touch screen.
* @default true
*/
zoomOnPinch?: boolean;
/** Controls if the viewport should pan by scrolling inside the container
/**
* Controls if the viewport should pan by scrolling inside the container.
*
* Can be limited to a specific direction with panOnScrollMode
* Can be limited to a specific direction with `panOnScrollMode`.
* @default false
*/
panOnScroll?: boolean;
/** Controls how fast viewport should be panned on scroll.
/**
* Controls how fast viewport should be panned on scroll.
*
* Use togther with panOnScroll prop.
* Use together with `panOnScroll` prop.
* @default 0.5
*/
panOnScrollSpeed?: number;
/** This prop is used to limit the direction of panning when panOnScroll is enabled.
/**
* This prop is used to limit the direction of panning when `panOnScroll` is enabled.
*
* The "free" option allows panning in any direction.
* The `"free"` option allows panning in any direction.
* @default "free"
* @example "horizontal" | "vertical"
*/
panOnScrollMode?: PanOnScrollMode;
/** Controls if the viewport should zoom by double clicking somewhere on the flow */
/**
* Controls if the viewport should zoom by double-clicking somewhere on the flow.
* @default true
*/
zoomOnDoubleClick?: boolean;
/**
* The radius around an edge connection that can trigger an edge reconnection.
* @default 10
*/
reconnectRadius?: number;
/**
* If a node is draggable, clicking and dragging that node will move it around the canvas. Adding
* the `"nodrag"` class prevents this behavior and this prop allows you to change the name of that
* class.
* @default "nodrag"
*/
noDragClassName?: string;
/**
* Typically, scrolling the mouse wheel when the mouse is over the canvas will zoom the viewport.
* Adding the `"nowheel"` class to an element n the canvas will prevent this behavior and this prop
* allows you to change the name of that class.
* @default "nowheel"
*/
noWheelClassName?: string;
/**
* If an element in the canvas does not stop mouse events from propagating, clicking and dragging
* that element will pan the viewport. Adding the `"nopan"` class prevents this behavior and this
* prop allows you to change the name of that class.
* @default "nopan"
*/
noPanClassName?: string;
/** If set, initial viewport will show all nodes & edges */
/** When `true`, the flow will be zoomed and panned to fit all the nodes initially provided. */
fitView?: boolean;
/** Options to be used in combination with fitView
/**
* When you typically call `fitView` on a `ReactFlowInstance`, you can provide an object of
* options to customize its behavior. This prop lets you do the same for the initial `fitView`
* call.
* @example
* const fitViewOptions = {
* padding: 0.1,
@@ -439,85 +592,105 @@ export interface ReactFlowProps<NodeType extends Node = Node, EdgeType extends E
* };
*/
fitViewOptions?: FitViewOptions;
/**The connectOnClick option lets you click or tap on a source handle to start a connection
/**
* The `connectOnClick` option lets you click or tap on a source handle to start a connection
* and then click on a target handle to complete the connection.
*
* If you set this option to false, users will need to drag the connection line to the target
* If you set this option to `false`, users will need to drag the connection line to the target
* handle to create a connection.
* @default true
*/
connectOnClick?: boolean;
/** Set position of the attribution
/**
* By default, React Flow will render a small attribution in the bottom right corner of the flow.
*
* You can use this prop to change its position in case you want to place something else there.
* @default 'bottom-right'
* @example 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
*/
attributionPosition?: PanelPosition;
/** By default, we render a small attribution in the corner of your flows that links back to the project.
/**
* By default, we render a small attribution in the corner of your flows that links back to the project.
*
* Anyone is free to remove this attribution whether they're a Pro subscriber or not
* but we ask that you take a quick look at our {@link https://reactflow.dev/learn/troubleshooting/remove-attribution | removing attribution guide}
* before doing so.
*/
proOptions?: ProOptions;
/** Enabling this option will raise the z-index of nodes when they are selected.
/**
* Enabling this option will raise the z-index of nodes when they are selected.
* @default true
*/
elevateNodesOnSelect?: boolean;
/** Enabling this option will raise the z-index of edges when they are selected.
* @default true
/**
* Enabling this option will raise the z-index of edges when they are selected.
*/
elevateEdgesOnSelect?: boolean;
/**
* Can be set true if built-in keyboard controls should be disabled.
* You can use this prop to disable keyboard accessibility features such as selecting nodes or
* moving selected nodes with the arrow keys.
* @default false
*/
disableKeyboardA11y?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* When `true`, the viewport will pan automatically when the cursor moves to the edge of the
* viewport while dragging a node.
* @default true
*/
autoPanOnNodeDrag?: boolean;
/** You can enable this prop to automatically pan the viewport while dragging a node.
/**
* When `true`, the viewport will pan automatically when the cursor moves to the edge of the
* viewport while creating a connection.
* @default true
*/
autoPanOnConnect?: boolean;
/** The speed at which the viewport pans while dragging a node or a selection box.
/**
* The speed at which the viewport pans while dragging a node or a selection box.
* @default 15
*/
autoPanSpeed?: number;
/** You can enable this prop to automatically pan the viewport while making a new connection.
* @default true
/**
* The radius around a handle where you drop a connection line to create a new edge.
* @default 20
*/
connectionRadius?: number;
/** Ocassionally something may happen that causes Svelte Flow to throw an error.
/**
* Occasionally something may happen that causes React Flow to throw an error.
*
* Instead of exploding your application, we log a message to the console and then call this event handler.
* You might use it for additional logging or to show a message to the user.
*/
onError?: OnError;
/** This callback can be used to validate a new connection
/**
* This callback can be used to validate a new connection
*
* If you return false, the edge will not be added to your flow.
* If you have custom connection logic its preferred to use this callback over the isValidConnection prop on the handle component for performance reasons.
* @default (connection: Connection) => true
* If you return `false`, the edge will not be added to your flow.
* If you have custom connection logic its preferred to use this callback over the
* `isValidConnection` prop on the handle component for performance reasons.
*/
isValidConnection?: IsValidConnection<EdgeType>;
/** With a threshold greater than zero you can control the distinction between node drag and click events.
/**
* With a threshold greater than zero you can delay node drag events.
*
* If threshold equals 1, you need to drag the node 1 pixel before a drag event is fired.
*
* 1 is the default value, so clicks don't trigger drag events.
* @default 1
*/
nodeDragThreshold?: number;
/** Sets a fixed width for the flow */
/** Sets a fixed width for the flow. */
width?: number;
/** Sets a fixed height for the flow */
/** Sets a fixed height for the flow. */
height?: number;
/** Controls color scheme used for styling the flow
* @default 'system'
/**
* Controls color scheme used for styling the flow.
* @default 'light'
* @example 'system' | 'light' | 'dark'
*/
colorMode?: ColorMode;
/** If set true, some debug information will be logged to the console like which events are fired.
*
* @default undefined
/**
* If set `true`, some debug information will be logged to the console like which events are fired.
* @default false
*/
debug?: boolean;
}

View File

@@ -19,8 +19,18 @@ import type {
import { EdgeTypes, InternalNode, Node } from '.';
/**
* @inline
*/
export type EdgeLabelOptions = {
label?: string | ReactNode;
/**
* The label or custom element to render along the edge. This is commonly a text label or some
* custom controls.
*/
label?: ReactNode;
/**
* Custom styles to apply to the label.
*/
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
@@ -29,7 +39,8 @@ export type EdgeLabelOptions = {
};
/**
* The Edge type is mainly used for the `edges` that get passed to the ReactFlow component
* An `Edge` is the complete description with everything React Flow needs
* to know in order to render it.
* @public
*/
export type Edge<
@@ -39,6 +50,11 @@ export type Edge<
EdgeLabelOptions & {
style?: CSSProperties;
className?: string;
/**
* Determines whether the edge can be updated by dragging the source or target to a new node.
* This property will override the default set by the `edgesReconnectable` prop on the
* `<ReactFlow />` component.
*/
reconnectable?: boolean | HandleType;
focusable?: boolean;
};
@@ -91,17 +107,26 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
disableKeyboardA11y?: boolean;
};
/**
* Many properties on an [`Edge`](/api-reference/types/edge) are optional. When a new edge is created,
* the properties that are not provided will be filled in with the default values
* passed to the `defaultEdgeOptions` prop of the [`<ReactFlow />`](/api-reference/react-flow#defaultedgeoptions) component.
*/
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
export type EdgeTextProps = SVGAttributes<SVGElement> &
export type EdgeTextProps = Omit<SVGAttributes<SVGElement>, 'x' | 'y'> &
EdgeLabelOptions & {
/** The x position where the label should be rendered. */
x: number;
/** The y position where the label should be rendered. */
y: number;
};
/**
* Custom edge component props
* When you implement a custom edge it is wrapped in a component that enables some
* basic functionality. The `EdgeProps` type is the props that are passed to this.
* @public
* @expand
*/
export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
EdgeType,
@@ -121,22 +146,42 @@ export type EdgeProps<EdgeType extends Edge = Edge> = Pick<
/**
* BaseEdge component props
* @public
* @expand
*/
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd'> &
export type BaseEdgeProps = Omit<SVGAttributes<SVGPathElement>, 'd' | 'path' | 'markerStart' | 'markerEnd'> &
EdgeLabelOptions & {
/** Additional padding where interacting with an edge is still possible */
/**
* The width of the invisible area around the edge that the user can interact with. This is
* useful for making the edge easier to click or hover over.
* @default 20
*/
interactionWidth?: number;
/** The x position of edge label */
labelX?: number;
/** The y position of edge label */
labelY?: number;
/** SVG path of the edge */
/**
* The SVG path string that defines the edge. This should look something like
* `'M 0 0 L 100 100'` for a simple line. The utility functions like `getSimpleBezierEdge` can
* be used to generate this string for you.
*/
path: string;
/**
* The id of the SVG marker to use at the start of the edge. This should be defined in a
* `<defs>` element in a separate SVG document or element.
*/
markerStart?: string;
/**
* The id of the SVG marker to use at the end of the edge. This should be defined in a `<defs>`
* element in a separate SVG document or element.
*/
markerEnd?: string;
};
/**
* Helper type for edge components that get exported by the library
* @public
* @expand
*/
export type EdgeComponentProps = EdgePosition &
EdgeLabelOptions & {
@@ -156,39 +201,53 @@ export type EdgeComponentWithPathOptions<PathOptions> = EdgeComponentProps & {
/**
* BezierEdge component props
* @public
* @expand
*/
export type BezierEdgeProps = EdgeComponentWithPathOptions<BezierPathOptions>;
/**
* SmoothStepEdge component props
* @public
* @expand
*/
export type SmoothStepEdgeProps = EdgeComponentWithPathOptions<SmoothStepPathOptions>;
/**
* StepEdge component props
* @public
* @expand
*/
export type StepEdgeProps = EdgeComponentWithPathOptions<StepPathOptions>;
/**
* StraightEdge component props
* @public
* @expand
*/
export type StraightEdgeProps = Omit<EdgeComponentProps, 'sourcePosition' | 'targetPosition'>;
/**
* SimpleBezier component props
* @public
* @expand
*/
export type SimpleBezierEdgeProps = EdgeComponentProps;
export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newConnection: Connection) => void;
/**
* If you want to render a custom component for connection lines, you can set the
* `connectionLineComponent` prop on the [`<ReactFlow />`](/api-reference/react-flow#connection-connectionLineComponent)
* component. The `ConnectionLineComponentProps` are passed to your custom component.
*
* @public
*/
export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType;
/** The node the connection line originates from. */
fromNode: InternalNode<NodeType>;
/** The handle on the `fromNode` that the connection line originates from. */
fromHandle: Handle;
fromX: number;
fromY: number;
@@ -196,6 +255,10 @@ export type ConnectionLineComponentProps<NodeType extends Node = Node> = {
toY: number;
fromPosition: Position;
toPosition: Position;
/**
* If there is an `isValidConnection` callback, this prop will be set to `"valid"` or `"invalid"`
* based on the return value of that callback. Otherwise, it will be `null`.
*/
connectionStatus: 'valid' | 'invalid' | null;
toNode: InternalNode<NodeType> | null;
toHandle: Handle | null;

View File

@@ -18,11 +18,44 @@ import {
import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
/**
* This type can be used to type the `onNodesChange` function with a custom node type.
*
* @public
*
* @example
*
* ```ts
* const onNodesChange: OnNodesChange<MyNodeType> = useCallback((changes) => {
* setNodes((nodes) => applyNodeChanges(nodes, changes));
* },[]);
* ```
*/
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
/**
* This type can be used to type the `onEdgesChange` function with a custom edge type.
*
* @public
*
* @example
*
* ```ts
* const onEdgesChange: OnEdgesChange<MyEdgeType> = useCallback((changes) => {
* setEdges((edges) => applyEdgeChanges(edges, changes));
* },[]);
* ```
*/
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
export type OnNodesDelete<NodeType extends Node = Node> = (nodes: NodeType[]) => void;
export type OnEdgesDelete<EdgeType extends Edge = Edge> = (edges: EdgeType[]) => void;
/**
* This type can be used to type the `onDelete` function with a custom node and edge type.
*
* @public
*/
export type OnDelete<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (params: {
nodes: NodeType[];
edges: EdgeType[];
@@ -39,6 +72,7 @@ export type NodeTypes = Record<
}
>
>;
export type EdgeTypes = Record<
string,
ComponentType<
@@ -51,25 +85,38 @@ export type EdgeTypes = Record<
>
>;
export type UnselectNodesAndEdgesParams = {
nodes?: Node[];
edges?: Edge[];
export type UnselectNodesAndEdgesParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes?: NodeType[];
edges?: EdgeType[];
};
export type OnSelectionChangeParams = {
nodes: Node[];
edges: Edge[];
export type OnSelectionChangeParams<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: NodeType[];
edges: EdgeType[];
};
export type OnSelectionChangeFunc = (params: OnSelectionChangeParams) => void;
export type OnSelectionChangeFunc<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
params: OnSelectionChangeParams<NodeType, EdgeType>
) => void;
export type FitViewParams<NodeType extends Node = Node> = FitViewParamsBase<NodeType>;
/**
* When calling [`fitView`](/api-reference/types/react-flow-instance#fitview) these options
* can be used to customize the behaviour. For example, the `duration` option can be used to
* transform the viewport smoothly over a given amount of time.
*
* @public
*/
export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<NodeType>;
export type FitView = (fitViewOptions?: FitViewOptions) => Promise<boolean>;
export type FitView<NodeType extends Node = Node> = (fitViewOptions?: FitViewOptions<NodeType>) => Promise<boolean>;
export type OnInit<NodeType extends Node = Node, EdgeType extends Edge = Edge> = (
reactFlowInstance: ReactFlowInstance<NodeType, EdgeType>
) => void;
/**
* @inline
*/
export type ViewportHelperFunctions = {
/**
* Zooms viewport in by 1.2.
@@ -84,14 +131,15 @@ export type ViewportHelperFunctions = {
*/
zoomOut: ZoomInOut;
/**
* Sets the current zoom level.
* Zoom the viewport to a given zoom level. Passing in a `duration` will animate the viewport to
* the new zoom level.
*
* @param zoomLevel - the zoom level to set
* @param options.duration - optional duration. If set, a transition will be applied
*/
zoomTo: ZoomTo;
/**
* Returns the current zoom level.
* Get the current zoom level of the viewport.
*
* @returns current zoom as a number
*/
@@ -110,18 +158,8 @@ export type ViewportHelperFunctions = {
*/
getViewport: GetViewport;
/**
* Fits the view.
*
* @param options.padding - optional padding
* @param options.includeHiddenNodes - optional includeHiddenNodes
* @param options.minZoom - optional minZoom
* @param options.maxZoom - optional maxZoom
* @param options.duration - optional duration. If set, a transition will be applied
* @param options.nodes - optional nodes to fit the view to
*/
fitView: FitView;
/**
* Sets the center of the view to the given position.
* Center the viewport on a given position. Passing in a `duration` will animate the viewport to
* the new position.
*
* @param x - x position
* @param y - y position
@@ -129,14 +167,17 @@ export type ViewportHelperFunctions = {
*/
setCenter: SetCenter;
/**
* Fits the view to the given bounds .
* A low-level utility function to fit the viewport to a given rectangle. By passing in a
* `duration`, the viewport will animate from its current position to the new position. The
* `padding` option can be used to add space around the bounds.
*
* @param bounds - the bounds ({ x: number, y: number, width: number, height: number }) to fit the view to
* @param options.padding - optional padding
*/
fitBounds: FitBounds;
/**
* Converts a screen / client position to a flow position.
* With this function you can translate a screen pixel position to a flow position. It is useful
* for implementing drag and drop from a sidebar for example.
*
* @param clientPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @param options.snapToGrid - if true, the converted position will be snapped to the grid
@@ -147,7 +188,7 @@ export type ViewportHelperFunctions = {
*/
screenToFlowPosition: (clientPosition: XYPosition, options?: { snapToGrid: boolean }) => XYPosition;
/**
* Converts a flow position to a screen / client position.
* Translate a position inside the flow's canvas to a screen pixel position.
*
* @param flowPosition - the screen / client position. When you are working with events you can use event.clientX and event.clientY
* @returns position as { x: number, y: number }

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
import type { Node, Edge, ViewportHelperFunctions, InternalNode, FitView } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
nodes: NodeType[];
@@ -13,6 +13,9 @@ export type DeleteElementsOptions = {
edges?: (Edge | { id: Edge['id'] })[];
};
/**
* @inline
*/
export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
/**
* Returns nodes.
@@ -21,13 +24,17 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/
getNodes: () => NodeType[];
/**
* Sets nodes.
* Set your nodes array to something else by either overwriting it with a new array or by passing
* in a function to update the existing array. If using a function, it is important to make sure a
* new array is returned instead of mutating the existing array. Calling this function will
* trigger the `onNodesChange` handler in a controlled flow.
*
* @param payload - the nodes to set or a function that receives the current nodes and returns the new nodes
*/
setNodes: (payload: NodeType[] | ((nodes: NodeType[]) => NodeType[])) => void;
/**
* Adds nodes.
* Add one or many nodes to your existing nodes array. Calling this function will trigger the
* `onNodesChange` handler in a controlled flow.
*
* @param payload - the nodes to add
*/
@@ -53,13 +60,17 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/
getEdges: () => EdgeType[];
/**
* Sets edges.
* Set your edges array to something else by either overwriting it with a new array or by passing
* in a function to update the existing array. If using a function, it is important to make sure a
* new array is returned instead of mutating the existing array. Calling this function will
* trigger the `onEdgesChange` handler in a controlled flow.
*
* @param payload - the edges to set or a function that receives the current edges and returns the new edges
*/
setEdges: (payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])) => void;
/**
* Adds edges.
* Add one or many edges to your existing edges array. Calling this function will trigger the
* `onEdgesChange` handler in a controlled flow.
*
* @param payload - the edges to add
*/
@@ -90,7 +101,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
deletedEdges: Edge[];
}>;
/**
* Returns all nodes that intersect with the given node or rect.
* Find all the nodes currently intersecting with a given node or rectangle. The `partially`
* parameter can be set to `true` to include nodes that are only partially intersecting.
*
* @param node - the node or rect to check for intersections
* @param partially - if true, the node is considered to be intersecting if it partially overlaps with the passed node or rect
@@ -104,7 +116,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodes?: NodeType[]
) => NodeType[];
/**
* Checks if the given node or rect intersects with the passed rect.
* Determine if a given node or rectangle is intersecting with another rectangle. The `partially`
* parameter can be set to true return `true` even if the node is only partially intersecting.
*
* @param node - the node or rect to check for intersections
* @param area - the rect to check for intersections
@@ -182,7 +195,8 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
*/
getNodesBounds: (nodes: (NodeType | InternalNode | string)[]) => Rect;
/**
* Gets all connections for a given handle belonging to a specific node.
* Get all the connections of a handle belonging to a specific node. The type parameter be either
* `'source'` or `'target'`.
* @deprecated
* @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)
@@ -200,7 +214,6 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
}) => HandleConnection[];
/**
* Gets all connections to a node. Can be filtered by handle type and id.
* @deprecated use `getNodeConnections` instead
* @param type - handle type 'source' or 'target'
* @param handleId - 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
@@ -215,12 +228,34 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
nodeId: string;
handleId?: string | null;
}) => NodeConnection[];
// /**
// * Fits the view.
// *
// * @param options.padding - optional padding
// * @param options.includeHiddenNodes - optional includeHiddenNodes
// * @param options.minZoom - optional minZoom
// * @param options.maxZoom - optional maxZoom
// * @param options.duration - optional duration. If set, a transition will be applied
// * @param options.nodes - optional nodes to fit the view to
// */
fitView: FitView<NodeType>;
};
/**
* The `ReactFlowInstance` provides a collection of methods to query and manipulate
* the internal state of your flow. You can get an instance by using the
* [`useReactFlow`](/api-reference/hooks/use-react-flow) hook or attaching a listener
* to the [`onInit`](/api-reference/react-flow#event-oninit) event.
*
* @public
*/
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
NodeType,
EdgeType
> &
Omit<ViewportHelperFunctions, 'initialized'> & {
ViewportHelperFunctions & {
/**
* React Flow needs to mount the viewport to the DOM and initialize its zoom and pan behavior.
* This property tells you when viewport is initialized.
*/
viewportInitialized: boolean;
};

View File

@@ -4,7 +4,10 @@ import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, I
import { NodeTypes } from './general';
/**
* The node data structure that gets used for the nodes prop.
* The `Node` type represents everything React Flow needs to know about a given node.
* Whenever you want to update a certain attribute of a node, you need to create a new
* node object.
*
* @public
*/
export type Node<
@@ -18,9 +21,10 @@ export type Node<
};
/**
* The node data structure that gets used for internal nodes.
* There are some data structures added under node.internal
* that are needed for tracking some properties
* The `InternalNode` type is identical to the base [`Node`](/api-references/types/node)
* type but is extended with some additional properties used internally.
* Some functions and callbacks that return nodes may return an `InternalNode`.
*
* @public
*/
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
@@ -56,8 +60,46 @@ export type NodeWrapperProps<NodeType extends Node> = {
nodeClickDistance?: number;
};
/**
* The `BuiltInNode` type represents the built-in node types that are available in React Flow.
* You can use this type to extend your custom node type if you still want ot use the built-in ones.
*
* @public
* @example
* ```ts
* type CustomNode = Node<{ value: number }, 'custom'>;
* type MyAppNode = CustomNode | BuiltInNode;
* ```
*/
export type BuiltInNode =
| Node<{ label: string }, 'input' | 'output' | 'default'>
| Node<Record<string, never>, 'group'>;
/**
* When you implement a [custom node](/learn/customization/custom-nodes) it is
* wrapped in a component that enables basic functionality like selection and
* dragging. Your custom node receives `NodeProps` as props.
*
* @public
* @example
* ```tsx
*import { useState } from 'react';
*import { NodeProps, Node } from '@xyflow/react';
*
*export type CounterNode = Node<{ initialCount?: number }, 'counter'>;
*
*export default function CounterNode(props: NodeProps<CounterNode>) {
* const [count, setCount] = useState(props.data?.initialCount ?? 0);
*
* return (
* <div>
* <p>Count: {count}</p>
* <button className="nodrag" onClick={() => setCount(count + 1)}>
* Increment
* </button>
* </div>
* );
*}
*```
*/
export type NodeProps<NodeType extends Node = Node> = NodePropsBase<NodeType>;

View File

@@ -119,9 +119,9 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
fitViewOnInit: boolean;
fitViewDone: boolean;
fitViewOnInitOptions: FitViewOptions | undefined;
fitViewQueued: boolean;
fitViewOptions: FitViewOptions | undefined;
fitViewResolver: PromiseWithResolvers<boolean> | null;
onNodesDelete?: OnNodesDelete<NodeType>;
onEdgesDelete?: OnEdgesDelete<EdgeType>;
@@ -134,7 +134,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
onViewportChangeEnd?: OnViewportChange;
onBeforeDelete?: OnBeforeDelete<NodeType, EdgeType>;
onSelectionChangeHandlers: OnSelectionChangeFunc[];
onSelectionChangeHandlers: OnSelectionChangeFunc<NodeType, EdgeType>[];
ariaLiveMessage: string;
autoPanOnConnect: boolean;
@@ -155,7 +155,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>, params?: { triggerFitView: boolean }) => void;
updateNodePositions: UpdateNodePositions;
resetSelectedElements: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams<NodeType, EdgeType>) => void;
addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void;
@@ -168,8 +168,6 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
panBy: PanBy;
fitView: (options?: FitViewOptions) => Promise<boolean>;
fitViewSync: (options?: FitViewOptions) => boolean;
setPaneClickDistance: (distance: number) => void;
};

View File

@@ -11,13 +11,17 @@ import {
} from '@xyflow/system';
import type { Node, Edge, InternalNode } from '../types';
// This function applies changes to nodes or edges that are triggered by React Flow internally.
// When you drag a node for example, React Flow will send a position change update.
// This function then applies the changes and returns the updated elements.
/*
* This function applies changes to nodes or edges that are triggered by React Flow internally.
* When you drag a node for example, React Flow will send a position change update.
* This function then applies the changes and returns the updated elements.
*/
function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElements: any[] = [];
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
/*
* 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[] = [];
@@ -26,15 +30,19 @@ function applyChanges(changes: any[], elements: any[]): any[] {
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
// the same element, it's going to be removed anyway!
/*
* For a 'remove' change we can safely ignore any other changes queued for
* the same element, it's going to be removed anyway!
*/
changesMap.set(change.id, [change]);
} else {
const elementChanges = changesMap.get(change.id);
if (elementChanges) {
// If we have some changes queued already, we can do a mutable update of
// that array and save ourselves some copying.
/*
* If we have some changes queued already, we can do a mutable update of
* that array and save ourselves some copying.
*/
elementChanges.push(change);
} else {
changesMap.set(change.id, [change]);
@@ -45,8 +53,10 @@ function applyChanges(changes: any[], elements: any[]): any[] {
for (const element of elements) {
const changes = changesMap.get(element.id);
// When there are no changes for an element we can just push it unmodified,
// no need to copy it.
/*
* When there are no changes for an element we can just push it unmodified,
* no need to copy it.
*/
if (!changes) {
updatedElements.push(element);
continue;
@@ -62,9 +72,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
continue;
}
// For other types of changes, we want to start with a shallow copy of the
// object so React knows this element has changed. Sequential changes will
/// each _mutate_ this object, so there's only ever one copy.
/**
* For other types of changes, we want to start with a shallow copy of the
* object so React knows this element has changed. Sequential changes will
* each _mutate_ this object, so there's only ever one copy.
*/
const updatedElement = { ...element };
for (const change of changes) {
@@ -74,8 +86,10 @@ 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
/*
* 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) {
@@ -133,22 +147,33 @@ function applyChange(change: any, element: any): any {
/**
* Drop in function that applies node changes to an array of nodes.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param nodes - Array of nodes to apply the changes to
* @returns Array of updated nodes
* @param changes - Array of changes to apply.
* @param nodes - Array of nodes to apply the changes to.
* @returns Array of updated nodes.
* @example
* const onNodesChange = useCallback(
(changes) => {
setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
},
[setNodes],
);
return (
<ReactFLow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
);
*```tsx
*import { useState, useCallback } from 'react';
*import { ReactFlow, applyNodeChanges, type Node, type Edge, type OnNodesChange } from '@xyflow/react';
*
*export default function Flow() {
* const [nodes, setNodes] = useState<Node[]>([]);
* const [edges, setEdges] = useState<Edge[]>([]);
* const onNodesChange: OnNodesChange = useCallback(
* (changes) => {
* setNodes((oldNodes) => applyNodeChanges(changes, oldNodes));
* },
* [setNodes],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} />
* );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link NodeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/
export function applyNodeChanges<NodeType extends Node = Node>(
changes: NodeChange<NodeType>[],
@@ -160,22 +185,33 @@ export function applyNodeChanges<NodeType extends Node = Node>(
/**
* Drop in function that applies edge changes to an array of edges.
* @public
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange} that describes how to update the edges of your flow in some way.
If you don't need any custom behaviour, this util can be used to take an array of these changes and apply them to your edges.
* @param changes - Array of changes to apply
* @param edges - Array of edge to apply the changes to
* @returns Array of updated edges
* @param changes - Array of changes to apply.
* @param edges - Array of edge to apply the changes to.
* @returns Array of updated edges.
* @example
* ```tsx
*import { useState, useCallback } from 'react';
*import { ReactFlow, applyEdgeChanges } from '@xyflow/react';
*
*export default function Flow() {
* const [nodes, setNodes] = useState([]);
* const [edges, setEdges] = useState([]);
* const onEdgesChange = useCallback(
(changes) => {
setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
},
[setEdges],
);
return (
<ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
);
* (changes) => {
* setEdges((oldEdges) => applyEdgeChanges(changes, oldEdges));
* },
* [setEdges],
* );
*
* return (
* <ReactFlow nodes={nodes} edges={edges} onEdgesChange={onEdgesChange} />
* );
*}
*```
* @remarks Various events on the <ReactFlow /> component can produce an {@link EdgeChange}
* that describes how to update the edges of your flow in some way.
* If you don't need any custom behaviour, this util can be used to take an array
* of these changes and apply them to your edges.
*/
export function applyEdgeChanges<EdgeType extends Edge = Edge>(
changes: EdgeChange<EdgeType>[],
@@ -205,9 +241,11 @@ export function getSelectionChanges(
// we don't want to set all items to selected=false on the first selection
if (!(item.selected === undefined && !willBeSelected) && item.selected !== willBeSelected) {
if (mutateItem) {
// this hack is needed for nodes. When the user dragged a node, it's selected.
// When another node gets dragged, we need to deselect the previous one,
// in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
/*
* this hack is needed for nodes. When the user dragged a node, it's selected.
* When another node gets dragged, we need to deselect the previous one,
* in order to have only one selected node at a time - the onNodesChange callback comes too late here :/
*/
item.selected = willBeSelected;
}
changes.push(createSelectionChange(item.id, willBeSelected));

View File

@@ -1,29 +1,57 @@
import { type Ref, type RefAttributes, forwardRef } from 'react';
import { type Ref, type RefAttributes, forwardRef, JSX } from 'react';
import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '../types';
/**
* Test whether an object is useable as a Node
* Test whether an object is usable as an [`Node`](/api-reference/types/node).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Node`](/api-reference/types/node) if it returns `true`.
*
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Node
* @param element - The element to test.
* @returns Tests whether the provided value can be used as a `Node`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Node` if it returns
* `true`.
*
* @example
* ```js
*import { isNode } from '@xyflow/react';
*
*if (isNode(node)) {
* // ...
*}
*```
*/
export const isNode = <NodeType extends Node = Node>(element: unknown): element is NodeType =>
isNodeBase<NodeType>(element);
/**
* Test whether an object is useable as an Edge
* Test whether an object is usable as an [`Edge`](/api-reference/types/edge).
* In TypeScript this is a type guard that will narrow the type of whatever you pass in to
* [`Edge`](/api-reference/types/edge) if it returns `true`.
*
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test
* @returns A boolean indicating whether the element is an Edge
* @returns Tests whether the provided value can be used as an `Edge`. If you're using TypeScript,
* this function acts as a type guard and will narrow the type of the value to `Edge` if it returns
* `true`.
*
* @example
* ```js
*import { isEdge } from '@xyflow/react';
*
*if (isEdge(edge)) {
* // ...
*}
*```
*/
export const isEdge = <EdgeType extends Edge = Edge>(element: unknown): element is EdgeType =>
isEdgeBase<EdgeType>(element);
// eslint-disable-next-line @typescript-eslint/ban-types
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export function fixedForwardRef<T, P = {}>(
render: (props: P, ref: Ref<T>) => JSX.Element
): (props: P & RefAttributes<T>) => JSX.Element {

View File

@@ -1,5 +1,60 @@
# @xyflow/svelte
## 0.1.35
### Patch Changes
- [#5158](https://github.com/xyflow/xyflow/pull/5158) [`06696060`](https://github.com/xyflow/xyflow/commit/0669606050bb2138a44a1591176ac8e16afeb0f1) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Fix typo in TSDoc comments `React Flow` -> `Svelte Flow`
- Updated dependencies [[`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e), [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd), [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03), [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a), [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b)]:
- @xyflow/system@0.0.55
## 0.1.34
### Patch Changes
- [#5139](https://github.com/xyflow/xyflow/pull/5139) [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Use `rgba` for colors with opacity instead of `rgb` for `MiniMap` mask color
- Updated dependencies [[`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a), [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be), [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe), [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654), [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761), [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e)]:
- @xyflow/system@0.0.54
## 0.1.33
### Patch Changes
- [#5124](https://github.com/xyflow/xyflow/pull/5124) [`b76f7f9e`](https://github.com/xyflow/xyflow/commit/b76f7f9eb4841f139b1468b8eda0430ddd19a1ae) Thanks [@bjornosal](https://github.com/bjornosal)! - Export NodeConnection type
## 0.1.32
### Patch Changes
- [#5059](https://github.com/xyflow/xyflow/pull/5059) [`065ff89d`](https://github.com/xyflow/xyflow/commit/065ff89d10488f9c76c56870511e45eaed299778) Thanks [@bcakmakoglu](https://github.com/bcakmakoglu)! - Prevent onPaneClick when connection is in progress. Closes [#5057](https://github.com/xyflow/xyflow/issues/5057)
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5093](https://github.com/xyflow/xyflow/pull/5093) [`65825e89`](https://github.com/xyflow/xyflow/commit/65825e89a6e2e7591087eb41ac89da4da7095f8f) Thanks [@moklick](https://github.com/moklick)! - Hidden nodes are not displayed in the mini map anymore
- Updated dependencies [[`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293), [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe), [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1)]:
- @xyflow/system@0.0.53
## 0.1.31
### Patch Changes
- [#5019](https://github.com/xyflow/xyflow/pull/5019) [`3e80317c`](https://github.com/xyflow/xyflow/commit/3e80317cf6da0e9fdc111c3ade88f2a88a10dbd6) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Add `"./package.json" to the `exports` field so that users can import it
- Updated dependencies [[`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90)]:
- @xyflow/system@0.0.52
## 0.1.30
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- Updated dependencies [[`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919), [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234), [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7)]:
- @xyflow/system@0.0.51
## 0.1.29
### Patch Changes

View File

@@ -34,6 +34,7 @@
"type": "module",
"module": "./dist/lib/index.js",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/lib/index.d.ts",
"svelte": "./dist/lib/index.js",
@@ -54,32 +55,32 @@
"@xyflow/system": "workspace:*"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/kit": "^2.16.1",
"@sveltejs/package": "^2.3.9",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.20.4",
"@sveltejs/package": "^2.3.10",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@typescript-eslint/eslint-plugin": "^8.22.0",
"@typescript-eslint/parser": "^8.22.0",
"autoprefixer": "^10.4.20",
"@typescript-eslint/eslint-plugin": "^8.29.1",
"@typescript-eslint/parser": "^8.29.1",
"autoprefixer": "^10.4.21",
"cssnano": "^7.0.6",
"dotenv": "^16.4.7",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"postcss": "^8.5.1",
"postcss-cli": "^11.0.0",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-svelte": "^3.5.1",
"postcss": "^8.5.3",
"postcss-cli": "^11.0.1",
"postcss-combine-duplicated-selectors": "^10.0.3",
"postcss-import": "^16.1.0",
"postcss-nested": "^7.0.2",
"postcss-rename": "^0.6.1",
"prettier": "^3.4.2",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.19.5",
"svelte-check": "^4.1.4",
"svelte-eslint-parser": "^0.43.0",
"svelte": "^5.25.8",
"svelte-check": "^4.1.5",
"svelte-eslint-parser": "^1.1.2",
"svelte-preprocess": "^6.0.3",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
"typescript": "^5.8.3"
},
"peerDependencies": {
"svelte": "^5.16.0"

View File

@@ -75,7 +75,8 @@
function onClick(event: MouseEvent) {
// We prevent click events when the user let go of the selectionKey during a selection
if (selectionInProgress) {
// We also prevent click events when a connection is in progress
if (selectionInProgress || store.connection.inProgress) {
selectionInProgress = false;
return;
}

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import { onMount } from 'svelte';
import { PanOnScrollMode, type PanZoomInstance, type Transform } from '@xyflow/system';
import zoom from '$lib/actions/zoom';
@@ -24,15 +23,8 @@
let panOnDragActive = $derived(store.panActivationKeyPressed || panOnDrag);
let panOnScrollActive = $derived(store.panActivationKeyPressed || panOnScroll);
const onTransformChange = (transform: Transform) =>
(store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] });
// We extract the initial value by destructuring
const { viewport: initialViewport } = store;
onMount(() => {
store.viewportInitialized = true;
});
</script>
<div
@@ -66,7 +58,9 @@
translateExtent: store.translateExtent,
lib: 'svelte',
paneClickDistance,
onTransformChange
onTransformChange: (transform: Transform) => {
store.viewport = { x: transform[0], y: transform[1], zoom: transform[2] };
}
}}
>
{@render children()}

View File

@@ -107,7 +107,8 @@ export {
type ResizeParams,
type ResizeParamsWithDirection,
type ResizeDragEvent,
type IsValidConnection
type IsValidConnection,
type NodeConnection
} from '@xyflow/system';
// system utils

View File

@@ -63,7 +63,10 @@
});
let boundingRect = $derived(
store.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(store.nodeLookup), viewBB)
? getBoundsOfRects(
getInternalNodesBounds(store.nodeLookup, { filter: (n) => !n.hidden }),
viewBB
)
: viewBB
);
let scaledWidth = $derived(boundingRect.width / width);

View File

@@ -1,5 +1,4 @@
import {
fitView as fitViewSystem,
panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil,
@@ -13,9 +12,7 @@ import {
type CoordinateExtent,
type UpdateConnection,
type ConnectionState,
getFitViewNodes,
updateAbsolutePositions,
getDimensions
updateAbsolutePositions
} from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
@@ -72,14 +69,8 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
nodeExtent: store.nodeExtent
});
if (!store.fitViewOnInitDone && store.fitViewOnInit) {
const fitViewOnInitDone = fitViewSync({
...store.fitViewOptions,
nodes: store.fitViewOptions?.nodes
});
if (fitViewOnInitDone) {
store.fitViewOnInitDone = fitViewOnInitDone;
}
if (store.fitViewQueued) {
store.resolveFitView();
}
const newNodes = new Map<string, Node>();
@@ -120,52 +111,41 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
}
function fitView(options?: FitViewOptions) {
const panZoom = store.panZoom;
const domNode = store.domNode;
// const panZoom = store.panZoom;
// const domNode = store.domNode;
if (!panZoom || !domNode) {
return Promise.resolve(false);
}
// if (!panZoom || !domNode) {
// return Promise.resolve(false);
// }
const { width, height } = getDimensions(domNode);
// const { width, height } = getDimensions(domNode);
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
// const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
return fitViewSystem(
{
nodes: fitViewNodes,
width,
height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
);
}
// return fitViewSystem(
// {
// nodes: fitViewNodes,
// width,
// height,
// minZoom: store.minZoom,
// maxZoom: store.maxZoom,
// panZoom
// },
// options
// );e3
// We either create a new Promise or reuse the existing one
// Even if fitView is called multiple times in a row, we only end up with a single Promise
const fitViewResolver = store.fitViewResolver ?? Promise.withResolvers<boolean>();
function fitViewSync(options?: FitViewOptions) {
const panZoom = store.panZoom;
// We schedule a fitView by setting fitViewQueued and triggering a setNodes
store.fitViewQueued = true;
store.fitViewOptions = options;
store.fitViewResolver = fitViewResolver;
if (!panZoom) {
return false;
}
// We need to update the nodes so that adoptUserNodes is triggered
store.nodes = [...store.nodes];
const fitViewNodes = getFitViewNodes(store.nodeLookup, options);
fitViewSystem(
{
nodes: fitViewNodes,
width: store.width,
height: store.height,
minZoom: store.minZoom,
maxZoom: store.maxZoom,
panZoom
},
options
);
return fitViewNodes.size > 0;
return fitViewResolver.promise;
}
function zoomBy(factor: number, options?: ViewportHelperFunctionOptions) {
@@ -350,10 +330,11 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
}
function reset() {
store.fitViewOnInitDone = false;
store.selectionRect = null;
store.selectionRectMode = null;
store.resetStoreValues();
unselectNodesAndEdges();
cancelConnection();
}
@@ -366,7 +347,7 @@ export function createStore(signals: StoreSignals): SvelteFlowStore {
updateNodeInternals,
zoomIn,
zoomOut,
fitView: (options?: FitViewOptions) => fitView(options),
fitView,
setMinZoom,
setMaxZoom,
setTranslateExtent,

View File

@@ -27,7 +27,8 @@ import {
type ParentLookup,
pointToRendererPoint,
type ColorModeClass,
type Transform
type Transform,
fitViewport
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +52,6 @@ import type {
OnBeforeDelete,
IsValidConnection,
Edge,
Node,
EdgeLayouted,
InternalNode
} from '$lib/types';
@@ -78,14 +78,54 @@ export const getInitialStore = (signals: StoreSignals) => {
// We use a class here, because Svelte adds getters & setter for us.
// Inline classes have some performance implications but we just call it once (max twice).
class SvelteFlowStore {
_nodes: Node[] = $derived.by(() => {
adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
flowId: string = $derived(signals.props.id ?? '1');
domNode = $state<HTMLDivElement | null>(null);
panZoom: PanZoomInstance | null = $state(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
nodesInitialized: boolean = $derived.by(() => {
const nodesInitialized = adoptUserNodes(signals.nodes, this.nodeLookup, this.parentLookup, {
nodeExtent: this.nodeExtent,
nodeOrigin: this.nodeOrigin,
elevateNodesOnSelect: signals.props.elevateNodesOnSelect ?? true,
checkEquality: true
});
return signals.nodes;
if (this.fitViewQueued && nodesInitialized) {
if (this.fitViewOptions?.duration) {
this.resolveFitView();
} else {
/**
* When no duration is set, viewport is set immediately which prevents an update
* I do not understand why, however we are setting state in a derived which is a no-go
*/
queueMicrotask(() => {
this.resolveFitView();
});
}
}
return nodesInitialized;
});
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $derived(this.panZoom !== null);
// TODO: Figure out initialized
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
}
return initialized;
});
_edges: Edge[] = $derived.by(() => {
@@ -94,7 +134,9 @@ export const getInitialStore = (signals: StoreSignals) => {
});
get nodes() {
return this._nodes;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
this.nodesInitialized;
return signals.nodes;
}
set nodes(nodes) {
signals.nodes = nodes;
@@ -116,7 +158,7 @@ export const getInitialStore = (signals: StoreSignals) => {
const {
// We need to access this._nodes to trigger on changes
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_nodes,
nodes,
_edges: edges,
_prevVisibleEdges: previousEdges,
nodeLookup,
@@ -174,11 +216,6 @@ export const getInitialStore = (signals: StoreSignals) => {
signals.elementsSelectable = value;
}
domNode = $state<HTMLDivElement | null>(null);
width = $state<number>(signals.width ?? 0);
height = $state<number>(signals.height ?? 0);
flowId: string = $derived(signals.props.id ?? '1');
minZoom: number = $derived(signals.props.minZoom ?? 0.5);
maxZoom: number = $derived(signals.props.maxZoom ?? 2);
@@ -192,11 +229,10 @@ export const getInitialStore = (signals: StoreSignals) => {
autoPanOnNodeDrag: boolean = $derived(signals.props.autoPanOnNodeDrag ?? true);
autoPanOnConnect: boolean = $derived(signals.props.autoPanOnConnect ?? true);
fitViewOnInitDone: boolean = $state(false);
fitViewOnInit: boolean = $derived(signals.props.fitView ?? false);
fitViewOptions: FitViewOptions | undefined = $derived(signals.props.fitViewOptions);
fitViewQueued: boolean = signals.props.fitView ?? false;
fitViewOptions: FitViewOptions | undefined = signals.props.fitViewOptions;
fitViewResolver: PromiseWithResolvers<boolean> | null = null;
panZoom: PanZoomInstance | null = $state(null);
snapGrid: SnapGrid | null = $derived(signals.props.snapGrid ?? null);
dragging: boolean = $state(false);
@@ -215,15 +251,16 @@ export const getInitialStore = (signals: StoreSignals) => {
// _viewport is the internal viewport.
// when binding to viewport, we operate on signals.viewport instead
initial: boolean = true;
_viewport: Viewport = $state(signals.props.initialViewport ?? { x: 0, y: 0, zoom: 1 });
get viewport() {
return signals.viewport ?? this._viewport;
}
set viewport(viewport: Viewport) {
set viewport(newViewport: Viewport) {
if (signals.viewport) {
signals.viewport = viewport;
signals.viewport = newViewport;
}
this._viewport = viewport;
this._viewport = newViewport;
}
// _connection is viewport independent and originating from XYHandle
@@ -271,25 +308,32 @@ export const getInitialStore = (signals: StoreSignals) => {
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
nodesInitialized: boolean = $state(false);
edgesInitialized: boolean = $state(false);
viewportInitialized: boolean = $state(false);
_initialNodesLength: number = signals.nodes?.length ?? 0;
_initialEdgesLength: number = signals.edges?.length ?? 0;
initialized: boolean = $derived.by(() => {
let initialized = false;
// if it hasn't been initialised check if it's now
if (this._initialNodesLength === 0) {
initialized = this.viewportInitialized;
} else if (this._initialEdgesLength === 0) {
initialized = this.viewportInitialized && this.nodesInitialized;
} else {
initialized = this.viewportInitialized && this.nodesInitialized && this.edgesInitialized;
resolveFitView = async () => {
if (!this.panZoom) {
return;
}
return initialized;
});
await fitViewport(
{
nodes: this.nodeLookup,
width: this.width,
height: this.height,
panZoom: this.panZoom,
minZoom: this.minZoom,
maxZoom: this.maxZoom
},
this.fitViewOptions
);
this.fitViewResolver?.resolve(true);
/**
* wait for the fitViewport to resolve before deleting the resolver,
* we want to reuse the old resolver if the user calls fitView again in the mean time
*/
this.fitViewQueued = false;
this.fitViewOptions = undefined;
this.fitViewResolver = null;
};
_prefersDark = new MediaQuery(
'(prefers-color-scheme: dark)',
@@ -328,7 +372,6 @@ export const getInitialStore = (signals: StoreSignals) => {
// Only way to check if an object is a proxy
// is to see if is failes to perform a structured clone
// TODO: is $state.raw really nessessary?
function warnIfDeeplyReactive(array: unknown[] | undefined, name: string) {
try {
if (array && array.length > 0) {

View File

@@ -12,7 +12,9 @@ import type { Node } from '$lib/types';
import type { ClassValue } from 'svelte/elements';
/**
* The Edge type is mainly used for the `edges` that get passed to the SvelteFlow component.
* An `Edge` is the complete description with everything Svelte Flow needs to know in order to
* render it.
* @public
*/
export type Edge<
EdgeData extends Record<string, unknown> = Record<string, unknown>,

View File

@@ -3,7 +3,7 @@ import { isNodeBase, isEdgeBase } from '@xyflow/system';
import type { Edge, Node } from '$lib/types';
/**
* Test whether an object is useable as a Node
* Test whether an object is usable as a Node
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Node if it returns true
* @param element - The element to test
@@ -13,7 +13,7 @@ export const isNode = <NodeType extends Node = Node>(element: unknown): element
isNodeBase<NodeType>(element);
/**
* Test whether an object is useable as an Edge
* Test whether an object is usable as an Edge
* @public
* @remarks In TypeScript this is a type guard that will narrow the type of whatever you pass in to Edge if it returns true
* @param element - The element to test

View File

@@ -1,5 +1,61 @@
# @xyflow/system
## 0.0.55
### Patch Changes
- [#5156](https://github.com/xyflow/xyflow/pull/5156) [`02a3b746`](https://github.com/xyflow/xyflow/commit/02a3b74645799a3f0ce670b69365fa86ecb0616e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetSmoothStepPathParams` and `getSmoothStepPath` function
- [#5168](https://github.com/xyflow/xyflow/pull/5168) [`cbe305e1`](https://github.com/xyflow/xyflow/commit/cbe305e15a5c5d3b92583e0ec12364b2509f49bd) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `getOutgoers`, `getIncomers` and `type GetNodesBoundsParams`
- [#5169](https://github.com/xyflow/xyflow/pull/5169) [`1f671bd4`](https://github.com/xyflow/xyflow/commit/1f671bd48f06230da841fdd1d7a312413ef16d03) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type ReconnectEdgeOptions` and `getViewportForBounds`
- [#5155](https://github.com/xyflow/xyflow/pull/5155) [`aaebc462`](https://github.com/xyflow/xyflow/commit/aaebc462951ded8e91374c3e084d77af5ed7380a) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetStraightPathParams` and `getStraightPath` function
- [#5157](https://github.com/xyflow/xyflow/pull/5157) [`6ec942fc`](https://github.com/xyflow/xyflow/commit/6ec942fc6501f81009c278cc995764bef3e8d03b) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `type GetBezierPathParams` and `getBezierPath` function
## 0.0.54
### Patch Changes
- [#5147](https://github.com/xyflow/xyflow/pull/5147) [`f819005b`](https://github.com/xyflow/xyflow/commit/f819005be362d044b16ce4c0b85432f3f300a13a) Thanks [@moklick](https://github.com/moklick)! - Pass dimensions to final resize change event
- [#5142](https://github.com/xyflow/xyflow/pull/5142) [`24a1bc89`](https://github.com/xyflow/xyflow/commit/24a1bc89348817ed9b5c87f74bf2519c705143be) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `HandleProps`, `NodeBase` and `InternalNodeBase`
- [#5136](https://github.com/xyflow/xyflow/pull/5136) [`36657cd6`](https://github.com/xyflow/xyflow/commit/36657cd66322c911e87eb37275c584a80025adfe) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `EdgeBase`
- [#5139](https://github.com/xyflow/xyflow/pull/5139) [`89de9ca8`](https://github.com/xyflow/xyflow/commit/89de9ca83fbf9263a687a0f5f915efb2beb31654) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Use `rgba` for colors with opacity instead of `rgb` for `MiniMap` mask color
- [#5148](https://github.com/xyflow/xyflow/pull/5148) [`2ac6e155`](https://github.com/xyflow/xyflow/commit/2ac6e155e35256ca436281df16344366e7d05761) Thanks [@moklick](https://github.com/moklick)! - Prevent browser zoom for pinch zoom gestures on nowheel elements
- [#5137](https://github.com/xyflow/xyflow/pull/5137) [`f0f378e5`](https://github.com/xyflow/xyflow/commit/f0f378e5b6918c2c30d9dc1e32587063cb942d4e) Thanks [@dimaMachina](https://github.com/dimaMachina)! - Improve TSDoc comments for `Connection` and `ConnectionInProgress`
## 0.0.53
### Patch Changes
- [#5118](https://github.com/xyflow/xyflow/pull/5118) [`5d15b01b`](https://github.com/xyflow/xyflow/commit/5d15b01ba8cb349d6397a6ed8162848b4dfec293) Thanks [@moklick](https://github.com/moklick)! - Do not swallow key events when a button is focused
- [#5067](https://github.com/xyflow/xyflow/pull/5067) [`cb685281`](https://github.com/xyflow/xyflow/commit/cb685281d0eaf03e9833271c31f92b1d143af2fe) Thanks [@peterkogo](https://github.com/peterkogo)! - Fix fitView not working immediately after adding new nodes
- [#5091](https://github.com/xyflow/xyflow/pull/5091) [`a79f30b3`](https://github.com/xyflow/xyflow/commit/a79f30b3dd7c8ff6400c8d22214b2c2282e5bac1) Thanks [@moklick](https://github.com/moklick)! - Add center-left and center-right as a panel position
## 0.0.52
### Patch Changes
- [#5052](https://github.com/xyflow/xyflow/pull/5052) [`99dd7d35`](https://github.com/xyflow/xyflow/commit/99dd7d3549e7423e7d103b2c956c8b37f5747b90) Thanks [@moklick](https://github.com/moklick)! - Show an error if user drags uninitialized node
## 0.0.51
### Patch Changes
- [#5010](https://github.com/xyflow/xyflow/pull/5010) [`6c121d42`](https://github.com/xyflow/xyflow/commit/6c121d427fea9a11e86a85f95d2c12ba8af34919) Thanks [@moklick](https://github.com/moklick)! - Add more TSDocs to components, hooks, utils funcs and types
- [#4990](https://github.com/xyflow/xyflow/pull/4990) [`4947029c`](https://github.com/xyflow/xyflow/commit/4947029cd6cda0f695e1fb4815e4030adb232234) Thanks [@damianstasik](https://github.com/damianstasik)! - Make it possible to stop autoPanOnDrag by setting it to false
- [#5003](https://github.com/xyflow/xyflow/pull/5003) [`e8e0d684`](https://github.com/xyflow/xyflow/commit/e8e0d684957b95d53a6cc11598c8755ff02117c7) Thanks [@dimaMachina](https://github.com/dimaMachina)! - repair lint command
## 0.0.50
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@xyflow/system",
"version": "0.0.50",
"version": "0.0.55",
"description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [
"node-based UI",
@@ -63,7 +63,7 @@
"@xyflow/eslint-config": "workspace:*",
"@xyflow/rollup-config": "workspace:*",
"@xyflow/tsconfig": "workspace:*",
"typescript": "5.1.3"
"typescript": "5.4.5"
},
"rollup": {
"globals": {

View File

@@ -26,6 +26,8 @@ export const errorMessages = {
`It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`,
error014: () =>
'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.',
error015: () =>
'It seems that you are trying to drag a node that is not initialized. Please use onNodesChange as explained in the docs.',
};
export const infiniteExtent: CoordinateExtent = [

Some files were not shown because too many files have changed in this diff Show More