Merge pull request #1555 from wbkd/v10

next major release v10
This commit is contained in:
Moritz Klack
2022-03-14 19:46:45 +01:00
committed by GitHub
152 changed files with 34026 additions and 26397 deletions

View File

@@ -6,6 +6,7 @@
"modules": false
}
],
"@babel/preset-typescript",
"@babel/preset-react"
],
"plugins": ["@babel/plugin-transform-runtime"]

7
.postcssrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"plugins": {
"autoprefixer" : {},
"postcss-nested": {}
}
}

5
.prettierrc.json Normal file
View File

@@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120
}

View File

@@ -7,6 +7,6 @@
},
"hooks": {
"after:bump": "npm run build",
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
"after:release": "echo Successfully released ${name} v${version}."
}
}

View File

@@ -1,24 +0,0 @@
language: node_js
node_js:
- 10.8
addons:
apt:
packages:
- libgconf-2-4
cache:
directories:
- ~/.npm
- ~/.cache
install:
- npm ci
before_script:
- cd example && npm install
- cd ..
script:
- npm run test

View File

@@ -27,5 +27,5 @@ Please use a meaningful commit message and add a little description of your chan
1. Install dependencies `npm install` and example dependencies `cd example && npm install`
2. Run library start task `npm start` and example start task `cd example && npm start`
3. No you can test your changes with the exisiting examples. Feel free to add a new one if it's needed for your changes
3. Now you can test your changes with the existing examples. Feel free to add a new one if it's needed for your changes
4. Run tests `npm test` and add new ones if you are introducing a new feature

104
README.md
View File

@@ -1,52 +1,62 @@
[![react-flow](https://user-images.githubusercontent.com/2857535/95224198-b2540000-07fa-11eb-832d-361d72d60345.jpg)](https://reactflow.dev)
![readme-header-dark](https://user-images.githubusercontent.com/3797215/156384064-08a889d6-73c0-4cbf-8ff3-28dc601d1f5f.svg#gh-dark-mode-only)
![readme-header](https://user-images.githubusercontent.com/3797215/156259138-fb9f59f8-52f2-474a-b78c-6570867e4ead.svg#gh-light-mode-only)
React Flow is a library for building node based graphs. You can easily implement custom node types and it comes with components like a mini map and graph controls. Feel free to check out the [examples](https://reactflow.dev/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started.
<div align="center">
## 👉 Next Major Release 👈
![GitHub License MIT](https://img.shields.io/github/license/wbkd/react-flow?color=%23FF0072)
![npm downloads](https://img.shields.io/npm/dt/react-flow-renderer?color=%23FF0072&label=downloads)
![GitHub Repo stars](https://img.shields.io/github/stars/wbkd/react-flow?color=%23FF0072)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/wbkd/react-flow?color=%23FF0072)
We are currently working on the next version. You can find the installation instructions and breaking changes in [this PR](https://github.com/wbkd/react-flow/pull/1555). Your feedback is highly appreciated :)
### Wire Your Ideas With React Flow!
A highly customizable React component for building interactive graphs and node-based editors.
- **Website:** https://reactflow.dev
- **Documentation:** https://reactflow.dev/docs
- **Examples:** https://reactflow.dev/examples
- **Website/Docs Repository:** https://github.com/wbkd/react-flow-docs
- **Community:** https://discord.gg/Bqt6xrs
- **Sponsorship:** https://github.com/sponsors/wbkd
[🚀 Getting Started](https://reactflow.dev/docs/getting-started/installation) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/Bqt6xrs) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
</div>
React Flow was initially created for [datablocks](https://datablocks.pro). A node-based editor for transforming, analyzing and visualizing data.
----
## Key Features
- **Easy to use:** Seamless zooming and panning, single- and multi selection of graph elements and keyboard shortcuts are supported out of the box
- **Customizable:** Different [node](https://reactflow.dev/docs/api/nodes/node-types) and [edge types](https://reactflow.dev/docs/api/edges/edge-types) and support for custom nodes with multiple handles and custom edges
- **Fast rendering:** Only nodes that have changed are re-rendered and only those in the viewport are displayed
- **Hooks and Utils:** [Hooks](https://reactflow.dev/docs/api/hooks/use-react-flow) for handling nodes, edges and the viewport and graph [helper functions](https://reactflow.dev/docs/api/graph-util-functions)
- **Plugin Components:** [Background](https://reactflow.dev/docs/api/plugin-components/background), [MiniMap](https://reactflow.dev/docs/api/plugin-components/minimap) and [Controls](https://reactflow.dev/docs/api/plugin-components/controls)
- **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
## Installation
```
The easiest way to get the latest version of React Flow is to install it via npm:
```bash
npm install react-flow-renderer
```
## Quick Start
This is a very basic example of how to use React Flow. You can find more advanced examples on the [website](https://reactflow.dev/examples).
This is only a very basic usage example of React Flow. To see everything that is possible with the library, please refer to the [website](https://reactflow.dev) for [guides](https://reactflow.dev/docs/guides/custom-nodes), [examples](https://reactflow.dev/docs/examples/overview) and [API reference](https://reactflow.dev/docs/api/react-flow-props).
```js
import React from 'react';
import ReactFlow from 'react-flow-renderer';
```jsx
import ReactFlow, { MiniMap, Controls } from 'react-flow-renderer';
const elements = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
// you can also pass a React component as a label
{ id: '2', data: { label: <div>Node 2</div> }, position: { x: 100, y: 100 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
];
const BasicFlow = () => <ReactFlow elements={elements} />;
function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<MiniMap />
<Controls />
</ReactFlow>
);
}
```
## Community Packages
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
## Development
Before you start you need to install the React Flow dependencies via `npm install` and the ones of the examples `cd example && npm install`.
If you want to contribute or develop custom features the easiest way is to start the dev server:
@@ -71,19 +81,33 @@ Testing is done with cypress. You can find the tests in the [`integration/flow`]
npm run test
```
## Maintainer
## Attribution
Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
Since v10.0.0 the React Flow renderer includes a small attribution that links to the React Flow website. It is only allowed to remove the attribution if you are subscribed to one of the [pro plans](https://pro.reactflow.dev/pricing). A subscription also provides more features such as a private Discord channel with the creators of React Flow or access to the source code of Datablocks (a node-based data editor built with React Flow).
## Support
## Maintainers
If you need custom support or features for your application we are [happy to hear from you](https://webkid.io/contact).
React Flow is developed and maintained by [webkid](https://webkid.io), a web development agency with the focus on data driven applications from Berlin. If you need help or want to talk to us about a collaboration, feel free to contact us:
* Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
* Christopher Möller • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze)
### Thanks!
You can also use our [contact form](https://webkid.io/contact/) or join the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
Special thanks to [Andy Lindemann](https://github.com/AndyLnd) for a lot of helpful contributions!
## Community Packages
---
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
React Flow is maintained by [webkid](https://webkid.io), a data visualization agency from Berlin. If you need help or want to develop react-based tools or data visualizations, [get in touch](https://webkid.io/contact)!
## Credits
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in your browser. Under the hood, React Flow depends on these great libraries:
* [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
* [react-draggable](https://github.com/react-grid-layout/react-draggable) - used for making the nodes draggable
* [zustand](https://github.com/pmndrs/zustand) - internal state management
## License
React Flow is [MIT licensed](https://github.com/wbkd/react-flow/blob/main/LICENSE).

View File

@@ -2,5 +2,6 @@
"baseUrl": "http://localhost:3000",
"viewportWidth": 1280,
"viewportHeight": 720,
"video": false
}
"video": false,
"pluginsFile": false
}

View File

@@ -33,7 +33,7 @@ describe('Basic Flow Rendering', () => {
});
it('deselects node', () => {
cy.get('.react-flow__renderer').click('bottomRight');
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
});
@@ -42,7 +42,7 @@ describe('Basic Flow Rendering', () => {
});
it('deselects edge', () => {
cy.get('.react-flow__renderer').click('bottomRight');
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
});
@@ -50,9 +50,9 @@ describe('Basic Flow Rendering', () => {
cy.get('body')
.type('{shift}', { release: false })
.get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true })
.trigger('mousemove', 800, 75, { which: 1 })
.trigger('mouseup', 'bottomRight', { force: true })
.trigger('mousedown', 1000, 1, { which: 1, force: true })
.trigger('mousemove', 1, 200, { which: 1 })
.trigger('mouseup', 1, 200, { force: true })
.get('.react-flow__node')
.first()
.should('have.class', 'selected')
@@ -68,9 +68,9 @@ describe('Basic Flow Rendering', () => {
cy.get('body')
.type('{shift}', { release: false })
.get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true })
.trigger('mousemove', 'bottomRight', { which: 1 })
.trigger('mouseup', 'bottomRight', { force: true })
.trigger('mousedown', 'topRight', { which: 1, force: true })
.trigger('mousemove', 'bottomLeft', { which: 1 })
.trigger('mouseup', 'bottomLeft', { force: true })
.get('.react-flow__node')
.should('have.class', 'selected')
.get('.react-flow__nodesselection-rect');
@@ -79,7 +79,7 @@ describe('Basic Flow Rendering', () => {
});
it('removes selection', () => {
cy.get('.react-flow__renderer').click('bottomRight');
cy.get('.react-flow__renderer').click('bottomLeft');
cy.get('.react-flow__nodesselection-rect').should('not.exist');
});
@@ -108,12 +108,12 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { which: 1 });
.trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
.find('.react-flow__handle.target')
.trigger('mousemove')
.trigger('mousemove', { force: true })
.trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 2);
@@ -127,29 +127,29 @@ describe('Basic Flow Rendering', () => {
});
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('zooms the pane', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});

View File

@@ -8,30 +8,30 @@ describe('Controls Testing', () => {
});
it('zooms in', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomin')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
it('zooms out', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-zoomout')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});
// view is already fitted so we drag the pane to un-fit it
it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
@@ -41,19 +41,19 @@ describe('Controls Testing', () => {
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })
.then(() => {
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
});
});
});
it('fits view', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__controls-fitview')
.click()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
});
});

View File

@@ -20,18 +20,13 @@ describe('Empty Flow Rendering', () => {
cy.get('body').type('{shift}', { release: true });
});
it('renders an empty mini map', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-node').should('not.exist');
});
it('adds two nodes', () => {
cy.contains('add node').click();
cy.contains('add node').click();
});
it('connects nodes', () => {
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { which: 1 });
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.last()
@@ -41,9 +36,4 @@ describe('Empty Flow Rendering', () => {
cy.get('.react-flow__edge').should('have.length', 1);
});
it('renders mini map with two nodes', () => {
cy.get('.react-flow__minimap');
cy.get('.react-flow__minimap-node').should('have.length', 2);
});
});

View File

@@ -1,4 +1,4 @@
import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../dist/ReactFlow.js';
import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/esm/index.js';
const nodes = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
@@ -13,8 +13,6 @@ const edges = [
{ id: 'e2-3', source: '2', target: '3' },
];
const elements = [...nodes, ...edges];
describe('Graph Utils Testing', () => {
it('tests isNode function', () => {
expect(isNode(nodes[0])).to.be.true;
@@ -27,41 +25,42 @@ describe('Graph Utils Testing', () => {
});
it('tests getOutgoers function', () => {
const outgoers = getOutgoers(nodes[0], elements);
const outgoers = getOutgoers(nodes[0], nodes, edges);
expect(outgoers.length).to.be.equal(2);
const noOutgoers = getOutgoers(nodes[2], elements);
const noOutgoers = getOutgoers(nodes[2], nodes, edges);
expect(noOutgoers.length).to.be.equal(0);
});
it('tests getIncomers function', () => {
const incomers = getIncomers(nodes[2], elements);
const incomers = getIncomers(nodes[2], nodes, edges);
expect(incomers.length).to.be.equal(2);
const noIncomers = getIncomers(nodes[0], elements);
const noIncomers = getIncomers(nodes[0], nodes, edges);
expect(noIncomers.length).to.be.equal(0);
});
describe('tests addEdge function', () => {
it('adds edge', () => {
const newEdge = { source: '1', target: '4' };
const nextElements = addEdge(newEdge, elements);
const nextEdges = addEdge(newEdge, edges);
expect(nextElements.length).to.be.equal(elements.length + 1);
expect(nextEdges.length).to.be.equal(edges.length + 1);
});
it('tries to add existing edge', () => {
const newEdge = { source: '2', target: '3' };
const nextElements = addEdge(newEdge, elements);
const nextEdges = addEdge(newEdge, edges);
expect(nextElements.length).to.be.equal(elements.length);
expect(nextEdges.length).to.be.equal(edges.length);
});
it('tries to add invalid edge', () => {
const newEdge = { nosource: '1', notarget: '3' };
try {
addEdge(newEdge, elements);
addEdge(newEdge, edges);
} catch (e) {
console.log(e.message);
@@ -69,66 +68,4 @@ describe('Graph Utils Testing', () => {
}
});
});
describe('tests removeElements function', () => {
it('removes a node', () => {
const nextElements = removeElements([nodes[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 1);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
it('removes multiple nodes', () => {
const elementsToRemove = [nodes[0], nodes[1]];
const nextElements = removeElements(elementsToRemove, elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 2);
expect(nextEdges.length).to.be.equal(0);
});
it('removes no node', () => {
const nextElementsNoRemove = removeElements([], elements);
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
});
it('tries to removes node that does not exist', () => {
const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements);
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
});
it('removes an edge', () => {
const nextElements = removeElements([edges[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length);
expect(nextEdges.length).to.be.equal(edges.length - 1);
});
it('removes multiple edges', () => {
const nextElements = removeElements([edges[0], edges[1]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
it('removes node and edge', () => {
const nextElements = removeElements([nodes[0], edges[0]], elements);
const nextNodes = nextElements.filter((e) => isNode(e));
const nextEdges = nextElements.filter((e) => isEdge(e));
expect(nextNodes.length).to.be.equal(nodes.length - 1);
expect(nextEdges.length).to.be.equal(edges.length - 2);
});
});
});

View File

@@ -3,6 +3,16 @@ describe('Hidden Flow Rendering', () => {
cy.visit('/hidden');
});
it('renders empty flow', () => {
cy.get('.react-flow__node').should('not.exist');
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
it('toggles isHidden mode', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders initial flow', () => {
cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4);
@@ -10,7 +20,7 @@ describe('Hidden Flow Rendering', () => {
cy.get('.react-flow__minimap-node').should('have.length', 4);
});
it('toggles isHidden mode', () => {
it('toggles isHidden mode again', () => {
cy.get('.react-flow__ishidden').click();
});
@@ -19,14 +29,4 @@ describe('Hidden Flow Rendering', () => {
cy.get('.react-flow__edge').should('not.exist');
cy.get('.react-flow__minimap-node').should('not.exist');
});
it('toggles isHidden mode again', () => {
cy.get('.react-flow__ishidden').click();
});
it('renders initial flow', () => {
cy.get('.react-flow__node').should('have.length', 4);
cy.get('.react-flow__edge').should('have.length', 3);
cy.get('.react-flow__minimap-node').should('have.length', 4);
});
});

View File

@@ -93,7 +93,8 @@ describe('Interaction Flow Rendering', () => {
});
it('selects an edge by click', () => {
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
cy.get('.react-flow__edge:first').click({ force: true });
cy.get('.react-flow__edge:first').should('have.class', 'selected');
});
it('toggles connectable mode', () => {
@@ -104,7 +105,7 @@ describe('Interaction Flow Rendering', () => {
cy.get('.react-flow__node')
.contains('Node 3')
.find('.react-flow__handle.source')
.trigger('mousedown', { which: 1 });
.trigger('mousedown', { button: 0 });
cy.get('.react-flow__node')
.contains('Node 4')
@@ -120,12 +121,12 @@ describe('Interaction Flow Rendering', () => {
});
it('zooms by scroll', () => {
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: 200 })
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});
@@ -136,12 +137,12 @@ describe('Interaction Flow Rendering', () => {
it('zooms by double click', () => {
cy.get('.react-flow__controls-zoomout').click();
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.dblclick()
.then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
});
});

View File

@@ -19,7 +19,7 @@ describe('Minimap Testing', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
@@ -33,16 +33,14 @@ describe('Minimap Testing', () => {
it('changes node position', () => {
const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
cy.wait(1000);
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
});
});
@@ -53,7 +51,7 @@ describe('Minimap Testing', () => {
// for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => {
cy.get('.react-flow__renderer')
cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win })

View File

@@ -1,17 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

48988
example/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,9 @@
"react": "file:../node_modules/react",
"react-dom": "file:../node_modules/react-dom",
"react-flow-renderer": "file:../",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"typescript": "^4.4.3"
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0",
"typescript": "^4.5.5"
},
"scripts": {
"start": "react-scripts start",
@@ -34,10 +34,10 @@
]
},
"devDependencies": {
"@types/dagre": "^0.7.46",
"@types/dagre": "^0.7.47",
"@types/localforage": "0.0.34",
"@types/react": "^17.0.27",
"@types/react-dom": "^17.0.9",
"@types/react-router-dom": "^5.3.0"
"@types/react": "file:../node_modules/@types/react",
"@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.3"
}
}

View File

@@ -1,80 +1,69 @@
import React, { useState, MouseEvent } from 'react';
import { MouseEvent } from 'react';
import ReactFlow, {
removeElements,
addEdge,
isNode,
ReactFlowProvider,
Background,
Elements,
BackgroundVariant,
FlowElement,
Node,
Edge,
Connection,
OnLoadParams,
useReactFlow,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
const instance = useReactFlow();
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
}
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return el;
});
});
return node;
})
);
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
el.className = el.className === 'light' ? 'dark' : 'light';
}
instance.setNodes((nodes) =>
nodes.map((node) => {
node.className = node.className === 'light' ? 'dark' : 'light';
return el;
});
});
return node;
})
);
};
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
defaultNodes={initialNodes}
defaultEdges={initialEdges}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
fitView
>
<Background variant={BackgroundVariant.Lines} />
@@ -94,4 +83,10 @@ const BasicFlow = () => {
);
};
export default BasicFlow;
export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}

View File

@@ -0,0 +1,98 @@
import ReactFlow, {
useReactFlow,
Background,
BackgroundVariant,
Node,
Edge,
ReactFlowProvider,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const defaultNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const defaultEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
const defaultEdgeOptions = {
animated: true,
};
// This is bad practise. You should either use a controlled or an uncontrolled component.
// This is just an example for testing the API.
const ControlledUncontrolled = () => {
const [nodes, , onNodesChange] = useNodesState(defaultNodes);
const [edges, , onEdgesChange] = useEdgesState(defaultEdges);
const instance = useReactFlow();
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const updateNodePositions = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
return edge;
})
);
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
defaultEdgeOptions={defaultEdgeOptions}
fitView
>
<Background variant={BackgroundVariant.Lines} />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
red edges
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<ControlledUncontrolled />
</ReactFlowProvider>
);
}

View File

@@ -1,28 +1,31 @@
import React, { useState } from 'react';
import ReactFlow, {
removeElements,
Node,
addEdge,
Background,
BackgroundVariant,
Elements,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import ConnectionLine from './ConnectionLine';
const initialElements: Elements = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
const initialEdges: Edge[] = [];
const ConnectionLineFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
connectionLineComponent={ConnectionLine}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
>
<Background variant={BackgroundVariant.Lines} />

View File

@@ -1,46 +1,49 @@
import React, { useState, useEffect, MouseEvent } from 'react';
import { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react';
import ReactFlow, {
isEdge,
removeElements,
addEdge,
MiniMap,
Controls,
Node,
FlowElement,
OnLoadParams,
Elements,
ReactFlowInstance,
Position,
SnapGrid,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode';
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView();
};
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16];
const nodeTypes = {
selectorNode: ColorSelectorNode,
};
const CustomNodeFlow = () => {
const [elements, setElements] = useState<Elements>([]);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setElements((els) =>
els.map((e) => {
if (isEdge(e) || e.id !== '2') {
return e;
setNodes((nds) =>
nds.map((node) => {
if (node.id !== '2') {
return node;
}
const color = event.target.value;
@@ -48,9 +51,9 @@ const CustomNodeFlow = () => {
setBgColor(color);
return {
...e,
...node,
data: {
...e.data,
...node.data,
color,
},
};
@@ -58,7 +61,7 @@ const CustomNodeFlow = () => {
);
};
setElements([
setNodes([
{
id: '1',
type: 'input',
@@ -87,31 +90,35 @@ const CustomNodeFlow = () => {
position: { x: 550, y: 100 },
targetPosition: Position.Left,
},
]);
setEdges([
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
]);
}, []);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) =>
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
const onConnect = (connection: Connection) =>
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
style={{ background: bgColor }}
onLoad={onLoad}
onInit={onInit}
nodeTypes={nodeTypes}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
defaultZoom={1.5}
fitView
>
<MiniMap
nodeStrokeColor={(n: Node): string => {
@@ -132,4 +139,4 @@ const CustomNodeFlow = () => {
);
};
export default CustomNodeFlow;
export default CustomNodeFlow;

View File

@@ -0,0 +1,83 @@
import ReactFlow, {
useReactFlow,
Background,
BackgroundVariant,
Node,
Edge,
ReactFlowProvider,
} from 'react-flow-renderer';
const defaultNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const defaultEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
];
const defaultEdgeOptions = {
animated: true,
};
const DefaultNodes = () => {
const instance = useReactFlow();
const logToObject = () => console.log(instance.toObject());
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const updateNodePositions = () => {
instance.setNodes((nodes) =>
nodes.map((node) => {
node.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return node;
})
);
};
const updateEdgeColors = () => {
instance.setEdges((edges) =>
edges.map((edge) => {
edge.style = {
stroke: '#ff5050',
};
return edge;
})
);
};
return (
<ReactFlow defaultNodes={defaultNodes} defaultEdges={defaultEdges} defaultEdgeOptions={defaultEdgeOptions} fitView>
<Background variant={BackgroundVariant.Lines} />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
red edges
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default function App() {
return (
<ReactFlowProvider>
<DefaultNodes />
</ReactFlowProvider>
);
}

View File

@@ -1,4 +1,4 @@
import ReactFlow from 'react-flow-renderer';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import DragHandleNode from './DragHandleNode';
@@ -6,16 +6,24 @@ const nodeTypes = {
dragHandleNode: DragHandleNode,
};
const elements = [
const initialNodes: Node[] = [
{
id: '2',
type: 'dragHandleNode',
dragHandle: '.custom-drag-handle',
style: { border: '1px solid #ddd', padding: '20px 40px' },
position: { x: 200, y: 200 },
data: null,
},
];
const DragHandleFlow = () => <ReactFlow elements={elements} nodeTypes={nodeTypes} />;
const initialEdges: Edge[] = [];
const DragHandleFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges] = useEdgesState(initialEdges);
return <ReactFlow nodes={nodes} onNodesChange={onNodesChange} edges={edges} nodeTypes={nodeTypes} />;
};
export default DragHandleFlow;

View File

@@ -1,22 +1,21 @@
import React, { useState, DragEvent } from 'react';
import { useState, DragEvent } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
removeElements,
Controls,
OnLoadParams,
Elements,
ReactFlowInstance,
Connection,
Edge,
ElementId,
Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import Sidebar from './Sidebar';
import './dnd.css';
const initialElements = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }];
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }];
const onDragOver = (event: DragEvent) => {
event.preventDefault();
@@ -24,15 +23,15 @@ const onDragOver = (event: DragEvent) => {
};
let id = 0;
const getId = (): ElementId => `dndnode_${id++}`;
const getId = () => `dndnode_${id++}`;
const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<OnLoadParams>();
const [elements, setElements] = useState<Elements>(initialElements);
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onLoad = (_reactFlowInstance: OnLoadParams) => setReactFlowInstance(_reactFlowInstance);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
const onDrop = (event: DragEvent) => {
event.preventDefault();
@@ -47,7 +46,7 @@ const DnDFlow = () => {
data: { label: `${type} node` },
};
setElements((es) => es.concat(newNode));
setNodes((nds) => nds.concat(newNode));
}
};
@@ -56,10 +55,12 @@ const DnDFlow = () => {
<ReactFlowProvider>
<div className="reactflow-wrapper">
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onEdgesChange={onEdgesChange}
onNodesChange={onNodesChange}
onConnect={onConnect}
onElementsRemove={onElementsRemove}
onLoad={onLoad}
onInit={onInit}
onDrop={onDrop}
onDragOver={onDragOver}
>

View File

@@ -1,40 +1,49 @@
/**
* Example for checking the different edge types and source and target positions
*/
import React, { useState } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Controls,
Background,
OnLoadParams,
ReactFlowInstance,
Connection,
Edge,
Elements,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import { getElements } from './utils';
const onLoad = (reactFlowInstance: OnLoadParams) => {
const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements());
console.log(reactFlowInstance.getNodes());
};
const initialElements = getElements();
const { nodes: initialNodes, edges: initialEdges } = getElements();
const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight'];
const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
const EdgeTypesFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
onConnect={onConnect}
minZoom={0.2}
zoomOnScroll={false}
selectionKeyCode="a+s"
multiSelectionKeyCode={multiSelectionKeyCode}
deleteKeyCode={deleteKeyCode}
zoomActivationKeyCode="z"
>
<MiniMap />
<Controls />

View File

@@ -1,4 +1,4 @@
import { ElementId, Elements, Position } from 'react-flow-renderer';
import { Edge, Node, Position } from 'react-flow-renderer';
const nodeWidth = 80;
const nodeGapWidth = nodeWidth * 2;
@@ -9,10 +9,10 @@ const sourceTargetPositions = [
{ source: Position.Right, target: Position.Left },
];
const nodeColors = [
['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'],
['#0c5956', '#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8', '#4fa6e0'],
];
const edgeTypes = ['default', 'step', 'smoothstep', 'straight'];
const edgeTypes = ['default', 'step', 'smoothstep', 'straight', 'simplebezier'];
const offsets = [
{
x: 0,
@@ -49,10 +49,10 @@ const offsets = [
];
let id = 0;
const getNodeId = (): ElementId => (id++).toString();
const getNodeId = (): string => (id++).toString();
export function getElements(): Elements {
const initialElements = [];
export function getElements(): { nodes: Node[]; edges: Edge[] } {
const initialElements = { nodes: [] as Node[], edges: [] as Edge[] };
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex];
@@ -70,7 +70,7 @@ export function getElements(): Elements {
};
const sourceId = getNodeId();
const sourceData = { label: `Source ${sourceId}` };
const sourceNode = {
const sourceNode: Node = {
id: sourceId,
style,
data: sourceData,
@@ -85,7 +85,7 @@ export function getElements(): Elements {
x: sourcePosition.x + currOffset.x,
y: sourcePosition.y + currOffset.y,
};
const targetNode = {
const targetNode: Node = {
id: targetId,
style,
data: targetData,
@@ -94,10 +94,15 @@ export function getElements(): Elements {
targetPosition: currSourceTargetPos.target,
};
initialElements.push(sourceNode);
initialElements.push(targetNode);
initialElements.nodes.push(sourceNode);
initialElements.nodes.push(targetNode);
initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType });
initialElements.edges.push({
id: `${sourceId}-${targetId}`,
source: sourceId,
target: targetId,
type: currEdgeType,
} as Edge);
}
}
}

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { EdgeProps, getBezierPath, getMarkerEnd } from 'react-flow-renderer';
import { EdgeProps, getBezierPath } from 'react-flow-renderer';
const CustomEdge: FC<EdgeProps> = ({
id,
@@ -10,15 +10,12 @@ const CustomEdge: FC<EdgeProps> = ({
sourcePosition,
targetPosition,
data,
arrowHeadType,
markerEndId,
}) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} />
<path id={id} className="react-flow__edge-path" d={edgePath} />
<text>
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
{data.text}

View File

@@ -1,5 +1,5 @@
import { FC } from 'react';
import { EdgeProps, getBezierPath, getMarkerEnd, EdgeText, getEdgeCenter } from 'react-flow-renderer';
import { EdgeProps, getBezierPath, EdgeText, getEdgeCenter } from 'react-flow-renderer';
const CustomEdge: FC<EdgeProps> = ({
id,
@@ -10,11 +10,8 @@ const CustomEdge: FC<EdgeProps> = ({
sourcePosition,
targetPosition,
data,
arrowHeadType,
markerEndId,
}) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const [centerX, centerY] = getEdgeCenter({
sourceX,
sourceY,
@@ -24,7 +21,7 @@ const CustomEdge: FC<EdgeProps> = ({
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} />
<path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeText
x={centerX}
y={centerY}

View File

@@ -1,32 +1,31 @@
import React, { useState, MouseEvent } from 'react';
import { MouseEvent } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Controls,
Background,
OnLoadParams,
FlowElement,
EdgeTypesType,
Elements,
Connection,
Controls,
Edge,
ArrowHeadType,
EdgeTypes,
MarkerType,
MiniMap,
Node,
Position,
ReactFlowInstance,
useEdgesState,
useNodesState,
} from 'react-flow-renderer';
import CustomEdge from './CustomEdge';
import CustomEdge2 from './CustomEdge2';
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
{ id: '2b', data: { label: 'Node 2b' }, position: { x: -40, y: 300 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
@@ -35,12 +34,48 @@ const initialElements: Elements = [
{ id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } },
{ id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } },
{ id: '9', type: 'output', data: { label: 'Output 9' }, position: { x: 675, y: 500 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', className: 'normal-edge' },
{ id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
{
id: 'e2a-2b',
source: '2a',
target: '2b',
type: 'simplebezier',
label: 'simple bezier edge',
},
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
{
id: 'e5-7',
source: '5',
target: '7',
label: 'label with styled bg',
labelBgPadding: [8, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
{
id: 'e5-8',
source: '5',
target: '8',
type: 'custom',
data: { text: 'custom edge' },
},
{
id: 'e5-9',
source: '5',
target: '9',
type: 'custom2',
data: { text: 'custom edge 2' },
},
{
id: 'e5-6',
source: '5',
@@ -54,54 +89,46 @@ const initialElements: Elements = [
</>
),
labelStyle: { fill: 'red', fontWeight: 700 },
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e5-7',
source: '5',
target: '7',
label: 'label with styled bg',
labelBgPadding: [8, 4],
labelBgBorderRadius: 4,
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
arrowHeadType: ArrowHeadType.ArrowClosed,
},
{
id: 'e5-8',
source: '5',
target: '8',
type: 'custom',
data: { text: 'custom edge' },
arrowHeadType: ArrowHeadType.ArrowClosed,
},
{
id: 'e5-9',
source: '5',
target: '9',
type: 'custom2',
data: { text: 'custom edge 2' },
style: { stroke: '#ffcc00' },
markerEnd: {
type: MarkerType.Arrow,
color: '#FFCC00',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
strokeWidth: 2,
},
markerStart: {
type: MarkerType.ArrowClosed,
color: '#FFCC00',
orient: 'auto-start-reverse',
markerUnits: 'userSpaceOnUse',
width: 20,
height: 20,
},
},
];
const edgeTypes: EdgeTypesType = {
const edgeTypes: EdgeTypes = {
custom: CustomEdge,
custom2: CustomEdge2,
};
const EdgesFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
onLoad={onLoad}
onInit={onInit}
snapToGrid={true}
edgeTypes={edgeTypes}
>

View File

@@ -1,52 +1,51 @@
import React, { useState, MouseEvent, CSSProperties } from 'react';
import { MouseEvent, CSSProperties } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Controls,
Background,
OnLoadParams,
Elements,
ElementId,
Node,
FlowElement,
BackgroundVariant,
Connection,
Edge,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from 'react-flow-renderer';
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 };
const EmptyFlow = () => {
const [elements, setElements] = useState<Elements>([]);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const addRandomNode = () => {
const nodeId: ElementId = (elements.length + 1).toString();
const nodeId = (nodes.length + 1).toString();
const newNode: Node = {
id: nodeId,
data: { label: `Node: ${nodeId}` },
position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight },
};
setElements((els) => els.concat(newNode));
setNodes((nds) => nds.concat(newNode));
};
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false}
>
<MiniMap />
<Controls />
<Background variant={BackgroundVariant.Lines} />

View File

@@ -16,7 +16,9 @@ const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({
const targetNode = {
id: 'connection-target',
__rf: { width: 1, height: 1, position: { x: targetX, y: targetY } },
width: 1,
height: 1,
position: { x: targetX, y: targetY },
} as Node;
const { sx, sy } = getEdgeParams(sourceNode, targetNode);

View File

@@ -1,14 +1,15 @@
import { FC, useMemo, CSSProperties } from 'react';
import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer';
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-renderer';
import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, markerEndId, style }) => {
const nodes = useStoreState((state) => state.nodes);
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const nodeSelector = (s: ReactFlowState) => s.nodeInternals;
const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]);
const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]);
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const nodeInternals = useStore(nodeSelector);
const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]);
const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]);
if (!sourceNode || !targetNode) {
return null;
@@ -27,7 +28,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, marker
return (
<g className="react-flow__connection">
<path id={id} className="react-flow__edge-path" d={d} markerEnd={markerEnd} style={style as CSSProperties} />
<path id={id} className="react-flow__edge-path" d={d} style={style as CSSProperties} />
</g>
);
};

View File

@@ -1,15 +1,13 @@
import React, { useState } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
Background,
OnLoadParams,
EdgeTypesType,
Elements,
ReactFlowInstance,
EdgeTypes,
Connection,
Edge,
ArrowHeadType,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import './style.css';
@@ -18,29 +16,31 @@ import FloatingEdge from './FloatingEdge';
import FloatingConnectionLine from './FloatingConnectionLine';
import { createElements } from './utils';
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const initialElements: Elements = createElements();
const { nodes: initialNodes, edges: initialEdges } = createElements();
const edgeTypes: EdgeTypesType = {
const edgeTypes: EdgeTypes = {
floating: FloatingEdge,
};
const NodeAsHandleFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const FloatingEdges = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) =>
setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els));
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
return (
<div className="floatingedges">
<ReactFlow
elements={elements}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onLoad={onLoad}
onInit={onInit}
edgeTypes={edgeTypes}
connectionLineComponent={FloatingConnectionLine}
>
@@ -50,4 +50,4 @@ const NodeAsHandleFlow = () => {
);
};
export default NodeAsHandleFlow;
export default FloatingEdges;

View File

@@ -1,18 +1,19 @@
import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer';
import { Position, XYPosition, Node, Edge } from 'react-flow-renderer';
// this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
position: intersectionNodePosition,
} = intersectionNode.__rf;
const targetPosition = targetNode.__rf.position;
} = intersectionNode;
const targetPosition = targetNode.position;
const w = intersectionNodeWidth / 2;
const h = intersectionNodeHeight / 2;
const w = (intersectionNodeWidth ?? 0) / 2;
const h = (intersectionNodeHeight ?? 0) / 2;
const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h;
@@ -32,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi
// returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
const n = { ...node.__rf.position, ...node.__rf };
const n = { ...node.position, ...node };
const nx = Math.round(n.x);
const ny = Math.round(n.y);
const px = Math.round(intersectionPoint.x);
@@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) {
return Position.Left;
}
if (px >= nx + n.width - 1) {
if (px >= nx + (n.width ?? 0) - 1) {
return Position.Right;
}
if (py <= ny + 1) {
return Position.Top;
}
if (py >= n.y + n.height - 1) {
if (py >= n.y + (n.height ?? 0) - 1) {
return Position.Bottom;
}
@@ -72,11 +73,18 @@ export function getEdgeParams(source: Node, target: Node) {
};
}
export function createElements() {
const elements = [];
type NodesAndEdges = {
nodes: Node[];
edges: Edge[];
};
export function createElements(): NodesAndEdges {
const nodes: Node[] = [];
const edges: Edge[] = [];
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
elements.push({ id: 'target', data: { label: 'Target' }, position: center });
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
for (let i = 0; i < 8; i++) {
const degrees = i * (360 / 8);
@@ -84,16 +92,15 @@ export function createElements() {
const x = 250 * Math.cos(radians) + center.x;
const y = 250 * Math.sin(radians) + center.y;
elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
elements.push({
edges.push({
id: `edge-${i}`,
target: 'target',
source: `${i}`,
type: 'floating',
arrowHeadType: ArrowHeadType.Arrow,
});
}
return elements;
return { nodes, edges };
}

View File

@@ -1,34 +1,53 @@
import React, { useState } from 'react';
import { useState, useCallback } from 'react';
import { useEffect } from 'react';
import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Connection,
Edge,
Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const initialElements: Elements = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
const initialNodes: Node[] = [
{ id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4' },
];
const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => {
e.hidden = hidden;
return e;
});
const HiddenFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [isHidden, setIsHidden] = useState<boolean>(false);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes] = useNodesState(initialNodes);
const [edges, setEdges] = useEdgesState(initialEdges);
const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
useEffect(() => {
setElements((els) =>
els.map((e) => {
e.isHidden = isHidden;
return e;
})
);
setNodes(setHidden(isHidden));
setEdges(setHidden(isHidden));
}, [isHidden]);
return (
<ReactFlow elements={elements} onConnect={onConnect}>
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect}>
<MiniMap />
<Controls />

View File

@@ -1,37 +1,42 @@
import React, { useState, MouseEvent, WheelEvent } from 'react';
import { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import ReactFlow, {
addEdge,
MiniMap,
Controls,
Elements,
Node,
FlowElement,
Connection,
Edge,
PanOnScrollMode,
FlowTransform,
Viewport,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event);
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (flowTranasform?: FlowTransform) => console.log('onMoveEnd', flowTranasform);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (_: TouchEvent | MouseEvent, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const [isSelectable, setIsSelectable] = useState<boolean>(false);
const [isDraggable, setIsDraggable] = useState<boolean>(false);
@@ -41,14 +46,17 @@ const InteractionFlow = () => {
const [panOnScroll, setPanOnScroll] = useState<boolean>(false);
const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free);
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState<boolean>(false);
const [paneMoveable, setPaneMoveable] = useState<boolean>(true);
const [panOnDrag, setPanOnDrag] = useState<boolean>(true);
const [captureZoomClick, setCaptureZoomClick] = useState<boolean>(false);
const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false);
const [captureElementClick, setCaptureElementClick] = useState<boolean>(false);
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
elementsSelectable={isSelectable}
nodesConnectable={isConnectable}
nodesDraggable={isDraggable}
@@ -58,10 +66,11 @@ const InteractionFlow = () => {
panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick}
onConnect={onConnect}
onElementClick={captureElementClick ? onElementClick : undefined}
onNodeClick={captureElementClick ? onNodeClick : undefined}
onEdgeClick={captureElementClick ? onEdgeClick : undefined}
onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop}
paneMoveable={paneMoveable}
panOnDrag={panOnDrag}
onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
@@ -171,14 +180,14 @@ const InteractionFlow = () => {
</label>
</div>
<div>
<label htmlFor="panemoveable">
paneMoveable
<label htmlFor="panondrag">
panOnDrag
<input
id="panemoveable"
id="panondrag"
type="checkbox"
checked={paneMoveable}
onChange={(event) => setPaneMoveable(event.target.checked)}
className="react-flow__panemoveable"
checked={panOnDrag}
onChange={(event) => setPanOnDrag(event.target.checked)}
className="react-flow__panondrag"
/>
</label>
</div>

View File

@@ -1,74 +1,75 @@
import React, { useState } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
removeElements,
Controls,
isNode,
Elements,
Connection,
Edge,
NodeExtent,
CoordinateExtent,
Position,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import dagre from 'dagre';
import initialElements from './initial-elements';
import initialItems from './initial-elements';
import './layouting.css';
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeExtent: NodeExtent = [
const nodeExtent: CoordinateExtent = [
[0, 0],
[1000, 1000],
];
const LayoutFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onLayout = (direction: string) => {
const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction });
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, { width: 150, height: 50 });
} else {
dagreGraph.setEdge(el.source, el.target);
}
nodes.forEach((node) => {
dagreGraph.setNode(node.id, { width: 150, height: 50 });
});
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target);
});
dagre.layout(dagreGraph);
const layoutedElements = elements.map((el) => {
if (isNode(el)) {
const nodeWithPosition = dagreGraph.node(el.id);
el.targetPosition = isHorizontal ? Position.Left : Position.Top;
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
}
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
return el;
return node;
});
setElements(layoutedElements);
setNodes(layoutedNodes);
};
return (
<div className="layoutflow">
<ReactFlowProvider>
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onConnect={onConnect}
onElementsRemove={onElementsRemove}
nodeExtent={nodeExtent}
onLoad={() => onLayout('TB')}
onInit={() => onLayout('TB')}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<Controls />
</ReactFlow>

View File

@@ -1,8 +1,8 @@
import { Elements, XYPosition } from 'react-flow-renderer';
import { Node, Edge, XYPosition } from 'react-flow-renderer';
const position: XYPosition = { x: 0, y: 0 };
const elements: Elements = [
const nodes: Node[] = [
{
id: '1',
type: 'input',
@@ -56,6 +56,9 @@ const elements: Elements = [
position,
},
{ id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } },
];
const edges: Edge[] = [
{ id: 'e12', source: '1', target: '2', type: 'smoothstep' },
{ id: 'e13', source: '1', target: '3', type: 'smoothstep' },
{ id: 'e22a', source: '2', target: '2a', type: 'smoothstep' },
@@ -68,4 +71,6 @@ const elements: Elements = [
{ id: 'e57', source: '5', target: '7', type: 'smoothstep' },
];
export default elements;
const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;

View File

@@ -1,34 +1,45 @@
import React, { useState, FC } from 'react';
import { FC } from 'react';
import ReactFlow, {
removeElements,
addEdge,
Background,
Elements,
Node,
Edge,
Connection,
ReactFlowProvider,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import './multiflows.css';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const Flow: FC = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlowProvider>
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<Background />
</ReactFlow>
</ReactFlowProvider>

View File

@@ -0,0 +1,170 @@
import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
useNodesState,
useEdgesState,
MiniMap,
Controls,
Node,
Edge,
ReactFlowInstance,
Connection,
} from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
},
{
id: '2a',
data: { label: 'Node 2a' },
position: { x: 10, y: 50 },
parentNode: '2',
},
{ id: '3', data: { label: 'Node 3' }, position: { x: 320, y: 100 }, className: 'light' },
{
id: '4',
data: { label: 'Node 4' },
position: { x: 320, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 65 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: 150, width: 270 },
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 40 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default NestedFlow;

View File

@@ -1,10 +1,16 @@
import React, { useState, CSSProperties } from 'react';
import { CSSProperties } from 'react';
import ReactFlow, { addEdge, isEdge, OnLoadParams, Elements, Position, Connection, Edge } from 'react-flow-renderer';
import ReactFlow, {
addEdge,
Node,
Position,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '1',
sourcePosition: Position.Right,
@@ -20,31 +26,40 @@ const initialElements: Elements = [
data: { label: 'A Node' },
position: { x: 250, y: 0 },
},
{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true },
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }];
const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 };
const NodeTypeChangeFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const changeType = () => {
setElements((elms) =>
elms.map((el) => {
if (isEdge(el) || el.type === 'input') {
return el;
setNodes((nds) =>
nds.map((node) => {
if (node.type === 'input') {
return node;
}
return {
...el,
type: el.type === 'default' ? 'output' : 'default',
...node,
type: node.type === 'default' ? 'output' : 'default',
};
})
);
};
return (
<ReactFlow elements={elements} onConnect={onConnect} onLoad={onLoad}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
>
<button onClick={changeType} style={buttonStyle}>
change type
</button>

View File

@@ -1,16 +1,18 @@
import React, { useState, CSSProperties, FC } from 'react';
import { useState, CSSProperties, FC } from 'react';
import ReactFlow, {
addEdge,
Elements,
Node,
Position,
Connection,
Edge,
NodeProps,
NodeTypesType,
NodeTypes,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '1',
sourcePosition: Position.Right,
@@ -41,7 +43,7 @@ const NodeB: FC<NodeProps> = () => {
};
type NodeTypesObject = {
[key: string]: NodeTypesType;
[key: string]: NodeTypes;
};
const nodeTypesObjects: NodeTypesObject = {
@@ -55,16 +57,19 @@ const nodeTypesObjects: NodeTypesObject = {
const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]}
nodeTypesId={nodeTypesId}
>
<button onClick={changeType} style={buttonStyle}>
change type

View File

@@ -1,54 +1,52 @@
import React, { useState, MouseEvent, CSSProperties } from 'react';
import { MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
Controls,
Background,
isNode,
Node,
Elements,
FlowElement,
OnLoadParams,
FlowTransform,
Viewport,
SnapGrid,
ArrowHeadType,
Connection,
Edge,
ReactFlowInstance,
useNodesState,
useEdgesState,
OnSelectionChangeParams,
} from 'react-flow-renderer';
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDoubleClick = (_: MouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: MouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: MouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: MouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: MouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: MouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => {
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
const onSelectionContextMenu = (event: ReactMouseEvent, nodes: Node[]) => {
event.preventDefault();
console.log('selection context menu', nodes);
};
const onElementClick = (_: MouseEvent, element: FlowElement) =>
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements);
const onLoad = (reactFlowInstance: OnLoadParams) => {
console.log('flow loaded:', reactFlowInstance);
reactFlowInstance.fitView();
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('node click:', node);
const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log('pane ready:', reactFlowInstance);
};
const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform);
const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform);
const onEdgeContextMenu = (_: MouseEvent, edge: Edge) => console.log('edge context menu', edge);
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse move', edge);
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge);
const onMoveStart = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move start', viewport);
const onMoveEnd = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move end', viewport);
const onEdgeContextMenu = (_: ReactMouseEvent, edge: Edge) => console.log('edge context menu', edge);
const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
@@ -122,10 +120,13 @@ const initialElements: Elements = [
position: { x: 100, y: 480 },
},
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
{ id: 'e4-5', source: '4', target: '5', arrowHeadType: ArrowHeadType.Arrow, label: 'edge with arrow head' },
{ id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' },
{ id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
{
id: 'e5-7',
@@ -158,15 +159,17 @@ const nodeColor = (n: Node): string => {
};
const OverviewFlow = () => {
const [elements, setElements] = useState(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
@@ -182,7 +185,7 @@ const OverviewFlow = () => {
onSelectionChange={onSelectionChange}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onLoad={onLoad}
onInit={onInit}
connectionLineStyle={connectionLineStyle}
snapToGrid={true}
snapGrid={snapGrid}
@@ -191,6 +194,13 @@ const OverviewFlow = () => {
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeDoubleClick={onEdgeDoubleClick}
fitView
fitViewOptions={{ padding: 0.2 }}
attributionPosition="top-right"
maxZoom={Infinity}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
deleteKeyCode={null}
>
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<Controls />

View File

@@ -1,13 +1,13 @@
import React from 'react';
import { useStoreState, useStoreActions } from 'react-flow-renderer';
import { useStore, useStoreApi } from 'react-flow-renderer';
const Sidebar = () => {
const nodes = useStoreState((store) => store.nodes);
const transform = useStoreState((store) => store.transform);
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements);
const store = useStoreApi();
const nodeInternals = useStore((store) => store.nodeInternals);
const transform = useStore((store) => store.transform);
const selectAll = () => {
setSelectedElements(nodes.map((node) => ({ id: node.id, type: node.type })));
nodeInternals.forEach((node) => (node.selected = true));
store.setState({ nodeInternals: new Map(nodeInternals) });
};
return (
@@ -20,9 +20,9 @@ const Sidebar = () => {
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
</div>
<div className="title">Nodes</div>
{nodes.map((node) => (
{Array.from(nodeInternals).map(([, node]) => (
<div key={node.id}>
Node {node.id} - x: {node.__rf.position.x.toFixed(2)}, y: {node.__rf.position.y.toFixed(2)}
Node {node.id} - x: {node.position.x.toFixed(2)}, y: {node.position.y.toFixed(2)}
</div>
))}

View File

@@ -1,37 +1,40 @@
import React, { useState, MouseEvent } from 'react';
import { MouseEvent } from 'react';
import ReactFlow, {
ReactFlowProvider,
addEdge,
removeElements,
Node,
Controls,
OnLoadParams,
FlowElement,
Connection,
Edge,
Elements,
ConnectionMode,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from 'react-flow-renderer';
import Sidebar from './Sidebar';
import './provider.css';
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('pane ready:', reactFlowInstance);
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
const ProviderFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
return (
<div className="providerflow">
@@ -39,11 +42,13 @@ const ProviderFlow = () => {
<Sidebar />
<div className="reactflow-wrapper">
<ReactFlow
elements={elements}
onElementClick={onElementClick}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onElementsRemove={onElementsRemove}
onLoad={onLoad}
onInit={onInit}
connectionMode={ConnectionMode.Loose}
>
<Controls />

View File

@@ -1,5 +1,5 @@
import React, { memo, useCallback, Dispatch, FC } from 'react';
import { useZoomPanHelper, OnLoadParams, Elements, FlowExportObject } from 'react-flow-renderer';
import { useReactFlow, ReactFlowInstance, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer';
import localforage from 'localforage';
localforage.config({
@@ -12,12 +12,13 @@ const flowKey = 'example-flow';
const getNodeId = () => `randomnode_${+new Date()}`;
type ControlsProps = {
rfInstance?: OnLoadParams;
setElements: Dispatch<React.SetStateAction<Elements<any>>>;
rfInstance?: ReactFlowInstance;
setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
};
const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
const { transform } = useZoomPanHelper();
const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
const { setViewport } = useReactFlow();
const onSave = useCallback(() => {
if (rfInstance) {
@@ -28,17 +29,18 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
const onRestore = useCallback(() => {
const restoreFlow = async () => {
const flow: FlowExportObject | null = await localforage.getItem(flowKey);
const flow: ReactFlowJsonObject | null = await localforage.getItem(flowKey);
if (flow) {
const [x = 0, y = 0] = flow.position;
setElements(flow.elements || []);
transform({ x, y, zoom: flow.zoom || 0 });
const { x, y, zoom } = flow.viewport;
setNodes(flow.nodes || []);
setEdges(flow.edges || []);
setViewport({ x, y, zoom: zoom || 0 });
}
};
restoreFlow();
}, [setElements, transform]);
}, [setNodes, setEdges, setViewport]);
const onAdd = useCallback(() => {
const newNode = {
@@ -46,8 +48,8 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
data: { label: 'Added node' },
position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight },
};
setElements((els) => els.concat(newNode));
}, [setElements]);
setNodes((nds) => nds.concat(newNode));
}, [setNodes]);
return (
<div className="save__controls">

View File

@@ -1,34 +1,43 @@
import React, { useState } from 'react';
import { useState } from 'react';
import ReactFlow, {
ReactFlowProvider,
removeElements,
Node,
addEdge,
Elements,
Connection,
Edge,
OnLoadParams,
ReactFlowInstance,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import Controls from './Controls';
import './save.css';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
{ id: 'e1-2', source: '1', target: '2' },
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const SaveRestore = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams>();
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlowProvider>
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect} onLoad={setRfInstance}>
<Controls rfInstance={rfInstance} setElements={setElements} />
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={setRfInstance}
>
<Controls rfInstance={rfInstance} setNodes={setNodes} setEdges={setEdges} />
</ReactFlow>
</ReactFlowProvider>
);

View File

@@ -1,58 +1,61 @@
import React, { useState, CSSProperties } from 'react';
import { useState, CSSProperties, useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
MiniMap,
isNode,
Controls,
Background,
OnLoadParams,
Elements,
Connection,
ReactFlowInstance,
Edge,
Node,
NodeChange,
applyNodeChanges,
Connection,
addEdge,
} from 'react-flow-renderer';
import { getElements } from './utils';
import { getNodesAndEdges } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => {
const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements());
console.log(reactFlowInstance.getNodes());
};
const initialElements: Elements = getElements(30, 30);
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
const StressFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const updatePos = () => {
setElements((elms) => {
return elms.map((el) => {
if (isNode(el)) {
return {
...el,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
}
return el;
setNodes((nds) => {
return nds.map((n) => {
return {
...n,
position: {
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
},
};
});
});
};
const updateElements = () => {
const grid = Math.ceil(Math.random() * 10);
setElements(getElements(grid, grid));
const initialElements = getNodesAndEdges(grid, grid);
setNodes(initialElements.nodes);
setEdges(initialElements.edges);
};
const onNodesChange = useCallback((changes: NodeChange[]) => {
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}>
<ReactFlow nodes={nodes} edges={edges} onInit={onInit} onConnect={onConnect} onNodesChange={onNodesChange}>
<MiniMap />
<Controls />
<Background />

View File

@@ -1,7 +1,13 @@
import { Elements } from 'react-flow-renderer';
import { Node, Edge } from 'react-flow-renderer';
export function getElements(xElements: number = 10, yElements: number = 10): Elements {
const initialElements = [];
type ElementsCollection = {
nodes: Node[];
edges: Edge[];
};
export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection {
const initialNodes = [];
const initialEdges: Edge[] = [];
let nodeId = 1;
let recentNodeId = null;
@@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
data,
position,
};
initialElements.push(node);
initialNodes.push(node);
if (recentNodeId && nodeId <= xElements * yElements) {
initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
}
recentNodeId = nodeId;
@@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
}
}
return initialElements;
return {
nodes: initialNodes,
edges: initialEdges,
};
}

View File

@@ -0,0 +1,27 @@
import { memo, FC, CSSProperties } from 'react';
import { Handle, NodeProps, Position } from 'react-flow-renderer';
const infoStyle: CSSProperties = { fontSize: 11 };
const idStyle: CSSProperties = {
fontSize: 10,
color: '#888899',
position: 'absolute',
top: 2,
left: 2,
};
const ColorSelectorNode: FC<NodeProps> = ({ zIndex, xPos, yPos, id }) => {
return (
<>
<Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div>
<div style={infoStyle}>
x:{Math.round(xPos || 0)} y:{Math.round(yPos || 0)} z:{zIndex}
</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(ColorSelectorNode);

View File

@@ -0,0 +1,197 @@
import { useState, MouseEvent, useCallback } from 'react';
import ReactFlow, {
addEdge,
Background,
MiniMap,
Controls,
Node,
Edge,
ReactFlowInstance,
Connection,
MarkerType,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import DebugNode from './DebugNode';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{
id: '4',
data: { label: 'Node 4' },
position: { x: 100, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255,50, 50, 0.5)', width: 500, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 15 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 150, y: 50 },
className: 'light',
style: { backgroundColor: 'rgba(50, 50, 255, 0.5)', height: 200, width: 300 },
parentNode: '4',
expandParent: true,
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 20 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 20, y: 100 },
className: 'light',
parentNode: '4b',
},
{
id: '5',
type: 'group',
data: { label: 'Node 5' },
position: { x: 650, y: 250 },
className: 'light',
style: { width: 400, height: 150 },
zIndex: 1000,
},
{
id: '5a',
data: { label: 'Node 5a' },
position: { x: 25, y: 50 },
className: 'light',
parentNode: '5',
},
{
id: '5b',
data: { label: 'Node 5b' },
position: { x: 225, y: 50 },
className: 'light',
parentNode: '5',
},
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
];
const initialEdges: Edge[] = [
{
id: 'e1-2',
source: '1',
target: '2',
markerEnd: { type: MarkerType.Arrow, strokeWidth: 2, width: 15, height: 15, color: '#f00' },
},
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', zIndex: 100 },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2', zIndex: 100 },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
const nodeTypes = {
default: DebugNode,
};
const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
if (!n.parentNode) {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
}
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
nodeTypes={nodeTypes}
fitView
>
<MiniMap />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default BasicFlow;

View File

@@ -1,26 +1,30 @@
import React, { useState, MouseEvent } from 'react';
import ReactFlow, { removeElements, addEdge, Node, FlowElement, Elements, Connection, Edge } from 'react-flow-renderer';
import { MouseEvent } from 'react';
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const elementsA: Elements = [
const nodesA: Node[] = [
{ id: '1a', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ 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 edgesA: Edge[] = [
{ id: 'e1-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' },
];
const elementsB: Elements = [
const nodesB: Node[] = [
{ id: 'inputb', type: 'input', data: { label: 'Input' }, position: { x: 300, y: 5 }, className: 'light' },
{ id: '1b', data: { label: 'Node 1' }, position: { x: 0, y: 100 }, className: 'light' },
{ id: '2b', data: { label: 'Node 2' }, position: { x: 200, y: 100 }, className: 'light' },
{ id: '3b', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' },
];
const edgesB: Edge[] = [
{ id: 'e1b', source: 'inputb', target: '1b' },
{ id: 'e2b', source: 'inputb', target: '2b' },
{ id: 'e3b', source: 'inputb', target: '3b' },
@@ -28,23 +32,39 @@ const elementsB: Elements = [
];
const BasicFlow = () => {
const [elements, setElements] = useState<Elements>(elementsA);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
>
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={() => setElements(elementsA)} style={{ marginRight: 5 }}>
<button
onClick={() => {
setNodes(nodesA);
setEdges(edgesA);
}}
style={{ marginRight: 5 }}
>
flow a
</button>
<button onClick={() => setElements(elementsB)}>flow b</button>
<button
onClick={() => {
setNodes(nodesB);
setEdges(edgesB);
}}
>
flow b
</button>
</div>
</ReactFlow>
);

View File

@@ -0,0 +1,50 @@
import { useCallback } from 'react';
import ReactFlow, {
Node,
Edge,
useNodesState,
useEdgesState,
Position,
Connection,
addEdge,
} from 'react-flow-renderer';
import './touch-device.css';
const initialNodes: Node[] = [
{
id: '1',
data: { label: 'Node 1' },
position: { x: 100, y: 100 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 300, y: 100 },
sourcePosition: Position.Right,
targetPosition: Position.Left,
},
];
const initialEdges: Edge[] = [];
const TouchDeviceFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback((connection: Connection) => setEdges((eds) => addEdge(connection, eds)), []);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
className="touchdevice-flow"
/>
);
};
export default TouchDeviceFlow;

View File

@@ -0,0 +1,19 @@
.touchdevice-flow .react-flow__handle {
width: 20px;
height: 20px;
border-radius: 3px;
background-color: #9f7aea;
}
.touchdevice-flow .react-flow__handle.connecting {
animation: bounce 1600ms infinite ease-out;
}
@keyframes bounce {
0% {
transform: translate(0, -50%) scale(1);
}
50% {
transform: translate(0, -50%) scale(1.1);
}
}

View File

@@ -1,67 +1,79 @@
import React, { useState, useCallback } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
NodeTypesType,
useReactFlow,
NodeTypes,
addEdge,
useZoomPanHelper,
ReactFlowProvider,
Elements,
Node,
Connection,
Edge,
ElementId,
ConnectionLineType,
ConnectionMode,
updateEdge,
ArrowHeadType,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import CustomNode from './CustomNode';
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '00',
type: 'custom',
position: { x: 300, y: 250 },
data: null,
},
{
id: '01',
type: 'custom',
position: { x: 100, y: 50 },
data: null,
},
{
id: '02',
type: 'custom',
position: { x: 500, y: 50 },
data: null,
},
{
id: '03',
type: 'custom',
position: { x: 500, y: 500 },
data: null,
},
{
id: '04',
type: 'custom',
position: { x: 100, y: 500 },
data: null,
},
{
id: '10',
type: 'custom',
position: { x: 300, y: 5 },
data: null,
},
{
id: '20',
type: 'custom',
position: { x: 600, y: 250 },
data: null,
},
{
id: '30',
type: 'custom',
position: { x: 300, y: 600 },
data: null,
},
{
id: '40',
type: 'custom',
position: { x: 5, y: 250 },
data: null,
},
];
const initialEdges: Edge[] = [
{
id: 'e0-1a',
source: '00',
@@ -69,7 +81,6 @@ const initialElements: Elements = [
sourceHandle: 'left',
targetHandle: 'bottom',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-1b',
@@ -78,7 +89,6 @@ const initialElements: Elements = [
sourceHandle: 'top',
targetHandle: 'right',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-2a',
@@ -87,7 +97,6 @@ const initialElements: Elements = [
sourceHandle: 'top',
targetHandle: 'left',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-2b',
@@ -96,7 +105,6 @@ const initialElements: Elements = [
sourceHandle: 'right',
targetHandle: 'bottom',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-3a',
@@ -105,7 +113,6 @@ const initialElements: Elements = [
sourceHandle: 'right',
targetHandle: 'top',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-3b',
@@ -114,7 +121,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom',
targetHandle: 'left',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-4a',
@@ -123,7 +129,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom',
targetHandle: 'right',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-4b',
@@ -132,7 +137,6 @@ const initialElements: Elements = [
sourceHandle: 'left',
targetHandle: 'top',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-10',
@@ -141,7 +145,6 @@ const initialElements: Elements = [
sourceHandle: 'top',
targetHandle: 'bottom',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-20',
@@ -150,7 +153,6 @@ const initialElements: Elements = [
sourceHandle: 'right',
targetHandle: 'left',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-30',
@@ -159,7 +161,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom',
targetHandle: 'top',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
{
id: 'e0-40',
@@ -168,31 +169,33 @@ const initialElements: Elements = [
sourceHandle: 'left',
targetHandle: 'right',
type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
},
];
const nodeTypes: NodeTypesType = {
const nodeTypes: NodeTypes = {
custom: CustomNode,
};
let id = 4;
const getId = (): ElementId => `${id++}`;
const getId = () => `${id++}`;
const UpdateNodeInternalsFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'default' }, els));
const { project } = useZoomPanHelper();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setElements((els) => updateEdge(oldEdge, newConnection, els));
setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onPaneClick = useCallback(
(evt) =>
setElements((els) =>
els.concat({
setNodes((nds) =>
nds.concat({
id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
type: 'custom',
data: null,
})
),
[project]
@@ -200,7 +203,10 @@ const UpdateNodeInternalsFlow = () => {
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onConnect={onConnect}
onPaneClick={onPaneClick}

View File

@@ -1,16 +1,19 @@
import React, { useState } from 'react';
import React, { useState, useCallback } from 'react';
import ReactFlow, {
Controls,
updateEdge,
addEdge,
Elements,
OnLoadParams,
applyNodeChanges,
applyEdgeChanges,
ReactFlowInstance,
Connection,
Edge,
removeElements,
Node,
NodeChange,
EdgeChange,
} from 'react-flow-renderer';
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
@@ -46,29 +49,41 @@ const initialElements: Elements = [
position: { x: 400, y: 100 },
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
},
{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' },
];
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }];
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge);
const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge);
const UpdatableEdge = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setElements((els) => updateEdge(oldEdge, newConnection, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onNodesChange = useCallback((changes: NodeChange[]) => {
console.log(changes);
setNodes((ns) => applyNodeChanges(changes, ns));
}, []);
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return (
<ReactFlow
elements={elements}
onLoad={onLoad}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true}
onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart}
onElementsRemove={onElementsRemove}
onEdgeUpdateEnd={onEdgeUpdateEnd}
>
<Controls />

View File

@@ -1,64 +1,75 @@
import React, { useEffect, useState } from 'react';
import ReactFlow, { Elements } from 'react-flow-renderer';
import { useEffect, useState } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import './updatenode.css';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
{ id: 'e1-2', source: '1', target: '2' },
];
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
const UpdateNode = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee');
const [nodeHidden, setNodeHidden] = useState<boolean>(false);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.data = {
...el.data,
n.data = {
...n.data,
label: nodeName,
};
}
return el;
return n;
})
);
}, [nodeName, setElements]);
}, [nodeName]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
el.style = { ...el.style, backgroundColor: nodeBg };
n.style = { ...n.style, backgroundColor: nodeBg };
}
return el;
return n;
})
);
}, [nodeBg, setElements]);
}, [nodeBg]);
useEffect(() => {
setElements((els) =>
els.map((el) => {
if (el.id === '1' || el.id === 'e1-2') {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value
el.isHidden = nodeHidden;
n.hidden = nodeHidden;
}
return el;
return n;
})
);
}, [nodeHidden, setElements]);
}, [nodeHidden]);
return (
<ReactFlow elements={elements} defaultZoom={1.5} minZoom={0.2} maxZoom={4}>
<ReactFlow
nodes={nodes}
edges={edges}
defaultZoom={1.5}
minZoom={0.2}
maxZoom={4}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
>
<div className="updatenode__controls">
<label>label:</label>
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />

View File

@@ -1,5 +1,4 @@
import React, { memo, FC, useMemo, CSSProperties } from 'react';
import { Handle, Position, NodeProps } from 'react-flow-renderer';
const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' };

View File

@@ -1,23 +1,23 @@
import React, { useState, useCallback, CSSProperties } from 'react';
import { useCallback, CSSProperties } from 'react';
import ReactFlow, {
NodeTypesType,
NodeTypes,
addEdge,
useZoomPanHelper,
useReactFlow,
ReactFlowProvider,
Elements,
Node,
Connection,
Edge,
ElementId,
useUpdateNodeInternals,
Position,
isEdge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import CustomNode from './CustomNode';
const initialHandleCount = 1;
const initialElements: Elements = [
const initialNodes: Node[] = [
{
id: '1',
type: 'custom',
@@ -28,23 +28,25 @@ const initialElements: Elements = [
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 };
const nodeTypes: NodeTypesType = {
const nodeTypes: NodeTypes = {
custom: CustomNode,
};
let id = 5;
const getId = (): ElementId => `${id++}`;
const getId = (): string => `${id++}`;
const UpdateNodeInternalsFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const updateNodeInternals = useUpdateNodeInternals();
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const { project } = useZoomPanHelper();
const { project } = useReactFlow();
const onPaneClick = useCallback(
(evt) =>
setElements((els) =>
els.concat({
setNodes((nds) =>
nds.concat({
id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
data: { label: 'new node' },
@@ -56,25 +58,17 @@ const UpdateNodeInternalsFlow = () => {
);
const toggleHandleCount = useCallback(() => {
setElements((els) =>
els.map((el) => {
if (isEdge(el)) {
return el;
}
return { ...el, data: { ...el.data, handleCount: el.data?.handleCount === 1 ? 2 : 1 } };
setNodes((nds) =>
nds.map((node) => {
return { ...node, data: { ...node.data, handleCount: node.data?.handleCount === 1 ? 2 : 1 } };
})
);
}, []);
const toggleHandlePosition = useCallback(() => {
setElements((els) =>
els.map((el) => {
if (isEdge(el)) {
return el;
}
return { ...el, data: { ...el.data, handlePosition: el.data?.handlePosition === 0 ? 1 : 0 } };
setNodes((nds) =>
nds.map((node) => {
return { ...node, data: { ...node.data, handlePosition: node.data?.handlePosition === 0 ? 1 : 0 } };
})
);
}, []);
@@ -82,7 +76,15 @@ const UpdateNodeInternalsFlow = () => {
const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]);
return (
<ReactFlow elements={elements} nodeTypes={nodeTypes} onConnect={onConnect} onPaneClick={onPaneClick}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes}
onConnect={onConnect}
onPaneClick={onPaneClick}
>
<div style={buttonWrapperStyles}>
<button onClick={toggleHandleCount}>toggle handle count</button>
<button onClick={toggleHandlePosition}>toggle handle position</button>

View File

@@ -1,42 +1,46 @@
import React, { useState, useCallback } from 'react';
import { useCallback } from 'react';
import ReactFlow, {
removeElements,
Node,
addEdge,
Background,
MiniMap,
useZoomPanHelper,
useReactFlow,
ReactFlowProvider,
Elements,
ElementId,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
const initialElements: Elements = [
const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
let id = 5;
const getId = (): ElementId => `${id++}`;
const getId = () => `${id++}`;
const UseZoomPanHelperFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const { project, setCenter, zoomIn, zoomOut } = useZoomPanHelper();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const { project, setCenter, zoomIn, zoomOut, fitView } = useReactFlow();
const onPaneClick = useCallback(
(evt) => {
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
setElements((els) =>
els.concat({
setNodes((nds) =>
nds.concat({
id: getId(),
position: projectedPosition,
data: {
@@ -48,25 +52,31 @@ const UseZoomPanHelperFlow = () => {
[project]
);
const onElementClick = useCallback(
const onNodeClick = useCallback(
(_, element) => {
const { x, y } = element.position;
setCenter(x, y, 1);
setCenter(x, y, { zoom: 1, duration: 1200 });
},
[setCenter]
);
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect}
onPaneClick={onPaneClick}
fitView
fitViewOptions={{ duration: 1200, padding: 0.2 }}
maxZoom={Infinity}
>
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}>
<button onClick={() => zoomIn(1200)}>zoomIn</button>
<button onClick={() => zoomOut(0)}>zoomOut</button>
<button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button>
<button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button>
</div>
<Background />
<MiniMap />

View File

@@ -1,27 +1,27 @@
import React, { useState, MouseEvent as ReactMouseEvent, FC } from 'react';
import { MouseEvent as ReactMouseEvent, FC } from 'react';
import ReactFlow, {
addEdge,
Handle,
OnLoadParams,
Connection,
Position,
Elements,
Node,
Edge,
OnConnectStartParams,
NodeProps,
NodeTypesType,
NodeTypes,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
import './validation.css';
const initialElements: Elements = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 } },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 } },
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 } },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 } },
const initialNodes: Node[] = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null },
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: null },
];
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
const isValidConnection = (connection: Connection) => connection.target === 'B';
const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) =>
console.log('on connect start', { nodeId, handleType });
@@ -43,31 +43,36 @@ const CustomNode: FC<NodeProps> = ({ id }) => (
</>
);
const nodeTypes: NodeTypesType = {
const nodeTypes: NodeTypes = {
custominput: CustomInput,
customnode: CustomNode,
};
const HorizontalFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements);
const ValidationFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => {
console.log('on connect', params);
setElements((els) => addEdge(params, els));
setEdges((eds) => addEdge(params, eds));
};
return (
<ReactFlow
elements={elements}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
selectNodesOnDrag={false}
onLoad={onLoad}
className="validationflow"
nodeTypes={nodeTypes}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
fitView
/>
);
};
export default HorizontalFlow;
export default ValidationFlow;

View File

@@ -1,89 +1,106 @@
import React, { ChangeEvent } from 'react';
import { ChangeEvent } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom';
import Overview from './Overview';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import Basic from './Basic';
import CustomNode from './CustomNode';
import Stress from './Stress';
import Interaction from './Interaction';
import Empty from './Empty';
import Edges from './Edges';
import Validation from './Validation';
import Provider from './Provider';
import Hidden from './Hidden';
import EdgeTypes from './EdgeTypes';
import ControlledUncontrolled from './ControlledUncontrolled';
import CustomConnectionLine from './CustomConnectionLine';
import CustomNode from './CustomNode';
import DefaultNodes from './DefaultNodes';
import DragHandle from './DragHandle';
import DragNDrop from './DragNDrop';
import Edges from './Edges';
import EdgeTypes from './EdgeTypes';
import Empty from './Empty';
import FloatingEdges from './FloatingEdges';
import Hidden from './Hidden';
import './index.css';
import Interaction from './Interaction';
import Layouting from './Layouting';
import MultiFlows from './MultiFlows';
import NestedNodes from './NestedNodes';
import NodeTypeChange from './NodeTypeChange';
import NodeTypesObjectChange from './NodeTypesObjectChange';
import Overview from './Overview';
import Provider from './Provider';
import SaveRestore from './SaveRestore';
import Stress from './Stress';
import Subflow from './Subflow';
import SwitchFlow from './Switch';
import TouchDevice from './TouchDevice';
import Undirectional from './Undirectional';
import UpdatableEdge from './UpdatableEdge';
import UpdateNode from './UpdateNode';
import SaveRestore from './SaveRestore';
import DragNDrop from './DragNDrop';
import Layout from './Layouting';
import SwitchFlows from './Switch';
import UseZoomPanHelper from './UseZoomPanHelper';
import UseUpdateNodeInternals from './UseUpdateNodeInternals';
import Undirectional from './Undirectional';
import MultiFlows from './MultiFlows';
import DragHandle from './DragHandle';
import UseZoomPanHelper from './UseZoomPanHelper';
import Validation from './Validation';
import './index.css';
const routes = [
{
path: '/',
component: Overview,
},
{
path: '/edges',
component: Edges,
},
{
path: '/custom-node',
component: CustomNode,
},
{
path: '/validation',
component: Validation,
},
{
path: '/provider',
component: Provider,
},
{
path: '/stress',
component: Stress,
},
{
path: '/interaction',
component: Interaction,
},
{
path: '/basic',
component: Basic,
},
{
path: '/empty',
component: Empty,
},
{
path: '/hidden',
component: Hidden,
},
{
path: '/edge-types',
component: EdgeTypes,
path: '/default-nodes',
component: DefaultNodes,
},
{
path: '/custom-connectionline',
component: CustomConnectionLine,
},
{
path: '/custom-node',
component: CustomNode,
},
{
path: '/draghandle',
component: DragHandle,
},
{
path: '/dragndrop',
component: DragNDrop,
},
{
path: '/edges',
component: Edges,
},
{
path: '/edge-types',
component: EdgeTypes,
},
{
path: '/empty',
component: Empty,
},
{
path: '/floating-edges',
component: FloatingEdges,
},
{
path: '/hidden',
component: Hidden,
},
{
path: '/interaction',
component: Interaction,
},
{
path: '/layouting',
component: Layouting,
},
{
path: '/multiflows',
component: MultiFlows,
},
{
path: '/nested-nodes',
component: NestedNodes,
},
{
path: '/nodetype-change',
component: NodeTypeChange,
@@ -93,28 +110,40 @@ const routes = [
component: NodeTypesObjectChange,
},
{
path: '/updatable-edge',
component: UpdatableEdge,
},
{
path: '/update-node',
component: UpdateNode,
path: '/provider',
component: Provider,
},
{
path: '/save-restore',
component: SaveRestore,
},
{
path: '/drag-and-drop',
component: DragNDrop,
path: '/stress',
component: Stress,
},
{
path: '/layouting',
component: Layout,
path: '/subflow',
component: Subflow,
},
{
path: '/switch',
component: SwitchFlows,
component: SwitchFlow,
},
{
path: '/touch-device',
component: TouchDevice,
},
{
path: '/undirectional',
component: Undirectional,
},
{
path: '/updatable-edge',
component: UpdatableEdge,
},
{
path: '/update-node',
component: UpdateNode,
},
{
path: '/usezoompanhelper',
@@ -125,46 +154,44 @@ const routes = [
component: UseUpdateNodeInternals,
},
{
path: '/undirectional',
component: Undirectional,
path: '/validation',
component: Validation,
},
{
path: '/multiflows',
component: MultiFlows,
},
{
path: '/draghandle',
component: DragHandle,
path: '/controlled-uncontrolled',
component: ControlledUncontrolled,
},
];
const Header = withRouter(({ history, location }) => {
const onChange = (event: ChangeEvent<HTMLSelectElement>) => history.push(event.target.value);
const Header = () => {
const navigate = useNavigate();
const location = useLocation();
const onChange = (event: ChangeEvent<HTMLSelectElement>) => navigate(event.target.value);
return (
<header>
<a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev
</a>
<select defaultValue={location.pathname} onChange={onChange}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.path === '/' ? 'overview' : route.path.substr(1, route.path.length)}
</option>
))}
</select>
</header>
<header>
<a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev
</a>
<select defaultValue={location.pathname} onChange={onChange}>
{routes.map((route) => (
<option value={route.path} key={route.path}>
{route.path === '/' ? 'overview' : route.path.substring(1, route.path.length)}
</option>
))}
</select>
</header>
);
});
};
ReactDOM.render(
<Router forceRefresh={true}>
<BrowserRouter>
<Header />
<Switch>
<Routes>
{routes.map((route) => (
<Route exact path={route.path} render={() => <route.component />} key={route.path} />
<Route path={route.path} key={route.path} element={<route.component />}/>
))}
</Switch>
</Router>,
</Routes>
</BrowserRouter>,
document.getElementById('root')
);

View File

@@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@@ -20,7 +16,6 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"],
"exclude": ["src_oldapi"]
}

View File

@@ -1,6 +1,7 @@
{
"internal": true,
"main": "../dist/nocss/ReactFlow-nocss.js",
"module": "../dist/nocss/ReactFlow-nocss.esm.js",
"type": "module",
"main": "../dist/nocss/index.js",
"module": "../dist/nocss/index.js",
"types": "../dist/nocss/index.d.ts"
}

1670
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,13 @@
{
"name": "react-flow-renderer",
"version": "9.7.4",
"version": "10.0.0-next.51",
"engines": {
"node": ">=12"
},
"main": "dist/ReactFlow.js",
"module": "dist/ReactFlow.esm.js",
"types": "dist/index.d.ts",
"type": "module",
"main": "dist/esm/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
@@ -28,51 +29,48 @@
"cy:open": "cypress open",
"release": "npm run test && release-it",
"release:notest": "release-it",
"release:next": "release-it --preRelease=next"
"release:next": "release-it --preRelease=next --no-git"
},
"dependencies": {
"@babel/runtime": "^7.16.7",
"@babel/runtime": "^7.17.2",
"classcat": "^5.0.3",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"react-draggable": "^4.4.4",
"react-redux": "^7.2.6",
"redux": "^4.1.2"
"zustand": "^3.7.1"
},
"devDependencies": {
"@babel/core": "^7.16.12",
"@babel/core": "^7.17.5",
"@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@babel/preset-typescript": "^7.16.7",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^3.0.1",
"@rollup/plugin-replace": "^4.0.0",
"@svgr/rollup": "^6.2.1",
"@types/d3": "^7.1.0",
"@types/react-redux": "^7.1.22",
"@types/redux": "^3.6.31",
"@types/resize-observer-browser": "^0.1.6",
"@types/react": "^17.0.40",
"@types/resize-observer-browser": "^0.1.7",
"autoprefixer": "^10.4.2",
"babel-preset-react-app": "^10.0.1",
"cypress": "^9.4.1",
"postcss": "^8.4.5",
"cypress": "^9.5.1",
"postcss": "^8.4.8",
"postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6",
"prettier": "2.5.1",
"prop-types": "^15.8.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"release-it": "^14.12.4",
"rollup": "^2.66.1",
"rollup-plugin-bundle-size": "^1.0.3",
"release-it": "^14.12.5",
"rollup": "^2.70.1",
"rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-serve": "^1.1.0",
"rollup-plugin-typescript2": "^0.31.1",
"rollup-plugin-typescript2": "^0.31.2",
"rollup-plugin-visualizer": "^5.6.0",
"start-server-and-test": "^1.14.0",
"typescript": "^4.5.4"
"typescript": "^4.6.2"
},
"peerDependencies": {
"react": "16 || 17",

View File

@@ -1,3 +0,0 @@
module.exports = {
plugins: [require('autoprefixer'), require('postcss-nested')],
};

View File

@@ -1,5 +0,0 @@
module.exports = {
trailingComma: 'es5',
singleQuote: true,
printWidth: 120
};

View File

@@ -2,11 +2,11 @@ import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss';
import bundleSize from 'rollup-plugin-bundle-size';
import replace from '@rollup/plugin-replace';
import svgr from '@svgr/rollup';
import typescript from 'rollup-plugin-typescript2';
import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core';
import { visualizer } from 'rollup-plugin-visualizer';
import pkg from './package.json';
@@ -14,38 +14,51 @@ const isProd = process.env.NODE_ENV === 'production';
const isTesting = process.env.NODE_ENV === 'testing';
const processEnv = isProd || isTesting ? 'production' : 'development';
export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injectCSS = true } = {}) => ({
input: 'src/index.ts',
external: ['react', 'react-dom', (id) => id.includes('@babel/runtime')],
export const baseConfig = ({ outputDir = 'dist/esm', injectCSS = true } = {}) => ({
input: [
'src/index.ts',
'src/additional-components/Controls/index.tsx',
'src/additional-components/Background/index.tsx',
'src/additional-components/MiniMap/index.tsx',
'src/hooks/useReactFlow.ts',
'src/hooks/useNodes.ts',
'src/hooks/useEdges.ts',
'src/hooks/useViewport.ts',
'src/hooks/useUpdateNodeInternals.ts',
],
external: [
'react',
'react-dom',
'classcat',
'd3-selection',
'd3-zoom',
'react-draggable',
'zustand',
'zustand/shallow',
'zustand/context',
(id) => id.includes('@babel/runtime'),
],
onwarn(warning, rollupWarn) {
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
rollupWarn(warning);
}
},
output: [
{
file: mainFile,
format: 'cjs',
sourcemap: true,
exports: 'named',
},
{
file: moduleFile,
format: 'esm',
sourcemap: true,
exports: 'named',
},
],
output: {
dir: outputDir,
format: 'esm',
sourcemap: true,
},
plugins: [
replace({
__ENV__: JSON.stringify(processEnv),
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
__INJECT_STYLES__: injectCSS,
preventAssignment: true,
}),
bundleSize(),
postcss({
minimize: isProd,
inject: injectCSS,
inject: false,
}),
svgr(),
resolve(),
@@ -60,6 +73,7 @@ export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injec
exclude: 'node_modules/**',
babelHelpers: 'runtime',
}),
visualizer(),
],
});
@@ -67,8 +81,7 @@ export default isProd && !isTesting
? [
baseConfig(),
baseConfig({
mainFile: 'dist/nocss/ReactFlow-nocss.js',
moduleFile: 'dist/nocss/ReactFlow-nocss.esm.js',
outputDir: 'dist/nocss',
injectCSS: false,
}),
]

View File

@@ -1,22 +1,17 @@
import React, { memo, useMemo, FC, HTMLAttributes } from 'react';
import React, { memo, useMemo, FC } from 'react';
import cc from 'classcat';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
import { useStore } from '../../store';
import { createGridLinesPath, createGridDotsPath } from './utils';
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
import { BackgroundVariant, ReactFlowState, BackgroundProps } from '../../types';
const defaultColors = {
[BackgroundVariant.Dots]: '#81818a',
[BackgroundVariant.Lines]: '#eee',
};
const transformSelector = (s: ReactFlowState) => s.transform;
const Background: FC<BackgroundProps> = ({
variant = BackgroundVariant.Dots,
gap = 15,
@@ -25,11 +20,11 @@ const Background: FC<BackgroundProps> = ({
style,
className,
}) => {
const [x, y, scale] = useStoreState((s) => s.transform);
const [x, y, scale] = useStore(transformSelector);
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []);
const bgClasses = cc(['react-flow__background', className]);
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;

View File

@@ -1,7 +1,8 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react';
import React, { memo, useCallback, FC, useEffect, useState } from 'react';
import cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import useReactFlow from '../../hooks/useReactFlow';
import PlusIcon from '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg';
@@ -9,21 +10,7 @@ import FitviewIcon from '../../../assets/icons/fitview.svg';
import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg';
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
import { FitViewParams } from '../../types';
export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
showZoom?: boolean;
showFitView?: boolean;
showInteractive?: boolean;
fitViewParams?: FitViewParams;
onZoomIn?: () => void;
onZoomOut?: () => void;
onFitView?: () => void;
onInteractiveChange?: (interactiveStatus: boolean) => void;
}
export interface ControlButtonProps extends HTMLAttributes<HTMLButtonElement> {}
import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
@@ -31,12 +18,14 @@ export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...
</button>
);
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<ControlProps> = ({
style,
showZoom = true,
showFitView = true,
showInteractive = true,
fitViewParams,
fitViewOptions,
onZoomIn,
onZoomOut,
onFitView,
@@ -44,11 +33,11 @@ const Controls: FC<ControlProps> = ({
className,
children,
}) => {
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false);
const setInteractive = useStoreActions((actions) => actions.setInteractive);
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useReactFlow();
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
const mapClasses = cc(['react-flow__controls', className]);
const onZoomInHandler = useCallback(() => {
@@ -62,14 +51,19 @@ const Controls: FC<ControlProps> = ({
}, [zoomOut, onZoomOut]);
const onFitViewHandler = useCallback(() => {
fitView?.(fitViewParams);
fitView?.(fitViewOptions);
onFitView?.();
}, [fitView, fitViewParams, onFitView]);
}, [fitView, fitViewOptions, onFitView]);
const onInteractiveChangeHandler = useCallback(() => {
setInteractive?.(!isInteractive);
store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive);
}, [isInteractive, setInteractive, onInteractiveChange]);
}, [isInteractive, onInteractiveChange]);
useEffect(() => {
setIsVisible(true);
@@ -83,21 +77,41 @@ const Controls: FC<ControlProps> = ({
<div className={mapClasses} style={style}>
{showZoom && (
<>
<ControlButton onClick={onZoomInHandler} className="react-flow__controls-zoomin">
<ControlButton
onClick={onZoomInHandler}
className="react-flow__controls-zoomin"
title="zoom in"
aria-label="zoom in"
>
<PlusIcon />
</ControlButton>
<ControlButton onClick={onZoomOutHandler} className="react-flow__controls-zoomout">
<ControlButton
onClick={onZoomOutHandler}
className="react-flow__controls-zoomout"
title="zoom out"
aria-label="zoom out"
>
<MinusIcon />
</ControlButton>
</>
)}
{showFitView && (
<ControlButton className="react-flow__controls-fitview" onClick={onFitViewHandler}>
<ControlButton
className="react-flow__controls-fitview"
onClick={onFitViewHandler}
title="fit view"
aria-label="fit view"
>
<FitviewIcon />
</ControlButton>
)}
{showInteractive && (
<ControlButton className="react-flow__controls-interactive" onClick={onInteractiveChangeHandler}>
<ControlButton
className="react-flow__controls-interactive"
onClick={onInteractiveChangeHandler}
title="toggle interactivity"
aria-label="toggle interactivity"
>
{isInteractive ? <UnlockIcon /> : <LockIcon />}
</ControlButton>
)}

View File

@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth,
className,
borderRadius,
shapeRendering
shapeRendering,
}: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string;

View File

@@ -1,27 +1,27 @@
import React, { memo, HTMLAttributes } from 'react';
import React, { memo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState } from '../../store/hooks';
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
import { Node, Rect } from '../../types';
import MiniMapNode from './MiniMapNode';
type StringFunc = (node: Node) => string;
import { useStore } from '../../store';
import { getRectOfNodes } from '../../utils/graph';
import { getBoundsofRects } from '../../utils';
export interface MiniMapProps extends HTMLAttributes<SVGSVGElement> {
nodeColor?: string | StringFunc;
nodeStrokeColor?: string | StringFunc;
nodeClassName?: string | StringFunc;
nodeBorderRadius?: number;
nodeStrokeWidth?: number;
maskColor?: string;
}
import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types';
declare const window: any;
const defaultWidth = 200;
const defaultHeight = 150;
const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
nodeInternals: s.nodeInternals,
});
const MiniMap = ({
style,
className,
@@ -32,20 +32,22 @@ const MiniMap = ({
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => {
const containerWidth = useStoreState((s) => s.width);
const containerHeight = useStoreState((s) => s.height);
const [tX, tY, tScale] = useStoreState((s) => s.transform);
const nodes = useStoreState((s) => s.nodes);
const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
const [tX, tY, tScale] = transform;
const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function
? nodeStrokeColor
: () => nodeStrokeColor) as StringFunc;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
const hasNodes = nodes && nodes.length;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
const nodeStrokeColorFunc = (
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
) as GetMiniMapNodeAttribute;
const nodeClassNameFunc = (
nodeClassName instanceof Function ? nodeClassName : () => nodeClassName
) as GetMiniMapNodeAttribute;
const hasNodes = nodeInternals && nodeInternals.size > 0;
// @TODO: work with nodeInternals instead of converting it to an array
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const bb = getRectOfNodes(nodes);
const viewBB: Rect = {
x: -tX / tScale,
@@ -64,7 +66,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2;
const height = viewHeight + offset * 2;
const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision";
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
return (
<svg
@@ -74,24 +76,28 @@ const MiniMap = ({
style={style}
className={mapClasses}
>
{nodes
.filter((node) => !node.isHidden)
.map((node) => (
<MiniMapNode
key={node.id}
x={node.__rf.position.x}
y={node.__rf.position.y}
width={node.__rf.width}
height={node.__rf.height}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
))}
{Array.from(nodeInternals)
.filter(([_, node]) => !node.hidden && node.width && node.height)
.map(([_, node]) => {
const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
return (
<MiniMapNode
key={node.id}
x={positionAbsolute?.x || 0}
y={positionAbsolute?.y || 0}
width={node.width!}
height={node.height!}
style={node.style}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
);
})}
<path
className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z

View File

@@ -1,16 +1,8 @@
import React, { FC, useMemo } from 'react';
import { Provider } from 'react-redux';
import React, { FC } from 'react';
import { initialState } from '../../store';
import configureStore from '../../store/configure-store';
import { Provider, createStore } from '../../store';
const ReactFlowProvider: FC = ({ children }) => {
const store = useMemo(() => {
return configureStore(initialState);
}, []);
return <Provider store={store}>{children}</Provider>;
};
const ReactFlowProvider: FC = ({ children }) => <Provider createStore={createStore}>{children}</Provider>;
ReactFlowProvider.displayName = 'ReactFlowProvider';

View File

@@ -1,5 +1,5 @@
// These components are not used by React Flow directly
// but the user can add them as children of a React Flow component
// They can be added as children of a React Flow component
export { default as MiniMap } from './MiniMap';
export { default as Controls, ControlButton } from './Controls';

View File

@@ -0,0 +1,33 @@
import React from 'react';
import cc from 'classcat';
import { AttributionPosition, ProOptions } from '../../types';
type AttributionProps = {
proOptions?: ProOptions;
position?: AttributionPosition;
};
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (
(proOptions?.account === 'paid-starter' || proOptions?.account === 'paid-enterprise') &&
proOptions?.hideAttribution
) {
return null;
}
const positionClasses = `${position}`.split('-');
return (
<div
className={cc(['react-flow__attribution', ...positionClasses])}
data-message="Please only hide this attribution when you have a pro account: https://pro.reactflow.dev/plans"
>
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
React Flow
</a>
</div>
);
}
export default Attribution;

View File

@@ -1,38 +1,39 @@
import React, { useEffect, useState, CSSProperties } from 'react';
import React, { useRef, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import {
ElementId,
Node,
Transform,
HandleElement,
Position,
ConnectionLineType,
ConnectionLineComponent,
HandleType,
Node,
ReactFlowState,
Position,
} from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
interface ConnectionLineProps {
connectionNodeId: ElementId;
connectionHandleId: ElementId | null;
connectionNodeId: string;
connectionHandleId: string | null;
connectionHandleType: HandleType;
connectionPositionX: number;
connectionPositionY: number;
connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean;
connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent;
}
const getSourceHandle = (handleId: ElementId | null, sourceNode: Node, connectionHandleType: HandleType) => {
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound =
sourceNode.__rf.handleBounds[connectionHandleType] || sourceNode.__rf.handleBounds[handleTypeInverted];
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
return handleId ? handleBound.find((d: HandleElement) => d.id === handleId) : handleBound[0];
const getSourceHandle = (handleId: string | null, sourceNode: Node, connectionHandleType: HandleType) => {
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound = sourceNode.handleBounds?.[connectionHandleType] || sourceNode.handleBounds?.[handleTypeInverted];
return handleId ? handleBound?.find((d: HandleElement) => d.id === handleId) : handleBound?.[0];
};
export default ({
@@ -43,29 +44,29 @@ export default ({
connectionPositionX,
connectionPositionY,
connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform,
isConnectable,
CustomConnectionLineComponent,
}: ConnectionLineProps) => {
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId;
const handleId = connectionHandleId;
useEffect(() => {
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
setSourceNode(nextSourceNode);
}, []);
const { nodeInternals, transform } = useStore(selector, shallow);
const sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
if (!sourceNode || !isConnectable) {
if (
!sourceNode.current ||
!sourceNode.current ||
!isConnectable ||
!sourceNode.current.handleBounds?.[connectionHandleType]
) {
return null;
}
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType);
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
const sourceHandle = getSourceHandle(handleId, sourceNode.current, connectionHandleType);
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (sourceNode.current?.width ?? 0) / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.current?.height ?? 0;
const sourceX = (sourceNode.current.positionAbsolute?.x || 0) + sourceHandleX;
const sourceY = (sourceNode.current.positionAbsolute?.y || 0) + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2];
@@ -85,7 +86,7 @@ export default ({
targetPosition={targetPosition}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode}
sourceNode={sourceNode.current as Node}
sourceHandle={sourceHandle}
/>
</g>
@@ -94,34 +95,26 @@ export default ({
let dAttr: string = '';
const pathParams = {
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) {
dAttr = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
dAttr = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
...pathParams,
borderRadius: 0,
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
dAttr = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
dAttr = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import EdgeText from './EdgeText';
import { BaseEdgeProps } from '../../types';
export default ({
path,
centerX,
centerY,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
}: BaseEdgeProps) => {
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
/>
) : null;
return (
<>
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} markerStart={markerStart} />
{text}
</>
);
};

View File

@@ -1,98 +1,111 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeProps, Position } from '../../types';
interface GetBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
centerX?: number;
centerY?: number;
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
centerX,
centerY,
}: GetBezierPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
const leftAndRight = [Position.Left, Position.Right];
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(targetPosition)) {
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(sourcePosition)) {
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
}
return path;
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
markerEndId,
}: EdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
const path = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
});
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
/>
) : null;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
return (
<>
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
{text}
</>
);
}
);
import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
import { getCenter } from './utils';
export interface GetBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
curvature?: number;
centerX?: number;
centerY?: number;
}
export function getBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
curvature = 0.25,
centerX,
centerY,
}: GetBezierPathParams): string {
const leftAndRight = [Position.Left, Position.Right];
const hasCurvature = curvature > 0;
let cX,
cY = 0;
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const distanceX = targetX - sourceX;
const absDistanceX = Math.abs(Math.min(0, distanceX));
const amtX = (Math.sqrt(absDistanceX) / 2) * (50 * curvature);
const hx1 = hasCurvature && distanceX < 0 ? sourceX + amtX : cX;
const hx2 = hasCurvature && distanceX < 0 ? targetX - amtX : cX;
return `M${sourceX},${sourceY} C${hx1},${sourceY} ${hx2},${targetY}, ${targetX},${targetY}`;
} else if (leftAndRight.includes(targetPosition)) {
return `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
} else if (leftAndRight.includes(sourcePosition)) {
return `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
}
cY = typeof centerY !== 'undefined' ? centerY : _centerY;
const distanceY = targetY - sourceY;
const absDistanceY = Math.abs(Math.min(0, distanceY));
const amtY = (Math.sqrt(absDistanceY) / 2) * (50 * curvature);
const hy1 = hasCurvature && distanceY < 0 ? sourceY + amtY : cY;
const hy2 = hasCurvature && distanceY < 0 ? targetY - amtY : cY;
return `M${sourceX},${sourceY} C${sourceX},${hy1} ${targetX},${hy2} ${targetX},${targetY}`;
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
curvature,
}: EdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
const path = getBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
curvature,
});
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
);
}
);

View File

@@ -0,0 +1,12 @@
import React, { memo } from 'react';
import BezierEdge, { getBezierPath, GetBezierPathParams } from './BezierEdge';
import { EdgeProps } from '../../types';
export function getSimpleBezierPath(props: GetBezierPathParams): string {
return getBezierPath({ ...props, curvature: 0 });
}
export default memo((props: EdgeProps) => {
return <BezierEdge {...props} curvature={0} />;
});

View File

@@ -1,8 +1,8 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { getCenter } from './utils';
import { EdgeSmoothStepProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
// These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes
@@ -21,7 +21,7 @@ const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
interface GetSmoothStepPathParams {
export interface GetSmoothStepPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
@@ -130,8 +130,8 @@ export default memo(
style,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
arrowHeadType,
markerEndId,
markerEnd,
markerStart,
borderRadius = 5,
}: EdgeSmoothStepProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
@@ -146,26 +146,21 @@ export default memo(
borderRadius,
});
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
) : null;
return (
<>
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
{text}
</>
);
}
);

View File

@@ -1,7 +1,6 @@
import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd } from './utils';
import BaseEdge from './BaseEdge';
import { EdgeProps } from '../../types';
export default memo(
@@ -17,39 +16,31 @@ export default memo(
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
markerEndId,
markerEnd,
markerStart,
}: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
const text = label ? (
<EdgeText
x={centerX}
y={centerY}
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
) : null;
return (
<>
<path
style={style}
className="react-flow__edge-path"
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
markerEnd={markerEnd}
/>
{text}
</>
);
}
);

View File

@@ -1,4 +1,5 @@
export { default as BezierEdge } from './BezierEdge';
export { default as StepEdge } from './StepEdge';
export { default as SimpleBezierEdge } from './SimpleBezierEdge';
export { default as SmoothStepEdge } from './SmoothStepEdge';
export { default as StepEdge } from './StepEdge';
export { default as StraightEdge } from './StraightEdge';
export { default as BezierEdge } from './BezierEdge';

View File

@@ -1,11 +1,11 @@
import { ArrowHeadType, Position } from '../../types';
import { MarkerType, Position } from '../../types';
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
if (typeof markerEndId !== 'undefined' && markerEndId) {
return `url(#${markerEndId})`;
}
return typeof arrowHeadType !== 'undefined' ? `url(#react-flow__${arrowHeadType})` : 'none';
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
};
export interface GetCenterParams {

View File

@@ -1,10 +1,17 @@
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { onMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
@@ -23,7 +30,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgPadding,
labelBgBorderRadius,
style,
arrowHeadType,
source,
target,
sourceX,
@@ -33,29 +39,27 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
sourcePosition,
targetPosition,
elementsSelectable,
markerEndId,
isHidden,
hidden,
sourceHandleId,
targetHandleId,
handleEdgeUpdate,
onConnectEdge,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const connectionMode = useStoreState((state) => state.connectionMode);
const store = useStoreApi();
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const [updating, setUpdating] = useState<boolean>(false);
const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
const edgeClasses = cc([
'react-flow__edge',
`react-flow__edge-${type}`,
@@ -89,8 +93,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) {
unsetNodesSelection();
addSelectedElements(edgeElement);
store.setState({ nodesSelectionActive: false });
addSelectedEdges([edgeElement.id]);
}
onClick?.(event, edgeElement);
@@ -146,21 +150,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement)
: undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
onMouseDown(
event,
handleId,
nodeId,
setConnectionNodeId,
setPosition,
store.setState,
onConnectEdge,
isTarget,
isValidConnection,
connectionMode,
isSourceHandle ? 'target' : 'source',
_onEdgeUpdate
_onEdgeUpdate,
store.getState
);
},
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge]
[id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
);
const onEdgeUpdaterSourceMouseDown = useCallback(
@@ -179,8 +192,10 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
if (isHidden) {
if (hidden) {
return null;
}
@@ -208,16 +223,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
arrowHeadType={arrowHeadType}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
markerEndId={markerEndId}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/>
{handleEdgeUpdate && (
<g

View File

@@ -1,20 +0,0 @@
import { useEffect } from 'react';
import { useStoreActions } from '../../store/hooks';
import { Elements } from '../../types';
interface ElementUpdaterProps {
elements: Elements;
}
const ElementUpdater = ({ elements }: ElementUpdaterProps) => {
const setElements = useStoreActions((actions) => actions.setElements);
useEffect(() => {
setElements(elements);
}, [elements]);
return null;
};
export default ElementUpdater;

View File

@@ -1,24 +1,19 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { SetState } from 'zustand';
import { getHostForElement } from '../../utils';
import {
ElementId,
XYPosition,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
OnConnectEndFunc,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
ConnectionMode,
SetConnectionId,
Connection,
HandleType,
ReactFlowState,
} from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetPosition = (pos: XYPosition) => void;
type Result = {
elementBelow: Element | null;
@@ -28,17 +23,15 @@ type Result = {
};
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
function checkElementBelowIsValid(
export function checkElementBelowIsValid(
event: MouseEvent,
connectionMode: ConnectionMode,
isTarget: boolean,
nodeId: ElementId,
handleId: ElementId | null,
nodeId: string,
handleId: string | null,
isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot
) {
// TODO: why does this throw an error? elementFromPoint should be available for ShadowRoot too
// @ts-ignore
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
@@ -91,19 +84,18 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown(
event: ReactMouseEvent,
handleId: ElementId | null,
nodeId: ElementId,
setConnectionNodeId: SetSourceIdFunc,
setPosition: SetPosition,
onConnect: OnConnectFunc,
handleId: string | null,
nodeId: string,
setState: SetState<ReactFlowState>,
onConnect: OnConnect,
isTarget: boolean,
isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc,
onConnectStop?: OnConnectStopFunc,
onConnectEnd?: OnConnectEndFunc
onConnectStart?: OnConnectStart,
onConnectStop?: OnConnectStop,
onConnectEnd?: OnConnectEnd
): void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document
@@ -113,7 +105,6 @@ export function onMouseDown(
return;
}
// @ts-ignore
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source');
@@ -126,18 +117,24 @@ export function onMouseDown(
const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element;
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
connectionNodeId: nodeId,
connectionHandleId: handleId,
connectionHandleType: handleType,
});
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) {
setPosition({
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
setState({
connectionPosition: {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
});
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
@@ -187,7 +184,11 @@ export function onMouseDown(
}
resetRecentHandle(recentHoveredHandle);
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);

View File

@@ -1,16 +1,29 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position } from '../../types';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { checkElementBelowIsValid, onMouseDown } from './handler';
import { getHostForElement } from '../../utils';
import { addEdge } from '../../utils/graph';
const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
const selector = (s: ReactFlowState) => ({
onConnectAction: s.onConnect,
onConnectStart: s.onConnectStart,
onConnectStop: s.onConnectStop,
onConnectEnd: s.onConnectEnd,
connectionMode: s.connectionMode,
connectionStartHandle: s.connectionStartHandle,
connectOnClick: s.connectOnClick,
hasDefaultEdges: s.hasDefaultEdges,
});
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
(
{
@@ -26,49 +39,64 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
},
ref
) => {
const nodeId = useContext(NodeIdContext) as ElementId;
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
const onConnectAction = useStoreState((state) => state.onConnect);
const onConnectStart = useStoreState((state) => state.onConnectStart);
const onConnectStop = useStoreState((state) => state.onConnectStop);
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
const connectionMode = useStoreState((state) => state.connectionMode);
const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string;
const {
onConnectAction,
onConnectStart,
onConnectStop,
onConnectEnd,
connectionMode,
connectionStartHandle,
connectOnClick,
hasDefaultEdges,
} = useStore(selector, shallow);
const handleId = id || null;
const isTarget = type === 'target';
const onConnectExtended = useCallback(
(params: Connection) => {
onConnectAction?.(params);
onConnect?.(params);
const { defaultEdgeOptions } = store.getState();
const edgeParams = {
...defaultEdgeOptions,
...params,
};
if (hasDefaultEdges) {
const { edges } = store.getState();
store.setState({ edges: addEdge(edgeParams, edges) });
}
onConnectAction?.(edgeParams);
onConnect?.(edgeParams);
},
[onConnectAction, onConnect]
[hasDefaultEdges, onConnectAction, onConnect]
);
const onMouseDownHandler = useCallback(
(event: React.MouseEvent) => {
onMouseDown(
event,
handleId,
nodeId,
setConnectionNodeId as unknown as SetSourceIdFunc,
setPosition as unknown as SetPosition,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
if (event.button === 0) {
onMouseDown(
event,
handleId,
nodeId,
store.setState,
onConnectExtended,
isTarget,
isValidConnection,
connectionMode,
undefined,
undefined,
onConnectStart,
onConnectStop,
onConnectEnd
);
}
},
[
handleId,
nodeId,
setConnectionNodeId,
setPosition,
onConnectExtended,
isTarget,
isValidConnection,
@@ -79,6 +107,47 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
]
);
const onClick = useCallback(
(event: React.MouseEvent) => {
if (!connectionStartHandle) {
onConnectStart?.(event, { nodeId, handleId, handleType: type });
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
} else {
const doc = getHostForElement(event.target as HTMLElement);
const { connection, isValid } = checkElementBelowIsValid(
event as unknown as MouseEvent,
connectionMode,
connectionStartHandle.type === 'target',
connectionStartHandle.nodeId,
connectionStartHandle.handleId || null,
isValidConnection,
doc
);
onConnectStop?.(event as unknown as MouseEvent);
if (isValid) {
onConnectExtended(connection);
}
onConnectEnd?.(event as unknown as MouseEvent);
store.setState({ connectionStartHandle: null });
}
},
[
connectionStartHandle,
onConnectStart,
onConnectExtended,
onConnectStop,
onConnectEnd,
isTarget,
nodeId,
handleId,
type,
]
);
const handleClasses = cc([
'react-flow__handle',
`react-flow__handle-${position}`,
@@ -88,6 +157,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
source: !isTarget,
target: isTarget,
connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
},
]);
@@ -98,6 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handlepos={position}
className={handleClasses}
onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref}
{...rest}
>

View File

@@ -8,13 +8,15 @@ const DefaultNode = ({
isConnectable,
targetPosition = Position.Top,
sourcePosition = Position.Bottom,
}: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
}: NodeProps) => {
return (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);
};
DefaultNode.displayName = 'DefaultNode';

View File

@@ -0,0 +1,5 @@
const GroupNode = () => null;
GroupNode.displayName = 'GroupNode';
export default GroupNode;

View File

@@ -5,7 +5,7 @@ import { NodeProps, Position } from '../../types';
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
<>
{data.label}
{data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</>
);

View File

@@ -6,7 +6,7 @@ import { NodeProps, Position } from '../../types';
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
<>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label}
{data?.label}
</>
);

View File

@@ -0,0 +1,25 @@
import { MouseEvent, useCallback } from 'react';
import { GetState } from 'zustand';
import { ReactFlowState, Node } from '../../types';
function useMemoizedMouseHandler(
id: string,
dragging: boolean,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
const memoizedHandler = useCallback(
(event: MouseEvent) => {
if (typeof handler !== 'undefined' && !dragging) {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
}
},
[handler, dragging, id]
);
return memoizedHandler;
}
export default useMemoizedMouseHandler;

View File

@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
export const getHandleBoundsByHandleType = (
selector: string,
nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect,
parentBounds: DOMRect,
k: number
): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector);
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map(
(handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
return handlesArray.map((handle): HandleElement => {
const bounds = handle.getBoundingClientRect();
const dimensions = getDimensions(handle);
const handleId = handle.getAttribute('data-handleid');
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
}
);
return {
id: handleId,
position: handlePosition,
x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k,
...dimensions,
};
});
};

View File

@@ -1,22 +1,21 @@
import React, {
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions } from '../../store/hooks';
import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions,
});
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
@@ -42,192 +41,188 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
selectNodesOnDrag,
sourcePosition,
targetPosition,
isHidden,
isInitialized,
hidden,
snapToGrid,
snapGrid,
isDragging,
dragging,
resizeObserver,
dragHandle,
zIndex,
isParent,
noPanClassName,
noDragClassName,
}: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
selector,
shallow
);
const nodeElement = useRef<HTMLDivElement>(null);
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex: selected ? 10 : 3,
zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents:
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
// prevents jumping of nodes on start
opacity: isInitialized ? 1 : 0,
pointerEvents: hasPointerEvents ? 'all' : 'none',
...style,
}),
[
selected,
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
]
[zIndex, xPos, yPos, hasPointerEvents, style]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
return;
}
return (event: MouseEvent) => onMouseEnter(event, node);
}, [onMouseEnter, isDragging, node]);
const grid = useMemo(
() => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
);
const onMouseMoveHandler = useMemo(() => {
if (!onMouseMove || isDragging) {
return;
}
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, isDragging, node]);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || isDragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, isDragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
const onSelectNodeHandler = useCallback(
(event: MouseEvent) => {
if (!isDraggable) {
if (isSelectable) {
unsetNodesSelection();
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
}
onClick?.(event, node);
if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
}
},
[isSelectable, selected, isDraggable, onClick, node]
[isSelectable, selected, isDraggable, onClick, id]
);
const onDragStart = useCallback(
(event: DraggableEvent) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection();
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
unsetNodesSelection();
addSelectedElements([]);
unselectNodesAndEdges();
store.setState({ nodesSelectionActive: false });
}
if (onNodeDragStart) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStart(event as MouseEvent, { ...node });
}
},
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
);
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
if (onNodeDrag) {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
updateNodePosDiff({
id,
diff: {
x: draggableData.deltaX,
y: draggableData.deltaY,
},
isDragging: true,
});
if (onNodeDrag) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDrag(event as MouseEvent, {
...node,
dragging: true,
position: {
x: node.position.x + draggableData.deltaX,
y: node.position.y + draggableData.deltaY,
},
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
},
});
}
},
[id, node, onNodeDrag]
[id, onNodeDrag]
);
const onDragStop = useCallback(
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) {
let node;
if (onClick || onNodeDragStop) {
node = store.getState().nodeInternals.get(id)!;
}
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements(node);
addSelectedNodes([id]);
}
onClick?.(event as MouseEvent, node);
if (onClick && node) {
onClick(event as MouseEvent, { ...node });
}
return;
}
updateNodePosDiff({
id: node.id,
isDragging: false,
updateNodePosition({
id,
dragging: false,
});
onNodeDragStop?.(event as MouseEvent, node);
if (onNodeDragStop && node) {
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
}
},
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
);
const onNodeDoubleClickHandler = useCallback(
(event: MouseEvent) => {
onNodeDoubleClick?.(event, node);
},
[node, onNodeDoubleClick]
);
useLayoutEffect(() => {
if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => {
if (nodeElement.current) {
if (nodeElement.current && !hidden) {
const currNode = nodeElement.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, []);
}, [hidden]);
if (isHidden) {
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== type;
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
if (hidden) {
return null;
}
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
]);
@@ -238,7 +233,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onStop={onDragStop}
scale={scale}
disabled={!isDraggable}
cancel=".nodrag"
cancel={`.${noDragClassName}`}
nodeRef={nodeElement}
grid={grid}
enableUserSelectHack={false}
@@ -267,8 +262,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isDragging={isDragging}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>

View File

@@ -1,57 +1,53 @@
/**
* The nodes selection rectangle gets displayed when a user
* made a selectio with on or several nodes
* made a selection with on or several nodes
*/
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable';
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import { DraggableCore, DraggableData } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState, useStoreActions } from '../../store/hooks';
import { isNode } from '../../utils/graph';
import { Node } from '../../types';
import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
updateNodePosition: s.updateNodePosition,
});
export default ({
function NodesSelection({
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
}: NodesSelectionProps) => {
const [tX, tY, tScale] = useStoreState((state) => state.transform);
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
const selectionActive = useStoreState((state) => state.selectionActive);
const selectedElements = useStoreState((state) => state.selectedElements);
const snapToGrid = useStoreState((state) => state.snapToGrid);
const snapGrid = useStoreState((state) => state.snapGrid);
const nodes = useStoreState((state) => state.nodes);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
noPanClassName,
}: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
selector,
shallow
);
const [tX, tY, tScale] = transform;
const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const selectedNodes = useMemo(
() =>
selectedElements
? selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
return {
...matchingNode,
position: matchingNode?.__rf.position,
} as Node;
})
: [],
[selectedElements, nodes]
);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
@@ -59,6 +55,8 @@ export default ({
[tX, tY, tScale]
);
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
@@ -78,25 +76,23 @@ export default ({
const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) {
onSelectionDrag(event, selectedNodes);
}
updateNodePosDiff({
updateNodePosition({
diff: {
x: data.deltaX,
y: data.deltaY,
},
isDragging: true,
dragging: true,
});
onSelectionDrag?.(event, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosDiff]
[onSelectionDrag, selectedNodes, updateNodePosition]
);
const onStop = useCallback(
(event: MouseEvent) => {
updateNodePosDiff({
isDragging: false,
updateNodePosition({
dragging: false,
});
onSelectionDragStop?.(event, selectedNodes);
@@ -106,22 +102,18 @@ export default ({
const onContextMenu = useCallback(
(event: MouseEvent) => {
const selectedNodes = selectedElements
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
: [];
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu]
[onSelectionContextMenu, selectedNodes]
);
if (!selectedElements || selectionActive) {
if (!selectedNodes?.length || userSelectionActive) {
return null;
}
return (
<div className="react-flow__nodesselection" style={style}>
<ReactDraggable
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<DraggableCore
scale={tScale}
grid={grid}
onStart={(event) => onStart(event as MouseEvent)}
@@ -136,7 +128,9 @@ export default ({
onContextMenu={onContextMenu}
style={innerStyle}
/>
</ReactDraggable>
</DraggableCore>
</div>
);
};
}
export default memo(NodesSelection);

View File

@@ -1,20 +1,38 @@
import { useEffect } from 'react';
import { memo, useEffect } from 'react';
import shallow from 'zustand/shallow';
import { Elements } from '../../types';
import { useStoreState } from '../../store/hooks';
import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
import { useStore } from '../../store';
interface SelectionListenerProps {
onSelectionChange: (elements: Elements | null) => void;
onSelectionChange: OnSelectionChangeFunc;
}
// This is a helper component for calling the onSelectionChange listener
const selector = (s: ReactFlowState) => ({
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
export default ({ onSelectionChange }: SelectionListenerProps) => {
const selectedElements = useStoreState((s) => s.selectedElements);
const areEqual = (objA: any, objB: any) => {
const selectedNodeIdsA = objA.selectedNodes.map((n: Node) => n.id);
const selectedNodeIdsB = objB.selectedNodes.map((n: Node) => n.id);
const selectedEdgeIdsA = objA.selectedEdges.map((e: Edge) => e.id);
const selectedEdgeIdsB = objB.selectedEdges.map((e: Edge) => e.id);
return shallow(selectedNodeIdsA, selectedNodeIdsB) && shallow(selectedEdgeIdsA, selectedEdgeIdsB);
};
// This is just a helper component for calling the onSelectionChange listener.
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => {
onSelectionChange(selectedElements);
}, [selectedElements]);
onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedNodes, selectedEdges]);
return null;
};
}
export default memo(SelectionListener);

View File

@@ -0,0 +1,161 @@
import { useEffect } from 'react';
import { SetState } from 'zustand';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import {
Node,
Edge,
ReactFlowState,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
CoordinateExtent,
OnNodesChange,
OnEdgesChange,
ConnectionMode,
SnapGrid,
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete
} from '../../types';
interface StoreUpdaterProps {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
maxZoom?: number;
nodeExtent?: CoordinateExtent;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
elementsSelectable?: boolean;
connectionMode?: ConnectionMode;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
translateExtent?: CoordinateExtent;
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
fitView?: boolean;
fitViewOptions?: FitViewOptions;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
}
const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent,
setNodeExtent: s.setNodeExtent,
reset: s.reset,
});
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
useEffect(() => {
if (typeof value !== 'undefined') {
setStoreState(value);
}
}, [value]);
}
function useDirectStoreUpdater(key: keyof ReactFlowState, value: any, setState: SetState<ReactFlowState>) {
useEffect(() => {
if (typeof value !== 'undefined') {
// @ts-ignore
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
nodesDraggable,
nodesConnectable,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
}: StoreUpdaterProps) => {
const {
setNodes,
setEdges,
setDefaultNodesAndEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
} = useStore(selector, shallow);
const store = useStoreApi();
useEffect(() => {
setDefaultNodesAndEdges(defaultNodes, defaultEdges);
return () => {
reset();
};
}, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
useDirectStoreUpdater('onConnect', onConnect, store.setState);
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges);
useStoreUpdater<Node[]>(defaultNodes, setNodes);
useStoreUpdater<Edge[]>(defaultEdges, setEdges);
useStoreUpdater<number>(minZoom, setMinZoom);
useStoreUpdater<number>(maxZoom, setMaxZoom);
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
return null;
};
export default StoreUpdater;

View File

@@ -2,100 +2,160 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/
import React, { memo } from 'react';
import React, { memo, useState, useRef, useCallback } from 'react';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks';
import { XYPosition } from '../../types';
import { useStore, useStoreApi } from '../../store';
import { getSelectionChanges } from '../../utils/changes';
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
type SelectionRect = Rect & {
startX: number;
startY: number;
draw: boolean;
};
type UserSelectionProps = {
selectionKeyPressed: boolean;
};
function getMousePosition(event: React.MouseEvent): XYPosition | void {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
return {
x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
};
}
const SelectionRect = () => {
const userSelectionRect = useStoreState((state) => state.userSelectionRect);
const selector = (s: ReactFlowState) => ({
userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
});
if (!userSelectionRect.draw) {
return null;
}
return (
<div
className="react-flow__selection"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
);
const initialRect: SelectionRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false,
};
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const selectionActive = useStoreState((state) => state.selectionActive);
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
const store = useStoreApi();
const prevSelectedNodesCount = useRef<number>(0);
const prevSelectedEdgesCount = useRef<number>(0);
const containerBounds = useRef<DOMRect>();
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
const { userSelectionActive, elementsSelectable } = useStore(selector, shallow);
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
const resetUserSelection = useCallback(() => {
setUserSelectionRect(initialRect);
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
}, []);
const onMouseDown = useCallback((event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
const mousePos = getMousePosition(event, containerBounds.current!);
setUserSelectionRect({
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
});
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
}, []);
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
return;
}
const mousePos = getMousePosition(event, containerBounds.current!);
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
prevSelectedNodesCount.current = selectedNodeIds.length;
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
prevSelectedEdgesCount.current = selectedEdgeIds.length;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
}
}
setUserSelectionRect(nextUserSelectRect);
};
const onMouseUp = useCallback(() => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
resetUserSelection();
}, []);
const onMouseLeave = useCallback(() => {
store.setState({ nodesSelectionActive: false });
resetUserSelection();
}, []);
if (!elementsSelectable || !renderUserSelectionPane) {
return null;
}
const onMouseDown = (event: React.MouseEvent): void => {
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
setUserSelection(mousePos);
};
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !selectionActive) {
return;
}
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
updateUserSelection(mousePos);
};
const onMouseUp = () => unsetUserSelection();
const onMouseLeave = () => {
unsetUserSelection();
unsetNodesSelection();
};
return (
<div
className="react-flow__selectionpane"
className="react-flow__selectionpane react-flow__container"
onMouseDown={onMouseDown}
onMouseMove={onMouseMove}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
>
<SelectionRect />
{userSelectionRect.draw && (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
)}
</div>
);
});

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