Merge branch 'main' into feature/edgeUpdateWithMoreInfo-1961

This commit is contained in:
Moritz Klack
2022-04-11 13:28:45 +02:00
committed by GitHub
162 changed files with 34920 additions and 27001 deletions
+1
View File
@@ -6,6 +6,7 @@
"modules": false "modules": false
} }
], ],
"@babel/preset-typescript",
"@babel/preset-react" "@babel/preset-react"
], ],
"plugins": ["@babel/plugin-transform-runtime"] "plugins": ["@babel/plugin-transform-runtime"]
-9
View File
@@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: '/'
schedule:
interval: monthly
time: '04:00'
open-pull-requests-limit: 10
target-branch: main
+7
View File
@@ -0,0 +1,7 @@
{
"plugins": {
"autoprefixer" : {},
"postcss-nested": {}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 120
}
+1 -1
View File
@@ -7,6 +7,6 @@
}, },
"hooks": { "hooks": {
"after:bump": "npm run build", "after:bump": "npm run build",
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}." "after:release": "echo Successfully released ${name} v${version}."
} }
} }
-24
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
+1 -1
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` 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` 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 4. Run tests `npm test` and add new ones if you are introducing a new feature
+18 -4
View File
@@ -1,7 +1,21 @@
Copyright 2022 webkid GmbH MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Copyright (c) 2019-2022 webkid GmbH
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+65 -39
View File
@@ -1,51 +1,67 @@
[![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 [🚀 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/6ZcAMJmJ) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
- **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
React Flow was initially created for [datablocks](https://datablocks.pro). A node-based editor for transforming, analyzing and visualizing data. </div>
----
## 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/)
## Attribution
React Flow includes a small attribution that links to the React Flow website. Per default the attribution is displayed in the bottom right corner. With the attribution we want to achieve that companies who are using React Flow in commercial applications finance the development of the library.
We expect users who are using React Flow commercially to subscribe to React Flow Pro if they want to remove the attribution. In non-commercial applications you may hide the attribution without subscribing but are welcome to [sponsor us on Github](https://github.com/sponsors/wbkd).
## Installation ## Installation
``` The easiest way to get the latest version of React Flow is to install it via npm:
```bash
npm install react-flow-renderer npm install react-flow-renderer
``` ```
## Quick Start ## 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 ```jsx
import React from 'react'; import ReactFlow, { MiniMap, Controls } from 'react-flow-renderer';
import ReactFlow from 'react-flow-renderer';
const elements = [ function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, return (
// you can also pass a React component as a label <ReactFlow
{ id: '2', data: { label: <div>Node 2</div> }, position: { x: 100, y: 100 } }, nodes={nodes}
{ id: 'e1-2', source: '1', target: '2', animated: true }, edges={edges}
]; onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
const BasicFlow = () => <ReactFlow elements={elements} />; onConnect={onConnect}
>
<MiniMap />
<Controls />
</ReactFlow>
);
}
``` ```
## Community Packages
* [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`. 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: If you want to contribute or develop custom features the easiest way is to start the dev server:
@@ -70,19 +86,29 @@ Testing is done with cypress. You can find the tests in the [`integration/flow`]
npm run test npm run test
``` ```
## Maintainer ## Maintainers
Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick) React Flow is developed and maintained by [webkid](https://webkid.io), a web development agency with 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:
## Support * 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)
If you need custom support or features for your application we are [happy to hear from you](https://webkid.io/contact). You can also use our [contact form](https://pro.reactflow.dev/contact) or join the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
## Community Packages
### Thanks! * [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
Special thanks to [Andy Lindemann](https://github.com/AndyLnd) for a lot of helpful contributions! ## 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:
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)! * [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).
+2 -1
View File
@@ -2,5 +2,6 @@
"baseUrl": "http://localhost:3000", "baseUrl": "http://localhost:3000",
"viewportWidth": 1280, "viewportWidth": 1280,
"viewportHeight": 720, "viewportHeight": 720,
"video": false "video": false,
"pluginsFile": false
} }
+17 -17
View File
@@ -33,7 +33,7 @@ describe('Basic Flow Rendering', () => {
}); });
it('deselects node', () => { 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'); cy.get('.react-flow__node:first').should('not.have.class', 'selected');
}); });
@@ -42,7 +42,7 @@ describe('Basic Flow Rendering', () => {
}); });
it('deselects edge', () => { 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'); cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
}); });
@@ -50,9 +50,9 @@ describe('Basic Flow Rendering', () => {
cy.get('body') cy.get('body')
.type('{shift}', { release: false }) .type('{shift}', { release: false })
.get('.react-flow__selectionpane') .get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true }) .trigger('mousedown', 1000, 1, { which: 1, force: true })
.trigger('mousemove', 800, 75, { which: 1 }) .trigger('mousemove', 1, 200, { which: 1 })
.trigger('mouseup', 'bottomRight', { force: true }) .trigger('mouseup', 1, 200, { force: true })
.get('.react-flow__node') .get('.react-flow__node')
.first() .first()
.should('have.class', 'selected') .should('have.class', 'selected')
@@ -68,9 +68,9 @@ describe('Basic Flow Rendering', () => {
cy.get('body') cy.get('body')
.type('{shift}', { release: false }) .type('{shift}', { release: false })
.get('.react-flow__selectionpane') .get('.react-flow__selectionpane')
.trigger('mousedown', 'topLeft', { which: 1, force: true }) .trigger('mousedown', 'topRight', { which: 1, force: true })
.trigger('mousemove', 'bottomRight', { which: 1 }) .trigger('mousemove', 'bottomLeft', { which: 1 })
.trigger('mouseup', 'bottomRight', { force: true }) .trigger('mouseup', 'bottomLeft', { force: true })
.get('.react-flow__node') .get('.react-flow__node')
.should('have.class', 'selected') .should('have.class', 'selected')
.get('.react-flow__nodesselection-rect'); .get('.react-flow__nodesselection-rect');
@@ -79,7 +79,7 @@ describe('Basic Flow Rendering', () => {
}); });
it('removes selection', () => { 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'); cy.get('.react-flow__nodesselection-rect').should('not.exist');
}); });
@@ -108,12 +108,12 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 3') .contains('Node 3')
.find('.react-flow__handle.source') .find('.react-flow__handle.source')
.trigger('mousedown', { which: 1 }); .trigger('mousedown', { button: 0 });
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 4') .contains('Node 4')
.find('.react-flow__handle.target') .find('.react-flow__handle.target')
.trigger('mousemove') .trigger('mousemove', { force: true })
.trigger('mouseup', { force: true }); .trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 2); cy.get('.react-flow__edge').should('have.length', 2);
@@ -127,29 +127,29 @@ describe('Basic Flow Rendering', () => {
}); });
it('drags the pane', () => { 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 // for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441 // https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => { cy.window().then((win) => {
cy.get('.react-flow__renderer') cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win }) .trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win }) .trigger('mouseup', { force: true, view: win })
.then(() => { .then(() => {
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag); expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
}); });
}); });
}); });
it('zooms the pane', () => { 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 }) .trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom); expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
}); });
}); });
+8 -8
View File
@@ -8,30 +8,30 @@ describe('Controls Testing', () => {
}); });
it('zooms in', () => { 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') cy.get('.react-flow__controls-zoomin')
.click() .click()
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom); expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
}); });
}); });
it('zooms out', () => { 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') cy.get('.react-flow__controls-zoomout')
.click() .click()
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom); expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
}); });
}); });
// view is already fitted so we drag the pane to un-fit it // view is already fitted so we drag the pane to un-fit it
it('drags the pane', () => { 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 // for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441 // https://github.com/cypress-io/cypress/issues/3441
@@ -41,19 +41,19 @@ describe('Controls Testing', () => {
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win }) .trigger('mouseup', { force: true, view: win })
.then(() => { .then(() => {
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag); expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
}); });
}); });
}); });
it('fits view', () => { 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') cy.get('.react-flow__controls-fitview')
.click() .click()
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom); expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
}); });
}); });
+1 -11
View File
@@ -20,18 +20,13 @@ describe('Empty Flow Rendering', () => {
cy.get('body').type('{shift}', { release: true }); 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', () => { it('adds two nodes', () => {
cy.contains('add node').click(); cy.contains('add node').click();
cy.contains('add node').click(); cy.contains('add node').click();
}); });
it('connects nodes', () => { 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') cy.get('.react-flow__node')
.last() .last()
@@ -41,9 +36,4 @@ describe('Empty Flow Rendering', () => {
cy.get('.react-flow__edge').should('have.length', 1); 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);
});
}); });
+11 -74
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 = [ const nodes = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, { 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' }, { id: 'e2-3', source: '2', target: '3' },
]; ];
const elements = [...nodes, ...edges];
describe('Graph Utils Testing', () => { describe('Graph Utils Testing', () => {
it('tests isNode function', () => { it('tests isNode function', () => {
expect(isNode(nodes[0])).to.be.true; expect(isNode(nodes[0])).to.be.true;
@@ -27,41 +25,42 @@ describe('Graph Utils Testing', () => {
}); });
it('tests getOutgoers function', () => { it('tests getOutgoers function', () => {
const outgoers = getOutgoers(nodes[0], elements); const outgoers = getOutgoers(nodes[0], nodes, edges);
expect(outgoers.length).to.be.equal(2); 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); expect(noOutgoers.length).to.be.equal(0);
}); });
it('tests getIncomers function', () => { it('tests getIncomers function', () => {
const incomers = getIncomers(nodes[2], elements); const incomers = getIncomers(nodes[2], nodes, edges);
expect(incomers.length).to.be.equal(2); 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); expect(noIncomers.length).to.be.equal(0);
}); });
describe('tests addEdge function', () => { describe('tests addEdge function', () => {
it('adds edge', () => { it('adds edge', () => {
const newEdge = { source: '1', target: '4' }; 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', () => { it('tries to add existing edge', () => {
const newEdge = { source: '2', target: '3' }; 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', () => { it('tries to add invalid edge', () => {
const newEdge = { nosource: '1', notarget: '3' }; const newEdge = { nosource: '1', notarget: '3' };
try { try {
addEdge(newEdge, elements); addEdge(newEdge, edges);
} catch (e) { } catch (e) {
console.log(e.message); 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);
});
});
}); });
+11 -11
View File
@@ -3,6 +3,16 @@ describe('Hidden Flow Rendering', () => {
cy.visit('/hidden'); 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', () => { it('renders initial flow', () => {
cy.get('.react-flow__renderer'); cy.get('.react-flow__renderer');
cy.get('.react-flow__node').should('have.length', 4); 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); 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(); 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__edge').should('not.exist');
cy.get('.react-flow__minimap-node').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);
});
}); });
+9 -8
View File
@@ -93,7 +93,8 @@ describe('Interaction Flow Rendering', () => {
}); });
it('selects an edge by click', () => { 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', () => { it('toggles connectable mode', () => {
@@ -104,7 +105,7 @@ describe('Interaction Flow Rendering', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 3') .contains('Node 3')
.find('.react-flow__handle.source') .find('.react-flow__handle.source')
.trigger('mousedown', { which: 1 }); .trigger('mousedown', { button: 0 });
cy.get('.react-flow__node') cy.get('.react-flow__node')
.contains('Node 4') .contains('Node 4')
@@ -120,12 +121,12 @@ describe('Interaction Flow Rendering', () => {
}); });
it('zooms by scroll', () => { 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 }) .trigger('wheel', 'topLeft', { deltaY: 200 })
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom); expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
}); });
}); });
@@ -136,12 +137,12 @@ describe('Interaction Flow Rendering', () => {
it('zooms by double click', () => { it('zooms by double click', () => {
cy.get('.react-flow__controls-zoomout').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() .dblclick()
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom); expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
}); });
}); });
+3 -5
View File
@@ -19,7 +19,7 @@ describe('Minimap Testing', () => {
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox'); const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d'); const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
cy.get('.react-flow__renderer') cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 }) .trigger('wheel', 'topLeft', { deltaY: -200 })
.then(() => { .then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox'); const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
@@ -33,16 +33,14 @@ describe('Minimap Testing', () => {
it('changes node position', () => { it('changes node position', () => {
const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x'); const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y'); 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.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 xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y'); 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(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag); 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 // for d3 we have to pass the window to the event
// https://github.com/cypress-io/cypress/issues/3441 // https://github.com/cypress-io/cypress/issues/3441
cy.window().then((win) => { cy.window().then((win) => {
cy.get('.react-flow__renderer') cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win }) .trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 'bottomLeft')
.trigger('mouseup', { force: true, view: win }) .trigger('mouseup', { force: true, view: win })
-17
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
}
+27630 -21394
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -8,9 +8,9 @@
"react": "file:../node_modules/react", "react": "file:../node_modules/react",
"react-dom": "file:../node_modules/react-dom", "react-dom": "file:../node_modules/react-dom",
"react-flow-renderer": "file:../", "react-flow-renderer": "file:../",
"react-router-dom": "^5.3.0", "react-router-dom": "^6.2.1",
"react-scripts": "4.0.3", "react-scripts": "5.0.0",
"typescript": "^4.4.3" "typescript": "^4.5.5"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
@@ -34,10 +34,10 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@types/dagre": "^0.7.46", "@types/dagre": "^0.7.47",
"@types/localforage": "0.0.34", "@types/localforage": "0.0.34",
"@types/react": "^17.0.27", "@types/react": "file:../node_modules/@types/react",
"@types/react-dom": "^17.0.9", "@types/react-dom": "^17.0.11",
"@types/react-router-dom": "^5.3.0" "@types/react-router-dom": "^5.3.3"
} }
} }
+37 -42
View File
@@ -1,80 +1,69 @@
import React, { useState, MouseEvent } from 'react'; import { MouseEvent } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements, ReactFlowProvider,
addEdge,
isNode,
Background, Background,
Elements,
BackgroundVariant, BackgroundVariant,
FlowElement,
Node, Node,
Edge, Edge,
Connection, useReactFlow,
OnLoadParams,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); 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: '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: '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: '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' }, { 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-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const BasicFlow = () => { const BasicFlow = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null); const instance = useReactFlow();
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 updatePos = () => { const updatePos = () => {
setElements((elms) => { instance.setNodes((nodes) =>
return elms.map((el) => { nodes.map((node) => {
if (isNode(el)) { node.position = {
el.position = { x: Math.random() * 400,
x: Math.random() * 400, y: Math.random() * 400,
y: Math.random() * 400, };
};
}
return el; return node;
}); })
}); );
}; };
const logToObject = () => console.log(rfInstance?.toObject()); const logToObject = () => console.log(instance.toObject());
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 }); const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => { const toggleClassnames = () => {
setElements((elms) => { instance.setNodes((nodes) =>
return elms.map((el) => { nodes.map((node) => {
if (isNode(el)) { node.className = node.className === 'light' ? 'dark' : 'light';
el.className = el.className === 'light' ? 'dark' : 'light';
}
return el; return node;
}); })
}); );
}; };
return ( return (
<ReactFlow <ReactFlow
elements={elements} defaultNodes={initialNodes}
onLoad={onLoad} defaultEdges={initialEdges}
onElementClick={onElementClick} onNodeClick={onNodeClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example" className="react-flow-basic-example"
defaultZoom={1.5} defaultZoom={1.5}
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}
fitView
> >
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
@@ -94,4 +83,10 @@ const BasicFlow = () => {
); );
}; };
export default BasicFlow; export default function App() {
return (
<ReactFlowProvider>
<BasicFlow />
</ReactFlowProvider>
);
}
@@ -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>
);
}
+12 -9
View File
@@ -1,28 +1,31 @@
import React, { useState } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements, Node,
addEdge, addEdge,
Background, Background,
BackgroundVariant, BackgroundVariant,
Elements,
Connection, Connection,
Edge, Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import ConnectionLine from './ConnectionLine'; 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 ConnectionLineFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
connectionLineComponent={ConnectionLine} connectionLineComponent={ConnectionLine}
onElementsRemove={onElementsRemove}
onConnect={onConnect} onConnect={onConnect}
> >
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
+31 -24
View File
@@ -1,46 +1,49 @@
import React, { useState, useEffect, MouseEvent } from 'react'; import { useState, useEffect, MouseEvent } from 'react';
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import ReactFlow, { import ReactFlow, {
isEdge,
removeElements,
addEdge, addEdge,
MiniMap, MiniMap,
Controls, Controls,
Node, Node,
FlowElement, ReactFlowInstance,
OnLoadParams,
Elements,
Position, Position,
SnapGrid, SnapGrid,
Connection, Connection,
Edge, useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import ColorSelectorNode from './ColorSelectorNode'; 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 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 initBgColor = '#1A192B';
const connectionLineStyle = { stroke: '#fff' }; const connectionLineStyle = { stroke: '#fff' };
const snapGrid: SnapGrid = [16, 16]; const snapGrid: SnapGrid = [16, 16];
const nodeTypes = { const nodeTypes = {
selectorNode: ColorSelectorNode, selectorNode: ColorSelectorNode,
}; };
const CustomNodeFlow = () => { const CustomNodeFlow = () => {
const [elements, setElements] = useState<Elements>([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bgColor, setBgColor] = useState<string>(initBgColor); const [bgColor, setBgColor] = useState<string>(initBgColor);
useEffect(() => { useEffect(() => {
const onChange = (event: ChangeEvent<HTMLInputElement>) => { const onChange = (event: ChangeEvent<HTMLInputElement>) => {
setElements((els) => setNodes((nds) =>
els.map((e) => { nds.map((node) => {
if (isEdge(e) || e.id !== '2') { if (node.id !== '2') {
return e; return node;
} }
const color = event.target.value; const color = event.target.value;
@@ -48,9 +51,9 @@ const CustomNodeFlow = () => {
setBgColor(color); setBgColor(color);
return { return {
...e, ...node,
data: { data: {
...e.data, ...node.data,
color, color,
}, },
}; };
@@ -58,7 +61,7 @@ const CustomNodeFlow = () => {
); );
}; };
setElements([ setNodes([
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -87,31 +90,35 @@ const CustomNodeFlow = () => {
position: { x: 550, y: 100 }, position: { x: 550, y: 100 },
targetPosition: Position.Left, targetPosition: Position.Left,
}, },
]);
setEdges([
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } }, { 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: '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' } }, { 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 = (connection: Connection) =>
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
style={{ background: bgColor }} style={{ background: bgColor }}
onLoad={onLoad} onInit={onInit}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
snapToGrid={true} snapToGrid={true}
snapGrid={snapGrid} snapGrid={snapGrid}
defaultZoom={1.5} defaultZoom={1.5}
fitView
> >
<MiniMap <MiniMap
nodeStrokeColor={(n: Node): string => { nodeStrokeColor={(n: Node): string => {
+83
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>
);
}
+11 -3
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'; import DragHandleNode from './DragHandleNode';
@@ -6,16 +6,24 @@ const nodeTypes = {
dragHandleNode: DragHandleNode, dragHandleNode: DragHandleNode,
}; };
const elements = [ const initialNodes: Node[] = [
{ {
id: '2', id: '2',
type: 'dragHandleNode', type: 'dragHandleNode',
dragHandle: '.custom-drag-handle', dragHandle: '.custom-drag-handle',
style: { border: '1px solid #ddd', padding: '20px 40px' }, style: { border: '1px solid #ddd', padding: '20px 40px' },
position: { x: 200, y: 200 }, 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; export default DragHandleFlow;
+17 -16
View File
@@ -1,22 +1,21 @@
import React, { useState, DragEvent } from 'react'; import { useState, DragEvent } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
addEdge, addEdge,
removeElements,
Controls, Controls,
OnLoadParams, ReactFlowInstance,
Elements,
Connection, Connection,
Edge, Edge,
ElementId,
Node, Node,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import './dnd.css'; 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) => { const onDragOver = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
@@ -24,15 +23,15 @@ const onDragOver = (event: DragEvent) => {
}; };
let id = 0; let id = 0;
const getId = (): ElementId => `dndnode_${id++}`; const getId = () => `dndnode_${id++}`;
const DnDFlow = () => { const DnDFlow = () => {
const [reactFlowInstance, setReactFlowInstance] = useState<OnLoadParams>(); const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
const onLoad = (_reactFlowInstance: OnLoadParams) => setReactFlowInstance(_reactFlowInstance);
const onDrop = (event: DragEvent) => { const onDrop = (event: DragEvent) => {
event.preventDefault(); event.preventDefault();
@@ -47,7 +46,7 @@ const DnDFlow = () => {
data: { label: `${type} node` }, data: { label: `${type} node` },
}; };
setElements((es) => es.concat(newNode)); setNodes((nds) => nds.concat(newNode));
} }
}; };
@@ -56,10 +55,12 @@ const DnDFlow = () => {
<ReactFlowProvider> <ReactFlowProvider>
<div className="reactflow-wrapper"> <div className="reactflow-wrapper">
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onEdgesChange={onEdgesChange}
onNodesChange={onNodesChange}
onConnect={onConnect} onConnect={onConnect}
onElementsRemove={onElementsRemove} onInit={onInit}
onLoad={onLoad}
onDrop={onDrop} onDrop={onDrop}
onDragOver={onDragOver} onDragOver={onDragOver}
> >
+22 -13
View File
@@ -1,40 +1,49 @@
/** /**
* Example for checking the different edge types and source and target positions * Example for checking the different edge types and source and target positions
*/ */
import React, { useState } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
MiniMap, MiniMap,
Controls, Controls,
Background, Background,
OnLoadParams, ReactFlowInstance,
Connection, Connection,
Edge, Edge,
Elements, useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import { getElements } from './utils'; import { getElements } from './utils';
const onLoad = (reactFlowInstance: OnLoadParams) => { const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView(); 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 EdgeTypesFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onLoad={onLoad} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
onConnect={onConnect} onConnect={onConnect}
minZoom={0.2} minZoom={0.2}
zoomOnScroll={false}
selectionKeyCode="a+s"
multiSelectionKeyCode={multiSelectionKeyCode}
deleteKeyCode={deleteKeyCode}
zoomActivationKeyCode="z"
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
+17 -12
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 nodeWidth = 80;
const nodeGapWidth = nodeWidth * 2; const nodeGapWidth = nodeWidth * 2;
@@ -9,10 +9,10 @@ const sourceTargetPositions = [
{ source: Position.Right, target: Position.Left }, { source: Position.Right, target: Position.Left },
]; ];
const nodeColors = [ const nodeColors = [
['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'], ['#0c5956', '#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'], ['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8', '#4fa6e0'],
]; ];
const edgeTypes = ['default', 'step', 'smoothstep', 'straight']; const edgeTypes = ['default', 'step', 'smoothstep', 'straight', 'simplebezier'];
const offsets = [ const offsets = [
{ {
x: 0, x: 0,
@@ -49,10 +49,10 @@ const offsets = [
]; ];
let id = 0; let id = 0;
const getNodeId = (): ElementId => (id++).toString(); const getNodeId = (): string => (id++).toString();
export function getElements(): Elements { export function getElements(): { nodes: Node[]; edges: Edge[] } {
const initialElements = []; const initialElements = { nodes: [] as Node[], edges: [] as Edge[] };
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) { for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex]; const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex];
@@ -70,7 +70,7 @@ export function getElements(): Elements {
}; };
const sourceId = getNodeId(); const sourceId = getNodeId();
const sourceData = { label: `Source ${sourceId}` }; const sourceData = { label: `Source ${sourceId}` };
const sourceNode = { const sourceNode: Node = {
id: sourceId, id: sourceId,
style, style,
data: sourceData, data: sourceData,
@@ -85,7 +85,7 @@ export function getElements(): Elements {
x: sourcePosition.x + currOffset.x, x: sourcePosition.x + currOffset.x,
y: sourcePosition.y + currOffset.y, y: sourcePosition.y + currOffset.y,
}; };
const targetNode = { const targetNode: Node = {
id: targetId, id: targetId,
style, style,
data: targetData, data: targetData,
@@ -94,10 +94,15 @@ export function getElements(): Elements {
targetPosition: currSourceTargetPos.target, targetPosition: currSourceTargetPos.target,
}; };
initialElements.push(sourceNode); initialElements.nodes.push(sourceNode);
initialElements.push(targetNode); 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);
} }
} }
} }
+2 -5
View File
@@ -1,5 +1,5 @@
import { FC } from 'react'; import { FC } from 'react';
import { EdgeProps, getBezierPath, getMarkerEnd } from 'react-flow-renderer'; import { EdgeProps, getBezierPath } from 'react-flow-renderer';
const CustomEdge: FC<EdgeProps> = ({ const CustomEdge: FC<EdgeProps> = ({
id, id,
@@ -10,15 +10,12 @@ const CustomEdge: FC<EdgeProps> = ({
sourcePosition, sourcePosition,
targetPosition, targetPosition,
data, data,
arrowHeadType,
markerEndId,
}) => { }) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
return ( return (
<> <>
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} /> <path id={id} className="react-flow__edge-path" d={edgePath} />
<text> <text>
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle"> <textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
{data.text} {data.text}
+3 -6
View File
@@ -1,5 +1,5 @@
import { FC } from 'react'; import { FC } from 'react';
import { EdgeProps, getBezierPath, getMarkerEnd, EdgeText, getEdgeCenter } from 'react-flow-renderer'; import { EdgeProps, getBezierPath, EdgeText, getBezierEdgeCenter } from 'react-flow-renderer';
const CustomEdge: FC<EdgeProps> = ({ const CustomEdge: FC<EdgeProps> = ({
id, id,
@@ -10,12 +10,9 @@ const CustomEdge: FC<EdgeProps> = ({
sourcePosition, sourcePosition,
targetPosition, targetPosition,
data, data,
arrowHeadType,
markerEndId,
}) => { }) => {
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition }); const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); const [centerX, centerY] = getBezierEdgeCenter({
const [centerX, centerY] = getEdgeCenter({
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
@@ -24,7 +21,7 @@ const CustomEdge: FC<EdgeProps> = ({
return ( return (
<> <>
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} /> <path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeText <EdgeText
x={centerX} x={centerX}
y={centerY} y={centerY}
+76 -49
View File
@@ -1,32 +1,31 @@
import React, { useState, MouseEvent } from 'react'; import { MouseEvent } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
MiniMap,
Controls,
Background, Background,
OnLoadParams,
FlowElement,
EdgeTypesType,
Elements,
Connection, Connection,
Controls,
Edge, Edge,
ArrowHeadType, EdgeTypes,
MarkerType,
MiniMap,
Node, Node,
Position,
ReactFlowInstance,
useEdgesState,
useNodesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import CustomEdge from './CustomEdge'; import CustomEdge from './CustomEdge';
import CustomEdge2 from './CustomEdge2'; 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 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: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } }, { id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } }, { 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: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } }, { id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, 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: '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: '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 } }, { 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: '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: '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: '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-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-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: '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', id: 'e5-6',
source: '5', source: '5',
@@ -54,54 +89,46 @@ const initialElements: Elements = [
</> </>
), ),
labelStyle: { fill: 'red', fontWeight: 700 }, labelStyle: { fill: 'red', fontWeight: 700 },
arrowHeadType: ArrowHeadType.Arrow, style: { stroke: '#ffcc00' },
}, markerEnd: {
{ type: MarkerType.Arrow,
id: 'e5-7', color: '#FFCC00',
source: '5', markerUnits: 'userSpaceOnUse',
target: '7', width: 20,
label: 'label with styled bg', height: 20,
labelBgPadding: [8, 4], strokeWidth: 2,
labelBgBorderRadius: 4, },
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 }, markerStart: {
arrowHeadType: ArrowHeadType.ArrowClosed, type: MarkerType.ArrowClosed,
}, color: '#FFCC00',
{ orient: 'auto-start-reverse',
id: 'e5-8', markerUnits: 'userSpaceOnUse',
source: '5', width: 20,
target: '8', height: 20,
type: 'custom', },
data: { text: 'custom edge' },
arrowHeadType: ArrowHeadType.ArrowClosed,
},
{
id: 'e5-9',
source: '5',
target: '9',
type: 'custom2',
data: { text: 'custom edge 2' },
}, },
]; ];
const edgeTypes: EdgeTypesType = { const edgeTypes: EdgeTypes = {
custom: CustomEdge, custom: CustomEdge,
custom2: CustomEdge2, custom2: CustomEdge2,
}; };
const EdgesFlow = () => { const EdgesFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onLoad={onLoad} onInit={onInit}
snapToGrid={true} snapToGrid={true}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
> >
+18 -19
View File
@@ -1,52 +1,51 @@
import React, { useState, MouseEvent, CSSProperties } from 'react'; import { MouseEvent, CSSProperties } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
MiniMap,
Controls, Controls,
Background, Background,
OnLoadParams,
Elements,
ElementId,
Node, Node,
FlowElement,
BackgroundVariant, BackgroundVariant,
Connection, Connection,
Edge, Edge,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 }; const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 };
const EmptyFlow = () => { const EmptyFlow = () => {
const [elements, setElements] = useState<Elements>([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const addRandomNode = () => { const addRandomNode = () => {
const nodeId: ElementId = (elements.length + 1).toString(); const nodeId = (nodes.length + 1).toString();
const newNode: Node = { const newNode: Node = {
id: nodeId, id: nodeId,
data: { label: `Node: ${nodeId}` }, data: { label: `Node: ${nodeId}` },
position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }, position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight },
}; };
setElements((els) => els.concat(newNode)); setNodes((nds) => nds.concat(newNode));
}; };
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onLoad={onLoad} edges={edges}
onElementClick={onElementClick} onInit={onInit}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={(p) => onConnect(p)} onConnect={(p) => onConnect(p)}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onlyRenderVisibleElements={false} onlyRenderVisibleElements={false}
> >
<MiniMap />
<Controls /> <Controls />
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
@@ -16,7 +16,9 @@ const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({
const targetNode = { const targetNode = {
id: 'connection-target', id: 'connection-target',
__rf: { width: 1, height: 1, position: { x: targetX, y: targetY } }, width: 1,
height: 1,
position: { x: targetX, y: targetY },
} as Node; } as Node;
const { sx, sy } = getEdgeParams(sourceNode, targetNode); const { sx, sy } = getEdgeParams(sourceNode, targetNode);
+8 -7
View File
@@ -1,14 +1,15 @@
import { FC, useMemo, CSSProperties } from 'react'; 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'; import { getEdgeParams } from './utils';
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, markerEndId, style }) => { const nodeSelector = (s: ReactFlowState) => s.nodeInternals;
const nodes = useStoreState((state) => state.nodes);
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]); const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]); const nodeInternals = useStore(nodeSelector);
const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]);
const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]);
if (!sourceNode || !targetNode) { if (!sourceNode || !targetNode) {
return null; return null;
@@ -27,7 +28,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, marker
return ( return (
<g className="react-flow__connection"> <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> </g>
); );
}; };
+20 -20
View File
@@ -1,15 +1,13 @@
import React, { useState } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
Background, Background,
OnLoadParams, ReactFlowInstance,
EdgeTypesType, EdgeTypes,
Elements,
Connection, Connection,
Edge, useNodesState,
ArrowHeadType, useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import './style.css'; import './style.css';
@@ -18,29 +16,31 @@ import FloatingEdge from './FloatingEdge';
import FloatingConnectionLine from './FloatingConnectionLine'; import FloatingConnectionLine from './FloatingConnectionLine';
import { createElements } from './utils'; 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, floating: FloatingEdge,
}; };
const NodeAsHandleFlow = () => { const FloatingEdges = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
const onConnect = (params: Connection | Edge) => }, []);
setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els));
return ( return (
<div className="floatingedges"> <div className="floatingedges">
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementsRemove={onElementsRemove} edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
onLoad={onLoad} onInit={onInit}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
connectionLineComponent={FloatingConnectionLine} connectionLineComponent={FloatingConnectionLine}
> >
@@ -50,4 +50,4 @@ const NodeAsHandleFlow = () => {
); );
}; };
export default NodeAsHandleFlow; export default FloatingEdges;
+22 -15
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 // this helper function returns the intersection point
// of the line between the center of the intersectionNode and the target node // of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition { 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 // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { const {
width: intersectionNodeWidth, width: intersectionNodeWidth,
height: intersectionNodeHeight, height: intersectionNodeHeight,
position: intersectionNodePosition, position: intersectionNodePosition,
} = intersectionNode.__rf; } = intersectionNode;
const targetPosition = targetNode.__rf.position; const targetPosition = targetNode.position;
const w = intersectionNodeWidth / 2; const w = (intersectionNodeWidth ?? 0) / 2;
const h = intersectionNodeHeight / 2; const h = (intersectionNodeHeight ?? 0) / 2;
const x2 = intersectionNodePosition.x + w; const x2 = intersectionNodePosition.x + w;
const y2 = intersectionNodePosition.y + h; 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 // returns the position (top,right,bottom or right) passed node compared to the intersection point
function getEdgePosition(node: Node, intersectionPoint: XYPosition) { 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 nx = Math.round(n.x);
const ny = Math.round(n.y); const ny = Math.round(n.y);
const px = Math.round(intersectionPoint.x); const px = Math.round(intersectionPoint.x);
@@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
if (px <= nx + 1) { if (px <= nx + 1) {
return Position.Left; return Position.Left;
} }
if (px >= nx + n.width - 1) { if (px >= nx + (n.width ?? 0) - 1) {
return Position.Right; return Position.Right;
} }
if (py <= ny + 1) { if (py <= ny + 1) {
return Position.Top; return Position.Top;
} }
if (py >= n.y + n.height - 1) { if (py >= n.y + (n.height ?? 0) - 1) {
return Position.Bottom; return Position.Bottom;
} }
@@ -72,11 +73,18 @@ export function getEdgeParams(source: Node, target: Node) {
}; };
} }
export function createElements() { type NodesAndEdges = {
const elements = []; nodes: Node[];
edges: Edge[];
};
export function createElements(): NodesAndEdges {
const nodes: Node[] = [];
const edges: Edge[] = [];
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; 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++) { for (let i = 0; i < 8; i++) {
const degrees = i * (360 / 8); const degrees = i * (360 / 8);
@@ -84,16 +92,15 @@ export function createElements() {
const x = 250 * Math.cos(radians) + center.x; const x = 250 * Math.cos(radians) + center.x;
const y = 250 * Math.sin(radians) + center.y; 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}`, id: `edge-${i}`,
target: 'target', target: 'target',
source: `${i}`, source: `${i}`,
type: 'floating', type: 'floating',
arrowHeadType: ArrowHeadType.Arrow,
}); });
} }
return elements; return { nodes, edges };
} }
+36 -17
View File
@@ -1,34 +1,53 @@
import React, { useState } from 'react'; import { useState, useCallback } from 'react';
import { useEffect } 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 = [ const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, { id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, { id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, { id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, { 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-2', source: '1', target: '2' },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4' }, { 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 HiddenFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes] = useNodesState(initialNodes);
const [isHidden, setIsHidden] = useState<boolean>(false); const [edges, setEdges] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const [isHidden, setIsHidden] = useState<boolean>(true);
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
useEffect(() => { useEffect(() => {
setElements((els) => setNodes(setHidden(isHidden));
els.map((e) => { setEdges(setHidden(isHidden));
e.isHidden = isHidden;
return e;
})
);
}, [isHidden]); }, [isHidden]);
return ( return (
<ReactFlow elements={elements} onConnect={onConnect}> <ReactFlow nodes={nodes} edges={edges} onConnect={onConnect}>
<MiniMap /> <MiniMap />
<Controls /> <Controls />
+32 -23
View File
@@ -1,37 +1,42 @@
import React, { useState, MouseEvent, WheelEvent } from 'react'; import { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
MiniMap, MiniMap,
Controls, Controls,
Elements,
Node, Node,
FlowElement,
Connection, Connection,
Edge, Edge,
PanOnScrollMode, PanOnScrollMode,
FlowTransform, Viewport,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }, { 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: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, { 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-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', node);
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event); 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 onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event); const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
const onMoveEnd = (flowTranasform?: FlowTransform) => console.log('onMoveEnd', flowTranasform); const onMoveEnd = (_: TouchEvent | MouseEvent, viewport: Viewport) => console.log('onMoveEnd', viewport);
const InteractionFlow = () => { const InteractionFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
const [isSelectable, setIsSelectable] = useState<boolean>(false); const [isSelectable, setIsSelectable] = useState<boolean>(false);
const [isDraggable, setIsDraggable] = useState<boolean>(false); const [isDraggable, setIsDraggable] = useState<boolean>(false);
@@ -41,14 +46,17 @@ const InteractionFlow = () => {
const [panOnScroll, setPanOnScroll] = useState<boolean>(false); const [panOnScroll, setPanOnScroll] = useState<boolean>(false);
const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free); const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free);
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState<boolean>(false); 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 [captureZoomClick, setCaptureZoomClick] = useState<boolean>(false);
const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false); const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false);
const [captureElementClick, setCaptureElementClick] = useState<boolean>(false); const [captureElementClick, setCaptureElementClick] = useState<boolean>(false);
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
elementsSelectable={isSelectable} elementsSelectable={isSelectable}
nodesConnectable={isConnectable} nodesConnectable={isConnectable}
nodesDraggable={isDraggable} nodesDraggable={isDraggable}
@@ -58,10 +66,11 @@ const InteractionFlow = () => {
panOnScrollMode={panOnScrollMode} panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick} zoomOnDoubleClick={zoomOnDoubleClick}
onConnect={onConnect} onConnect={onConnect}
onElementClick={captureElementClick ? onElementClick : undefined} onNodeClick={captureElementClick ? onNodeClick : undefined}
onEdgeClick={captureElementClick ? onEdgeClick : undefined}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
paneMoveable={paneMoveable} panOnDrag={panOnDrag}
onPaneClick={captureZoomClick ? onPaneClick : undefined} onPaneClick={captureZoomClick ? onPaneClick : undefined}
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined} onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined} onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
@@ -171,14 +180,14 @@ const InteractionFlow = () => {
</label> </label>
</div> </div>
<div> <div>
<label htmlFor="panemoveable"> <label htmlFor="panondrag">
paneMoveable panOnDrag
<input <input
id="panemoveable" id="panondrag"
type="checkbox" type="checkbox"
checked={paneMoveable} checked={panOnDrag}
onChange={(event) => setPaneMoveable(event.target.checked)} onChange={(event) => setPanOnDrag(event.target.checked)}
className="react-flow__panemoveable" className="react-flow__panondrag"
/> />
</label> </label>
</div> </div>
+32 -31
View File
@@ -1,74 +1,75 @@
import React, { useState } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
addEdge, addEdge,
removeElements,
Controls, Controls,
isNode,
Elements,
Connection, Connection,
Edge, CoordinateExtent,
NodeExtent,
Position, Position,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import dagre from 'dagre'; import dagre from 'dagre';
import initialElements from './initial-elements'; import initialItems from './initial-elements';
import './layouting.css'; import './layouting.css';
const dagreGraph = new dagre.graphlib.Graph(); const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({})); dagreGraph.setDefaultEdgeLabel(() => ({}));
const nodeExtent: NodeExtent = [ const nodeExtent: CoordinateExtent = [
[0, 0], [0, 0],
[1000, 1000], [1000, 1000],
]; ];
const LayoutFlow = () => { const LayoutFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const onLayout = (direction: string) => { const onLayout = (direction: string) => {
const isHorizontal = direction === 'LR'; const isHorizontal = direction === 'LR';
dagreGraph.setGraph({ rankdir: direction }); dagreGraph.setGraph({ rankdir: direction });
elements.forEach((el) => { nodes.forEach((node) => {
if (isNode(el)) { dagreGraph.setNode(node.id, { width: 150, height: 50 });
dagreGraph.setNode(el.id, { width: 150, height: 50 }); });
} else {
dagreGraph.setEdge(el.source, el.target); edges.forEach((edge) => {
} dagreGraph.setEdge(edge.source, edge.target);
}); });
dagre.layout(dagreGraph); dagre.layout(dagreGraph);
const layoutedElements = elements.map((el) => { const layoutedNodes = nodes.map((node) => {
if (isNode(el)) { const nodeWithPosition = dagreGraph.node(node.id);
const nodeWithPosition = dagreGraph.node(el.id); node.targetPosition = isHorizontal ? Position.Left : Position.Top;
el.targetPosition = isHorizontal ? Position.Left : Position.Top; node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom; // we need to pass a slightly different position in order to notify react flow about the change
// 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?
// @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 };
el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
}
return el; return node;
}); });
setElements(layoutedElements); setNodes(layoutedNodes);
}; };
return ( return (
<div className="layoutflow"> <div className="layoutflow">
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onConnect={onConnect} onConnect={onConnect}
onElementsRemove={onElementsRemove}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
onLoad={() => onLayout('TB')} onInit={() => onLayout('TB')}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
> >
<Controls /> <Controls />
</ReactFlow> </ReactFlow>
+8 -3
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 position: XYPosition = { x: 0, y: 0 };
const elements: Elements = [ const nodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -56,6 +56,9 @@ const elements: Elements = [
position, position,
}, },
{ id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } }, { id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } },
];
const edges: Edge[] = [
{ id: 'e12', source: '1', target: '2', type: 'smoothstep' }, { id: 'e12', source: '1', target: '2', type: 'smoothstep' },
{ id: 'e13', source: '1', target: '3', type: 'smoothstep' }, { id: 'e13', source: '1', target: '3', type: 'smoothstep' },
{ id: 'e22a', source: '2', target: '2a', 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' }, { id: 'e57', source: '5', target: '7', type: 'smoothstep' },
]; ];
export default elements; const nodesAndEdges = { nodes, edges };
export default nodesAndEdges;
+19 -8
View File
@@ -1,34 +1,45 @@
import React, { useState, FC } from 'react'; import { FC } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
Background, Background,
Elements, Node,
Edge, Edge,
Connection, Connection,
ReactFlowProvider, ReactFlowProvider,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import './multiflows.css'; 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: '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: '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: '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' }, { 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-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const Flow: FC = () => { const Flow: FC = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
const onConnect = (params: Edge | Connection) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect}> <ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<Background /> <Background />
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
+170
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;
+30 -15
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 initialNodes: Node[] = [
const initialElements: Elements = [
{ {
id: '1', id: '1',
sourcePosition: Position.Right, sourcePosition: Position.Right,
@@ -20,31 +26,40 @@ const initialElements: Elements = [
data: { label: 'A Node' }, data: { label: 'A Node' },
position: { x: 250, y: 0 }, 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 buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 };
const NodeTypeChangeFlow = () => { const NodeTypeChangeFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const changeType = () => { const changeType = () => {
setElements((elms) => setNodes((nds) =>
elms.map((el) => { nds.map((node) => {
if (isEdge(el) || el.type === 'input') { if (node.type === 'input') {
return el; return node;
} }
return { return {
...el, ...node,
type: el.type === 'default' ? 'output' : 'default', type: node.type === 'default' ? 'output' : 'default',
}; };
}) })
); );
}; };
return ( 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}> <button onClick={changeType} style={buttonStyle}>
change type change type
</button> </button>
+14 -9
View File
@@ -1,16 +1,18 @@
import React, { useState, CSSProperties, FC } from 'react'; import { useState, CSSProperties, FC } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Elements, Node,
Position, Position,
Connection, Connection,
Edge, Edge,
NodeProps, NodeProps,
NodeTypesType, NodeTypes,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
sourcePosition: Position.Right, sourcePosition: Position.Right,
@@ -41,7 +43,7 @@ const NodeB: FC<NodeProps> = () => {
}; };
type NodeTypesObject = { type NodeTypesObject = {
[key: string]: NodeTypesType; [key: string]: NodeTypes;
}; };
const nodeTypesObjects: NodeTypesObject = { const nodeTypesObjects: NodeTypesObject = {
@@ -55,16 +57,19 @@ const nodeTypesObjects: NodeTypesObject = {
const NodeTypeChangeFlow = () => { const NodeTypeChangeFlow = () => {
const [nodeTypesId, setNodeTypesId] = useState<string>('a'); const [nodeTypesId, setNodeTypesId] = useState<string>('a');
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a')); const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypesObjects[nodeTypesId]} nodeTypes={nodeTypesObjects[nodeTypesId]}
nodeTypesId={nodeTypesId}
> >
<button onClick={changeType} style={buttonStyle}> <button onClick={changeType} style={buttonStyle}>
change type change type
+51 -42
View File
@@ -1,54 +1,52 @@
import React, { useState, MouseEvent, CSSProperties } from 'react'; import { MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge, addEdge,
MiniMap, MiniMap,
Controls, Controls,
Background, Background,
isNode,
Node, Node,
Elements, Viewport,
FlowElement,
OnLoadParams,
FlowTransform,
SnapGrid, SnapGrid,
ArrowHeadType,
Connection, Connection,
Edge, Edge,
ReactFlowInstance,
useNodesState,
useEdgesState,
OnSelectionChangeParams,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node); const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDoubleClick = (_: MouseEvent, node: Node) => console.log('node double click', node); const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: MouseEvent) => console.log('pane click', event); const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: MouseEvent) => console.log('pane scroll', event); const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
const onPaneContextMenu = (event: MouseEvent) => console.log('pane context menu', event); const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
const onSelectionDrag = (_: MouseEvent, nodes: Node[]) => console.log('selection drag', nodes); const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
const onSelectionDragStart = (_: MouseEvent, nodes: Node[]) => console.log('selection drag start', nodes); const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
const onSelectionDragStop = (_: MouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes); const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => { const onSelectionContextMenu = (event: ReactMouseEvent, nodes: Node[]) => {
event.preventDefault(); event.preventDefault();
console.log('selection context menu', nodes); console.log('selection context menu', nodes);
}; };
const onElementClick = (_: MouseEvent, element: FlowElement) => const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('node click:', node);
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements); const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
const onLoad = (reactFlowInstance: OnLoadParams) => { const onInit = (reactFlowInstance: ReactFlowInstance) => {
console.log('flow loaded:', reactFlowInstance); console.log('pane ready:', reactFlowInstance);
reactFlowInstance.fitView();
}; };
const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform); const onMoveStart = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move start', viewport);
const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform); const onMoveEnd = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move end', viewport);
const onEdgeContextMenu = (_: MouseEvent, edge: Edge) => console.log('edge context menu', edge); const onEdgeContextMenu = (_: ReactMouseEvent, edge: Edge) => console.log('edge context menu', edge);
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('edge mouse enter', edge); const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse move', edge); const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge); const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', 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', id: '1',
type: 'input', type: 'input',
@@ -122,10 +120,13 @@ const initialElements: Elements = [
position: { x: 100, y: 480 }, position: { x: 100, y: 480 },
}, },
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } }, { 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-2', source: '1', target: '2', label: 'this is an edge label' },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' }, { 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-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
{ {
id: 'e5-7', id: 'e5-7',
@@ -158,15 +159,17 @@ const nodeColor = (n: Node): string => {
}; };
const OverviewFlow = () => { const OverviewFlow = () => {
const [elements, setElements] = useState(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll} onPaneScroll={onPaneScroll}
@@ -182,7 +185,7 @@ const OverviewFlow = () => {
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
onMoveStart={onMoveStart} onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd} onMoveEnd={onMoveEnd}
onLoad={onLoad} onInit={onInit}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
snapToGrid={true} snapToGrid={true}
snapGrid={snapGrid} snapGrid={snapGrid}
@@ -191,6 +194,12 @@ const OverviewFlow = () => {
onEdgeMouseMove={onEdgeMouseMove} onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave} onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeDoubleClick={onEdgeDoubleClick} onEdgeDoubleClick={onEdgeDoubleClick}
fitView
fitViewOptions={{ padding: 0.2 }}
attributionPosition="top-right"
maxZoom={Infinity}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
> >
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} /> <MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
<Controls /> <Controls />
+8 -8
View File
@@ -1,13 +1,13 @@
import React from 'react'; import { useStore, useStoreApi } from 'react-flow-renderer';
import { useStoreState, useStoreActions } from 'react-flow-renderer';
const Sidebar = () => { const Sidebar = () => {
const nodes = useStoreState((store) => store.nodes); const store = useStoreApi();
const transform = useStoreState((store) => store.transform); const nodeInternals = useStore((store) => store.nodeInternals);
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); const transform = useStore((store) => store.transform);
const selectAll = () => { 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 ( return (
@@ -20,9 +20,9 @@ const Sidebar = () => {
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}] [{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
</div> </div>
<div className="title">Nodes</div> <div className="title">Nodes</div>
{nodes.map((node) => ( {Array.from(nodeInternals).map(([, node]) => (
<div key={node.id}> <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> </div>
))} ))}
+20 -15
View File
@@ -1,37 +1,40 @@
import React, { useState, MouseEvent } from 'react'; import { MouseEvent } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
addEdge, addEdge,
removeElements, Node,
Controls, Controls,
OnLoadParams,
FlowElement,
Connection, Connection,
Edge, Edge,
Elements,
ConnectionMode, ConnectionMode,
useNodesState,
useEdgesState,
ReactFlowInstance,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import './provider.css'; import './provider.css';
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element); const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance); 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: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } }, { id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } }, { id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } }, { 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-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const ProviderFlow = () => { const ProviderFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
return ( return (
<div className="providerflow"> <div className="providerflow">
@@ -39,11 +42,13 @@ const ProviderFlow = () => {
<Sidebar /> <Sidebar />
<div className="reactflow-wrapper"> <div className="reactflow-wrapper">
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onElementsRemove={onElementsRemove} onInit={onInit}
onLoad={onLoad}
connectionMode={ConnectionMode.Loose} connectionMode={ConnectionMode.Loose}
> >
<Controls /> <Controls />
+14 -12
View File
@@ -1,5 +1,5 @@
import React, { memo, useCallback, Dispatch, FC } from 'react'; 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'; import localforage from 'localforage';
localforage.config({ localforage.config({
@@ -12,12 +12,13 @@ const flowKey = 'example-flow';
const getNodeId = () => `randomnode_${+new Date()}`; const getNodeId = () => `randomnode_${+new Date()}`;
type ControlsProps = { type ControlsProps = {
rfInstance?: OnLoadParams; rfInstance?: ReactFlowInstance;
setElements: Dispatch<React.SetStateAction<Elements<any>>>; setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
}; };
const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => { const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
const { transform } = useZoomPanHelper(); const { setViewport } = useReactFlow();
const onSave = useCallback(() => { const onSave = useCallback(() => {
if (rfInstance) { if (rfInstance) {
@@ -28,17 +29,18 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
const onRestore = useCallback(() => { const onRestore = useCallback(() => {
const restoreFlow = async () => { const restoreFlow = async () => {
const flow: FlowExportObject | null = await localforage.getItem(flowKey); const flow: ReactFlowJsonObject | null = await localforage.getItem(flowKey);
if (flow) { if (flow) {
const [x = 0, y = 0] = flow.position; const { x, y, zoom } = flow.viewport;
setElements(flow.elements || []); setNodes(flow.nodes || []);
transform({ x, y, zoom: flow.zoom || 0 }); setEdges(flow.edges || []);
setViewport({ x, y, zoom: zoom || 0 });
} }
}; };
restoreFlow(); restoreFlow();
}, [setElements, transform]); }, [setNodes, setEdges, setViewport]);
const onAdd = useCallback(() => { const onAdd = useCallback(() => {
const newNode = { const newNode = {
@@ -46,8 +48,8 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
data: { label: 'Added node' }, data: { label: 'Added node' },
position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight }, position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight },
}; };
setElements((els) => els.concat(newNode)); setNodes((nds) => nds.concat(newNode));
}, [setElements]); }, [setNodes]);
return ( return (
<div className="save__controls"> <div className="save__controls">
+21 -12
View File
@@ -1,34 +1,43 @@
import React, { useState } from 'react'; import { useState } from 'react';
import ReactFlow, { import ReactFlow, {
ReactFlowProvider, ReactFlowProvider,
removeElements, Node,
addEdge, addEdge,
Elements,
Connection, Connection,
Edge, Edge,
OnLoadParams, ReactFlowInstance,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import Controls from './Controls'; import Controls from './Controls';
import './save.css'; import './save.css';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } }, { id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, { 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 SaveRestore = () => {
const [rfInstance, setRfInstance] = useState<OnLoadParams>(); const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlowProvider> <ReactFlowProvider>
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect} onLoad={setRfInstance}> <ReactFlow
<Controls rfInstance={rfInstance} setElements={setElements} /> nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onInit={setRfInstance}
>
<Controls rfInstance={rfInstance} setNodes={setNodes} setEdges={setEdges} />
</ReactFlow> </ReactFlow>
</ReactFlowProvider> </ReactFlowProvider>
); );
+46 -30
View File
@@ -1,58 +1,74 @@
import React, { useState, CSSProperties } from 'react'; import { useState, CSSProperties, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements,
addEdge,
MiniMap, MiniMap,
isNode,
Controls, Controls,
Background, Background,
OnLoadParams, ReactFlowInstance,
Elements,
Connection,
Edge, Edge,
Node,
NodeChange,
applyNodeChanges,
Connection,
addEdge,
applyEdgeChanges,
EdgeChange,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import { getElements } from './utils'; import { getNodesAndEdges } from './utils';
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 }; const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
const onLoad = (reactFlowInstance: OnLoadParams) => { const onInit = (reactFlowInstance: ReactFlowInstance) => {
reactFlowInstance.fitView(); reactFlowInstance.fitView();
console.log(reactFlowInstance.getElements()); console.log(reactFlowInstance.getNodes());
}; };
const initialElements: Elements = getElements(30, 30); const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(5, 5);
const StressFlow = () => { const StressFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes] = useState<Node[]>(initialNodes);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = useCallback((connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
}, []);
const updatePos = () => { const updatePos = () => {
setElements((elms) => { setNodes((nds) => {
return elms.map((el) => { return nds.map((n) => {
if (isNode(el)) { return {
return { ...n,
...el, position: {
position: { x: Math.random() * window.innerWidth,
x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight,
y: Math.random() * window.innerHeight, },
}, };
};
}
return el;
}); });
}); });
}; };
const updateElements = () => { const updateElements = () => {
const grid = Math.ceil(Math.random() * 10); 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));
}, []);
const onEdgeChange = useCallback((changes: EdgeChange[]) => {
setEdges((es) => applyEdgeChanges(changes, es));
}, []);
return ( return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}> <ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onConnect={onConnect}
onNodesChange={onNodesChange}
onEdgesChange={onEdgeChange}
>
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<Background /> <Background />
+15 -6
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 { type ElementsCollection = {
const initialElements = []; nodes: Node[];
edges: Edge[];
};
export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection {
const initialNodes = [];
const initialEdges: Edge[] = [];
let nodeId = 1; let nodeId = 1;
let recentNodeId = null; let recentNodeId = null;
@@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
data, data,
position, position,
}; };
initialElements.push(node); initialNodes.push(node);
if (recentNodeId && nodeId <= xElements * yElements) { 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; recentNodeId = nodeId;
@@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
} }
} }
return initialElements; return {
nodes: initialNodes,
edges: initialEdges,
};
} }
+27
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);
+197
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;
+34 -14
View File
@@ -1,26 +1,30 @@
import React, { useState, MouseEvent } from 'react'; import { MouseEvent } from 'react';
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import ReactFlow, { removeElements, addEdge, Node, FlowElement, Elements, Connection, Edge } from 'react-flow-renderer';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); 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: '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: '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: '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' }, { 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-2', source: '1a', target: '2a' },
{ id: 'e1-3', source: '1a', target: '3a' }, { 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: '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: '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: '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: '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' }, { id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' },
];
const edgesB: Edge[] = [
{ id: 'e1b', source: 'inputb', target: '1b' }, { id: 'e1b', source: 'inputb', target: '1b' },
{ id: 'e2b', source: 'inputb', target: '2b' }, { id: 'e2b', source: 'inputb', target: '2b' },
{ id: 'e3b', source: 'inputb', target: '3b' }, { id: 'e3b', source: 'inputb', target: '3b' },
@@ -28,23 +32,39 @@ const elementsB: Elements = [
]; ];
const BasicFlow = () => { const BasicFlow = () => {
const [elements, setElements] = useState<Elements>(elementsA); const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onElementClick={onElementClick} edges={edges}
onElementsRemove={onElementsRemove} onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
> >
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> <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 flow a
</button> </button>
<button onClick={() => setElements(elementsB)}>flow b</button> <button
onClick={() => {
setNodes(nodesB);
setEdges(edgesB);
}}
>
flow b
</button>
</div> </div>
</ReactFlow> </ReactFlow>
); );
+50
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;
+19
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);
}
}
+34 -28
View File
@@ -1,67 +1,79 @@
import React, { useState, useCallback } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
NodeTypesType, useReactFlow,
NodeTypes,
addEdge, addEdge,
useZoomPanHelper,
ReactFlowProvider, ReactFlowProvider,
Elements, Node,
Connection, Connection,
Edge, Edge,
ElementId,
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
updateEdge, updateEdge,
ArrowHeadType, useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import CustomNode from './CustomNode'; import CustomNode from './CustomNode';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '00', id: '00',
type: 'custom', type: 'custom',
position: { x: 300, y: 250 }, position: { x: 300, y: 250 },
data: null,
}, },
{ {
id: '01', id: '01',
type: 'custom', type: 'custom',
position: { x: 100, y: 50 }, position: { x: 100, y: 50 },
data: null,
}, },
{ {
id: '02', id: '02',
type: 'custom', type: 'custom',
position: { x: 500, y: 50 }, position: { x: 500, y: 50 },
data: null,
}, },
{ {
id: '03', id: '03',
type: 'custom', type: 'custom',
position: { x: 500, y: 500 }, position: { x: 500, y: 500 },
data: null,
}, },
{ {
id: '04', id: '04',
type: 'custom', type: 'custom',
position: { x: 100, y: 500 }, position: { x: 100, y: 500 },
data: null,
}, },
{ {
id: '10', id: '10',
type: 'custom', type: 'custom',
position: { x: 300, y: 5 }, position: { x: 300, y: 5 },
data: null,
}, },
{ {
id: '20', id: '20',
type: 'custom', type: 'custom',
position: { x: 600, y: 250 }, position: { x: 600, y: 250 },
data: null,
}, },
{ {
id: '30', id: '30',
type: 'custom', type: 'custom',
position: { x: 300, y: 600 }, position: { x: 300, y: 600 },
data: null,
}, },
{ {
id: '40', id: '40',
type: 'custom', type: 'custom',
position: { x: 5, y: 250 }, position: { x: 5, y: 250 },
data: null,
}, },
];
const initialEdges: Edge[] = [
{ {
id: 'e0-1a', id: 'e0-1a',
source: '00', source: '00',
@@ -69,7 +81,6 @@ const initialElements: Elements = [
sourceHandle: 'left', sourceHandle: 'left',
targetHandle: 'bottom', targetHandle: 'bottom',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-1b', id: 'e0-1b',
@@ -78,7 +89,6 @@ const initialElements: Elements = [
sourceHandle: 'top', sourceHandle: 'top',
targetHandle: 'right', targetHandle: 'right',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-2a', id: 'e0-2a',
@@ -87,7 +97,6 @@ const initialElements: Elements = [
sourceHandle: 'top', sourceHandle: 'top',
targetHandle: 'left', targetHandle: 'left',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-2b', id: 'e0-2b',
@@ -96,7 +105,6 @@ const initialElements: Elements = [
sourceHandle: 'right', sourceHandle: 'right',
targetHandle: 'bottom', targetHandle: 'bottom',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-3a', id: 'e0-3a',
@@ -105,7 +113,6 @@ const initialElements: Elements = [
sourceHandle: 'right', sourceHandle: 'right',
targetHandle: 'top', targetHandle: 'top',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-3b', id: 'e0-3b',
@@ -114,7 +121,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom', sourceHandle: 'bottom',
targetHandle: 'left', targetHandle: 'left',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-4a', id: 'e0-4a',
@@ -123,7 +129,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom', sourceHandle: 'bottom',
targetHandle: 'right', targetHandle: 'right',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-4b', id: 'e0-4b',
@@ -132,7 +137,6 @@ const initialElements: Elements = [
sourceHandle: 'left', sourceHandle: 'left',
targetHandle: 'top', targetHandle: 'top',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-10', id: 'e0-10',
@@ -141,7 +145,6 @@ const initialElements: Elements = [
sourceHandle: 'top', sourceHandle: 'top',
targetHandle: 'bottom', targetHandle: 'bottom',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-20', id: 'e0-20',
@@ -150,7 +153,6 @@ const initialElements: Elements = [
sourceHandle: 'right', sourceHandle: 'right',
targetHandle: 'left', targetHandle: 'left',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-30', id: 'e0-30',
@@ -159,7 +161,6 @@ const initialElements: Elements = [
sourceHandle: 'bottom', sourceHandle: 'bottom',
targetHandle: 'top', targetHandle: 'top',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
{ {
id: 'e0-40', id: 'e0-40',
@@ -168,31 +169,33 @@ const initialElements: Elements = [
sourceHandle: 'left', sourceHandle: 'left',
targetHandle: 'right', targetHandle: 'right',
type: 'default', type: 'default',
arrowHeadType: ArrowHeadType.Arrow,
}, },
]; ];
const nodeTypes: NodeTypesType = { const nodeTypes: NodeTypes = {
custom: CustomNode, custom: CustomNode,
}; };
let id = 4; let id = 4;
const getId = (): ElementId => `${id++}`; const getId = () => `${id++}`;
const UpdateNodeInternalsFlow = () => { const UpdateNodeInternalsFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'default' }, els)); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const { project } = useZoomPanHelper();
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
const { project } = useReactFlow();
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) => const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setElements((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => (evt) =>
setElements((els) => setNodes((nds) =>
els.concat({ nds.concat({
id: getId(), id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }), position: project({ x: evt.clientX, y: evt.clientY - 40 }),
type: 'custom', type: 'custom',
data: null,
}) })
), ),
[project] [project]
@@ -200,7 +203,10 @@ const UpdateNodeInternalsFlow = () => {
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnect={onConnect} onConnect={onConnect}
onPaneClick={onPaneClick} onPaneClick={onPaneClick}
+29 -14
View File
@@ -1,17 +1,20 @@
import React, { useState } from 'react'; import React, { useState, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
Controls, Controls,
updateEdge, updateEdge,
addEdge, addEdge,
Elements, applyNodeChanges,
OnLoadParams, applyEdgeChanges,
ReactFlowInstance,
Connection, Connection,
Edge, Edge,
removeElements, Node,
NodeChange,
EdgeChange,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import {HandleType} from "../../../dist"; import {HandleType} from "../../../dist";
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -47,29 +50,41 @@ const initialElements: Elements = [
position: { x: 400, y: 100 }, position: { x: 400, y: 100 },
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 }, 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, handleType: HandleType) => console.log(`start update ${handleType} handle`, edge); const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge, handleType: HandleType) => console.log(`start update ${handleType} handle`, edge);
const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge, handleType: HandleType) => console.log(`end update ${handleType} handle`, edge); const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge, handleType: HandleType) => console.log(`end update ${handleType} handle`, edge);
const UpdatableEdge = () => { 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) => const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
setElements((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, 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 ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
onLoad={onLoad} edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onInit={onInit}
snapToGrid={true} snapToGrid={true}
onEdgeUpdate={onEdgeUpdate} onEdgeUpdate={onEdgeUpdate}
onConnect={onConnect} onConnect={onConnect}
onEdgeUpdateStart={onEdgeUpdateStart} onEdgeUpdateStart={onEdgeUpdateStart}
onElementsRemove={onElementsRemove}
onEdgeUpdateEnd={onEdgeUpdateEnd} onEdgeUpdateEnd={onEdgeUpdateEnd}
> >
<Controls /> <Controls />
+36 -25
View File
@@ -1,64 +1,75 @@
import React, { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import ReactFlow, { Elements } from 'react-flow-renderer'; import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
import './updatenode.css'; import './updatenode.css';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } }, { id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } }, { 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 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 [nodeName, setNodeName] = useState<string>('Node 1');
const [nodeBg, setNodeBg] = useState<string>('#eee'); const [nodeBg, setNodeBg] = useState<string>('#eee');
const [nodeHidden, setNodeHidden] = useState<boolean>(false); const [nodeHidden, setNodeHidden] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
setElements((els) => setNodes((nds) =>
els.map((el) => { nds.map((n) => {
if (el.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // it's important that you create a new object here in order to notify react flow about the change
el.data = { n.data = {
...el.data, ...n.data,
label: nodeName, label: nodeName,
}; };
} }
return el; return n;
}) })
); );
}, [nodeName, setElements]); }, [nodeName]);
useEffect(() => { useEffect(() => {
setElements((els) => setNodes((nds) =>
els.map((el) => { nds.map((n) => {
if (el.id === '1') { if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change // 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(() => { useEffect(() => {
setElements((els) => setNodes((nds) =>
els.map((el) => { nds.map((n) => {
if (el.id === '1' || el.id === 'e1-2') { if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value // 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 ( 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"> <div className="updatenode__controls">
<label>label:</label> <label>label:</label>
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} /> <input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
+11
View File
@@ -0,0 +1,11 @@
import { useKeyPress } from 'react-flow-renderer';
const UseKeyPressComponent = () => {
const metaPressed = useKeyPress(['Meta']);
console.log({ metaPressed });
return <div />;
};
export default UseKeyPressComponent;
+111
View File
@@ -0,0 +1,111 @@
import { useCallback } from 'react';
import ReactFlow, {
Node,
addEdge,
Background,
MiniMap,
useReactFlow,
ReactFlowProvider,
Connection,
Edge,
useNodesState,
useEdgesState,
} from 'react-flow-renderer';
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 = () => `${id++}`;
const UseZoomPanHelperFlow = () => {
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, addNodes, setNodes: setNodesHook } = useReactFlow();
const onPaneClick = useCallback(
(evt) => {
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
setNodes((nds) =>
nds.concat({
id: getId(),
position: projectedPosition,
data: {
label: `${projectedPosition.x}-${projectedPosition.y}`,
},
})
);
},
[project, setNodes]
);
const onNodeClick = useCallback(
(_, element) => {
const { x, y } = element.position;
setCenter(x, y, { zoom: 1, duration: 1200 });
},
[setCenter]
);
const onAddNode = useCallback(() => {
const newNode = {
id: getId(),
position: { x: Math.random() * 500, y: Math.random() * 500 },
data: {
label: 'New Node',
},
};
addNodes(newNode);
}, [addNodes]);
const onResetNodes = useCallback(() => {
setNodesHook(initialNodes);
}, [setNodesHook]);
return (
<ReactFlow
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({ duration: 1200 })}>zoomIn</button>
<button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button>
<button onClick={onAddNode}>add node</button>
<button onClick={onResetNodes}>reset nodes</button>
</div>
<Background />
<MiniMap />
</ReactFlow>
);
};
const WrappedFlow = () => (
<ReactFlowProvider>
<UseZoomPanHelperFlow />
</ReactFlowProvider>
);
export default WrappedFlow;
@@ -1,5 +1,4 @@
import React, { memo, FC, useMemo, CSSProperties } from 'react'; import React, { memo, FC, useMemo, CSSProperties } from 'react';
import { Handle, Position, NodeProps } from 'react-flow-renderer'; import { Handle, Position, NodeProps } from 'react-flow-renderer';
const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' }; const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' };
+31 -29
View File
@@ -1,23 +1,23 @@
import React, { useState, useCallback, CSSProperties } from 'react'; import { useCallback, CSSProperties } from 'react';
import ReactFlow, { import ReactFlow, {
NodeTypesType, NodeTypes,
addEdge, addEdge,
useZoomPanHelper, useReactFlow,
ReactFlowProvider, ReactFlowProvider,
Elements, Node,
Connection, Connection,
Edge, Edge,
ElementId,
useUpdateNodeInternals, useUpdateNodeInternals,
Position, Position,
isEdge, useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import CustomNode from './CustomNode'; import CustomNode from './CustomNode';
const initialHandleCount = 1; const initialHandleCount = 1;
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'custom', type: 'custom',
@@ -28,23 +28,25 @@ const initialElements: Elements = [
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 }; const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 };
const nodeTypes: NodeTypesType = { const nodeTypes: NodeTypes = {
custom: CustomNode, custom: CustomNode,
}; };
let id = 5; let id = 5;
const getId = (): ElementId => `${id++}`; const getId = (): string => `${id++}`;
const UpdateNodeInternalsFlow = () => { 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 updateNodeInternals = useUpdateNodeInternals();
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els)); const { project } = useReactFlow();
const { project } = useZoomPanHelper();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => (evt) =>
setElements((els) => setNodes((nds) =>
els.concat({ nds.concat({
id: getId(), id: getId(),
position: project({ x: evt.clientX, y: evt.clientY - 40 }), position: project({ x: evt.clientX, y: evt.clientY - 40 }),
data: { label: 'new node' }, data: { label: 'new node' },
@@ -56,25 +58,17 @@ const UpdateNodeInternalsFlow = () => {
); );
const toggleHandleCount = useCallback(() => { const toggleHandleCount = useCallback(() => {
setElements((els) => setNodes((nds) =>
els.map((el) => { nds.map((node) => {
if (isEdge(el)) { return { ...node, data: { ...node.data, handleCount: node.data?.handleCount === 1 ? 2 : 1 } };
return el;
}
return { ...el, data: { ...el.data, handleCount: el.data?.handleCount === 1 ? 2 : 1 } };
}) })
); );
}, []); }, []);
const toggleHandlePosition = useCallback(() => { const toggleHandlePosition = useCallback(() => {
setElements((els) => setNodes((nds) =>
els.map((el) => { nds.map((node) => {
if (isEdge(el)) { return { ...node, data: { ...node.data, handlePosition: node.data?.handlePosition === 0 ? 1 : 0 } };
return el;
}
return { ...el, data: { ...el.data, handlePosition: el.data?.handlePosition === 0 ? 1 : 0 } };
}) })
); );
}, []); }, []);
@@ -82,7 +76,15 @@ const UpdateNodeInternalsFlow = () => {
const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]); const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]);
return ( 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}> <div style={buttonWrapperStyles}>
<button onClick={toggleHandleCount}>toggle handle count</button> <button onClick={toggleHandleCount}>toggle handle count</button>
<button onClick={toggleHandlePosition}>toggle handle position</button> <button onClick={toggleHandlePosition}>toggle handle position</button>
-83
View File
@@ -1,83 +0,0 @@
import React, { useState, useCallback } from 'react';
import ReactFlow, {
removeElements,
addEdge,
Background,
MiniMap,
useZoomPanHelper,
ReactFlowProvider,
Elements,
ElementId,
Connection,
Edge,
} from 'react-flow-renderer';
const initialElements: Elements = [
{ 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' },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
];
let id = 5;
const getId = (): ElementId => `${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 onPaneClick = useCallback(
(evt) => {
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
setElements((els) =>
els.concat({
id: getId(),
position: projectedPosition,
data: {
label: `${projectedPosition.x}-${projectedPosition.y}`,
},
})
);
},
[project]
);
const onElementClick = useCallback(
(_, element) => {
const { x, y } = element.position;
setCenter(x, y, 1);
},
[setCenter]
);
return (
<ReactFlow
elements={elements}
onElementClick={onElementClick}
onElementsRemove={onElementsRemove}
onConnect={onConnect}
onPaneClick={onPaneClick}
>
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}>
<button onClick={() => zoomIn(1200)}>zoomIn</button>
<button onClick={() => zoomOut(0)}>zoomOut</button>
</div>
<Background />
<MiniMap />
</ReactFlow>
);
};
const WrappedFlow = () => (
<ReactFlowProvider>
<UseZoomPanHelperFlow />
</ReactFlowProvider>
);
export default WrappedFlow;
+22 -17
View File
@@ -1,27 +1,27 @@
import React, { useState, MouseEvent as ReactMouseEvent, FC } from 'react'; import { MouseEvent as ReactMouseEvent, FC } from 'react';
import ReactFlow, { import ReactFlow, {
addEdge, addEdge,
Handle, Handle,
OnLoadParams,
Connection, Connection,
Position, Position,
Elements, Node,
Edge, Edge,
OnConnectStartParams, OnConnectStartParams,
NodeProps, NodeProps,
NodeTypesType, NodeTypes,
useNodesState,
useEdgesState,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
import './validation.css'; import './validation.css';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 } }, { id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 } }, { id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null },
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 } }, { id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 } }, { 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 isValidConnection = (connection: Connection) => connection.target === 'B';
const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) => const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) =>
console.log('on connect start', { nodeId, handleType }); console.log('on connect start', { nodeId, handleType });
@@ -43,31 +43,36 @@ const CustomNode: FC<NodeProps> = ({ id }) => (
</> </>
); );
const nodeTypes: NodeTypesType = { const nodeTypes: NodeTypes = {
custominput: CustomInput, custominput: CustomInput,
customnode: CustomNode, customnode: CustomNode,
}; };
const HorizontalFlow = () => { const ValidationFlow = () => {
const [elements, setElements] = useState<Elements>(initialElements); const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnect = (params: Connection | Edge) => { const onConnect = (params: Connection | Edge) => {
console.log('on connect', params); console.log('on connect', params);
setElements((els) => addEdge(params, els)); setEdges((eds) => addEdge(params, eds));
}; };
return ( return (
<ReactFlow <ReactFlow
elements={elements} nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect} onConnect={onConnect}
selectNodesOnDrag={false} selectNodesOnDrag={false}
onLoad={onLoad}
className="validationflow" className="validationflow"
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onConnectStart={onConnectStart} onConnectStart={onConnectStart}
onConnectStop={onConnectStop} onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd} onConnectEnd={onConnectEnd}
fitView
/> />
); );
}; };
export default HorizontalFlow; export default ValidationFlow;
+120 -90
View File
@@ -1,89 +1,105 @@
import React, { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom'; import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import Overview from './Overview';
import Basic from './Basic'; import Basic from './Basic';
import CustomNode from './CustomNode'; import ControlledUncontrolled from './ControlledUncontrolled';
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 CustomConnectionLine from './CustomConnectionLine'; 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 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 NodeTypeChange from './NodeTypeChange';
import NodeTypesObjectChange from './NodeTypesObjectChange'; 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 UpdatableEdge from './UpdatableEdge';
import UpdateNode from './UpdateNode'; 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 UseUpdateNodeInternals from './UseUpdateNodeInternals';
import Undirectional from './Undirectional'; import UseReactFlow from './UseReactFlow';
import MultiFlows from './MultiFlows'; import Validation from './Validation';
import DragHandle from './DragHandle'; import UseKeyPress from './UseKeyPress';
import './index.css';
const routes = [ const routes = [
{ {
path: '/', path: '/',
component: Overview, 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', path: '/basic',
component: Basic, component: Basic,
}, },
{ {
path: '/empty', path: '/default-nodes',
component: Empty, component: DefaultNodes,
},
{
path: '/hidden',
component: Hidden,
},
{
path: '/edge-types',
component: EdgeTypes,
}, },
{ {
path: '/custom-connectionline', path: '/custom-connectionline',
component: CustomConnectionLine, 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', path: '/floating-edges',
component: FloatingEdges, 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', path: '/nodetype-change',
component: NodeTypeChange, component: NodeTypeChange,
@@ -92,6 +108,34 @@ const routes = [
path: '/nodetypesobject-change', path: '/nodetypesobject-change',
component: NodeTypesObjectChange, component: NodeTypesObjectChange,
}, },
{
path: '/provider',
component: Provider,
},
{
path: '/save-restore',
component: SaveRestore,
},
{
path: '/stress',
component: Stress,
},
{
path: '/subflow',
component: Subflow,
},
{
path: '/switch',
component: SwitchFlow,
},
{
path: '/touch-device',
component: TouchDevice,
},
{
path: '/undirectional',
component: Undirectional,
},
{ {
path: '/updatable-edge', path: '/updatable-edge',
component: UpdatableEdge, component: UpdatableEdge,
@@ -101,45 +145,31 @@ const routes = [
component: UpdateNode, component: UpdateNode,
}, },
{ {
path: '/save-restore', path: '/usereactflow',
component: SaveRestore, component: UseReactFlow,
},
{
path: '/drag-and-drop',
component: DragNDrop,
},
{
path: '/layouting',
component: Layout,
},
{
path: '/switch',
component: SwitchFlows,
},
{
path: '/usezoompanhelper',
component: UseZoomPanHelper,
}, },
{ {
path: '/useupdatenodeinternals', path: '/useupdatenodeinternals',
component: UseUpdateNodeInternals, component: UseUpdateNodeInternals,
}, },
{ {
path: '/undirectional', path: '/validation',
component: Undirectional, component: Validation,
}, },
{ {
path: '/multiflows', path: '/controlled-uncontrolled',
component: MultiFlows, component: ControlledUncontrolled,
}, },
{ {
path: '/draghandle', path: '/use-key-press',
component: DragHandle, component: UseKeyPress,
}, },
]; ];
const Header = withRouter(({ history, location }) => { const Header = () => {
const onChange = (event: ChangeEvent<HTMLSelectElement>) => history.push(event.target.value); const navigate = useNavigate();
const location = useLocation();
const onChange = (event: ChangeEvent<HTMLSelectElement>) => navigate(event.target.value);
return ( return (
<header> <header>
@@ -149,22 +179,22 @@ const Header = withRouter(({ history, location }) => {
<select defaultValue={location.pathname} onChange={onChange}> <select defaultValue={location.pathname} onChange={onChange}>
{routes.map((route) => ( {routes.map((route) => (
<option value={route.path} key={route.path}> <option value={route.path} key={route.path}>
{route.path === '/' ? 'overview' : route.path.substr(1, route.path.length)} {route.path === '/' ? 'overview' : route.path.substring(1, route.path.length)}
</option> </option>
))} ))}
</select> </select>
</header> </header>
); );
}); };
ReactDOM.render( ReactDOM.render(
<Router forceRefresh={true}> <BrowserRouter>
<Header /> <Header />
<Switch> <Routes>
{routes.map((route) => ( {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> </Routes>
</Router>, </BrowserRouter>,
document.getElementById('root') document.getElementById('root')
); );
+3 -8
View File
@@ -1,11 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
@@ -20,7 +16,6 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": ["src"],
"src" "exclude": ["src_oldapi"]
]
} }
+3 -2
View File
@@ -1,6 +1,7 @@
{ {
"internal": true, "internal": true,
"main": "../dist/nocss/ReactFlow-nocss.js", "type": "module",
"module": "../dist/nocss/ReactFlow-nocss.esm.js", "main": "../dist/nocss/index.js",
"module": "../dist/nocss/index.js",
"types": "../dist/nocss/index.d.ts" "types": "../dist/nocss/index.d.ts"
} }
+885 -1495
View File
File diff suppressed because it is too large Load Diff
+34 -27
View File
@@ -1,19 +1,28 @@
{ {
"name": "react-flow-renderer", "name": "react-flow-renderer",
"version": "9.7.4", "version": "10.0.9-next.0",
"engines": { "engines": {
"node": ">=12" "node": ">=14"
}, },
"main": "dist/ReactFlow.js", "main": "dist/umd/index.js",
"module": "dist/ReactFlow.esm.js", "module": "dist/esm/index.js",
"types": "dist/index.d.ts", "jsdelivr": "dist/umd/index.js",
"unpkg": "dist/umd/index.js",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/umd/index.js"
},
"./package.json": "./package.json"
},
"types": "dist/esm/index.d.ts",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/wbkd/react-flow.git" "url": "https://github.com/wbkd/react-flow.git"
}, },
"scripts": { "scripts": {
"build": "rollup -c --environment NODE_ENV:production && postcss src/*.css --dir dist", "build": "rimraf dist && rollup -c --environment NODE_ENV:production && postcss src/*.css --dir dist",
"start": "rollup -w -c", "start": "rollup -w -c",
"start:examples": "cd example && npm start", "start:examples": "cd example && npm start",
"build:examples": "cd example && npm run build", "build:examples": "cd example && npm run build",
@@ -28,51 +37,49 @@
"cy:open": "cypress open", "cy:open": "cypress open",
"release": "npm run test && release-it", "release": "npm run test && release-it",
"release:notest": "release-it", "release:notest": "release-it",
"release:next": "release-it --preRelease=next" "release:next": "release-it --preRelease=next --no-git"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.16.7", "@babel/runtime": "^7.17.8",
"classcat": "^5.0.3", "classcat": "^5.0.3",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0", "d3-zoom": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"react-draggable": "^4.4.4", "react-draggable": "^4.4.4",
"react-redux": "^7.2.6", "zustand": "^3.7.1"
"redux": "^4.1.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.12", "@babel/core": "^7.17.5",
"@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"@rollup/plugin-babel": "^5.3.0", "@babel/preset-typescript": "^7.16.7",
"@rollup/plugin-commonjs": "^21.0.1", "@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2",
"@rollup/plugin-node-resolve": "^13.1.3", "@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/d3": "^7.1.0",
"@types/react-redux": "^7.1.22", "@types/react": "^17.0.40",
"@types/redux": "^3.6.31", "@types/resize-observer-browser": "^0.1.7",
"@types/resize-observer-browser": "^0.1.6",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"babel-preset-react-app": "^10.0.1", "babel-preset-react-app": "^10.0.1",
"cypress": "^9.4.1", "cypress": "^9.5.1",
"postcss": "^8.4.5", "postcss": "^8.4.8",
"postcss-cli": "^9.1.0", "postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6", "postcss-nested": "^5.0.6",
"prettier": "2.5.1", "prettier": "2.5.1",
"prop-types": "^15.8.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"release-it": "^14.12.4", "release-it": "^14.12.5",
"rollup": "^2.66.1", "rimraf": "^3.0.2",
"rollup-plugin-bundle-size": "^1.0.3", "rollup": "^2.70.1",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-serve": "^1.1.0", "rollup-plugin-serve": "^1.1.0",
"rollup-plugin-typescript2": "^0.31.1", "rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.31.2",
"rollup-plugin-visualizer": "^5.6.0",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"typescript": "^4.5.4" "typescript": "^4.6.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "16 || 17", "react": "16 || 17",
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
plugins: [require('autoprefixer'), require('postcss-nested')],
};
-5
View File
@@ -1,5 +0,0 @@
module.exports = {
trailingComma: 'es5',
singleQuote: true,
printWidth: 120
};
+103 -51
View File
@@ -2,11 +2,11 @@ import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs'; import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel'; import babel from '@rollup/plugin-babel';
import postcss from 'rollup-plugin-postcss'; import postcss from 'rollup-plugin-postcss';
import bundleSize from 'rollup-plugin-bundle-size';
import replace from '@rollup/plugin-replace'; import replace from '@rollup/plugin-replace';
import svgr from '@svgr/rollup';
import typescript from 'rollup-plugin-typescript2'; import typescript from 'rollup-plugin-typescript2';
import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core'; import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core';
import { visualizer } from 'rollup-plugin-visualizer';
import { terser } from 'rollup-plugin-terser';
import pkg from './package.json'; import pkg from './package.json';
@@ -14,61 +14,113 @@ const isProd = process.env.NODE_ENV === 'production';
const isTesting = process.env.NODE_ENV === 'testing'; const isTesting = process.env.NODE_ENV === 'testing';
const processEnv = isProd || isTesting ? 'production' : 'development'; const processEnv = isProd || isTesting ? 'production' : 'development';
export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injectCSS = true } = {}) => ({ const defaultOutputOptions = {
input: 'src/index.ts', dir: 'dist/esm',
external: ['react', 'react-dom', (id) => id.includes('@babel/runtime')], format: 'esm',
onwarn(warning, rollupWarn) { sourcemap: true,
if (warning.code !== 'CIRCULAR_DEPENDENCY') { };
rollupWarn(warning);
} export const baseConfig = ({ outputOptions = {}, injectCSS = true } = {}) => {
}, const output = {
output: [ ...defaultOutputOptions,
{ ...outputOptions,
file: mainFile, };
format: 'cjs',
sourcemap: true, const isEsm = output.format === 'esm';
exports: 'named', const replaceOptions = isEsm ? {} : { 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) };
return {
input: isEsm
? [
'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',
]
: 'src/index.ts',
external: isEsm
? [
'react',
'react-dom',
'classcat',
'd3-selection',
'd3-zoom',
'react-draggable',
'zustand',
'zustand/shallow',
'zustand/context',
(id) => id.includes('@babel/runtime'),
]
: ['react', 'react-dom'],
onwarn(warning, rollupWarn) {
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
rollupWarn(warning);
}
}, },
{ output,
file: moduleFile,
format: 'esm', plugins: [
sourcemap: true, replace({
exports: 'named', __ENV__: JSON.stringify(processEnv),
}, __REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
], __INJECT_STYLES__: injectCSS,
plugins: [ preventAssignment: true,
replace({ ...replaceOptions,
__ENV__: JSON.stringify(processEnv), }),
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version), postcss({
preventAssignment: true, minimize: isProd,
}), inject: false,
bundleSize(), }),
postcss({ resolve(),
minimize: isProd, typescript({
inject: injectCSS, clean: true,
}), }),
svgr(), commonjs({
resolve(), include: 'node_modules/**',
typescript({ }),
clean: true, babel({
}), extensions: [...DEFAULT_BABEL_EXTENSIONS, '.ts', '.tsx'],
commonjs({ exclude: 'node_modules/**',
include: 'node_modules/**', babelHelpers: 'runtime',
}), }),
babel({ visualizer(),
extensions: [...DEFAULT_BABEL_EXTENSIONS, '.ts', '.tsx'], !isEsm && terser(),
exclude: 'node_modules/**', ],
babelHelpers: 'runtime', };
}), };
],
});
export default isProd && !isTesting export default isProd && !isTesting
? [ ? [
baseConfig(), baseConfig(),
baseConfig({ baseConfig({
mainFile: 'dist/nocss/ReactFlow-nocss.js', outputOptions: {
moduleFile: 'dist/nocss/ReactFlow-nocss.esm.js', dir: 'dist/umd',
format: 'umd',
exports: 'named',
name: 'ReactFlow',
globals: {
react: 'React',
'react-dom': 'ReactDOM',
classcat: 'cc',
'd3-selection': 'd3',
'd3-zoom': 'd3',
'react-draggable': 'ReactDraggable',
zustand: 'zustand',
'zustand/shallow': 'zustandShallow',
'zustand/context': 'zustandContext',
},
},
}),
baseConfig({
outputOptions: {
dir: 'dist/nocss',
},
injectCSS: false, injectCSS: false,
}), }),
] ]
+7 -12
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 cc from 'classcat';
import { useStoreState } from '../../store/hooks'; import { useStore } from '../../store';
import { BackgroundVariant } from '../../types';
import { createGridLinesPath, createGridDotsPath } from './utils'; import { createGridLinesPath, createGridDotsPath } from './utils';
import { BackgroundVariant, ReactFlowState, BackgroundProps } from '../../types';
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
const defaultColors = { const defaultColors = {
[BackgroundVariant.Dots]: '#81818a', [BackgroundVariant.Dots]: '#81818a',
[BackgroundVariant.Lines]: '#eee', [BackgroundVariant.Lines]: '#eee',
}; };
const transformSelector = (s: ReactFlowState) => s.transform;
const Background: FC<BackgroundProps> = ({ const Background: FC<BackgroundProps> = ({
variant = BackgroundVariant.Dots, variant = BackgroundVariant.Dots,
gap = 15, gap = 15,
@@ -25,11 +20,11 @@ const Background: FC<BackgroundProps> = ({
style, style,
className, 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. // 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 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 scaledGap = gap * scale;
const xOffset = x % scaledGap; const xOffset = x % scaledGap;
const yOffset = y % scaledGap; const yOffset = y % scaledGap;
@@ -0,0 +1,11 @@
import React from 'react';
function FitViewIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 30">
<path d="M3.692 4.63c0-.53.4-.938.939-.938h5.215V0H4.708C2.13 0 0 2.054 0 4.63v5.216h3.692V4.631zM27.354 0h-5.2v3.692h5.17c.53 0 .984.4.984.939v5.215H32V4.631A4.624 4.624 0 0027.354 0zm.954 24.83c0 .532-.4.94-.939.94h-5.215v3.768h5.215c2.577 0 4.631-2.13 4.631-4.707v-5.139h-3.692v5.139zm-23.677.94c-.531 0-.939-.4-.939-.94v-5.138H0v5.139c0 2.577 2.13 4.707 4.708 4.707h5.138V25.77H4.631z" />
</svg>
);
}
export default FitViewIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function LockIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0 8 0 4.571 3.429 4.571 7.619v3.048H3.048A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047zm4.724-13.866H7.467V7.619c0-2.59 2.133-4.724 4.723-4.724 2.591 0 4.724 2.133 4.724 4.724v3.048z" />
</svg>
);
}
export default LockIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function MinusIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 5">
<path d="M0 0h32v4.2H0z" />
</svg>
);
}
export default MinusIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function PlusIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<path d="M32 18.133H18.133V32h-4.266V18.133H0v-4.266h13.867V0h4.266v13.867H32z" />
</svg>
);
}
export default PlusIcon;
@@ -0,0 +1,11 @@
import React from 'react';
function UnlockIcon() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 32">
<path d="M21.333 10.667H19.81V7.619C19.81 3.429 16.38 0 12.19 0c-4.114 1.828-1.37 2.133.305 2.438 1.676.305 4.42 2.59 4.42 5.181v3.048H3.047A3.056 3.056 0 000 13.714v15.238A3.056 3.056 0 003.048 32h18.285a3.056 3.056 0 003.048-3.048V13.714a3.056 3.056 0 00-3.048-3.047zM12.19 24.533a3.056 3.056 0 01-3.047-3.047 3.056 3.056 0 013.047-3.048 3.056 3.056 0 013.048 3.048 3.056 3.056 0 01-3.048 3.047z" />
</svg>
);
}
export default UnlockIcon;
+48 -34
View File
@@ -1,29 +1,16 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react'; import React, { memo, useCallback, FC, useEffect, useState } from 'react';
import cc from 'classcat'; 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 PlusIcon from './Icons/Plus';
import MinusIcon from '../../../assets/icons/minus.svg'; import MinusIcon from './Icons/Minus';
import FitviewIcon from '../../../assets/icons/fitview.svg'; import FitviewIcon from './Icons/FitView';
import LockIcon from '../../../assets/icons/lock.svg'; import LockIcon from './Icons/Lock';
import UnlockIcon from '../../../assets/icons/unlock.svg'; import UnlockIcon from './Icons/Unlock';
import useZoomPanHelper from '../../hooks/useZoomPanHelper'; import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
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> {}
export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => ( export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => (
<button type="button" className={cc(['react-flow__controls-button', 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> </button>
); );
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<ControlProps> = ({ const Controls: FC<ControlProps> = ({
style, style,
showZoom = true, showZoom = true,
showFitView = true, showFitView = true,
showInteractive = true, showInteractive = true,
fitViewParams, fitViewOptions,
onZoomIn, onZoomIn,
onZoomOut, onZoomOut,
onFitView, onFitView,
@@ -44,11 +33,11 @@ const Controls: FC<ControlProps> = ({
className, className,
children, children,
}) => { }) => {
const store = useStoreApi();
const [isVisible, setIsVisible] = useState<boolean>(false); const [isVisible, setIsVisible] = useState<boolean>(false);
const setInteractive = useStoreActions((actions) => actions.setInteractive); const isInteractive = useStore(isInteractiveSelector);
const { zoomIn, zoomOut, fitView } = useZoomPanHelper(); const { zoomIn, zoomOut, fitView } = useReactFlow();
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
const mapClasses = cc(['react-flow__controls', className]); const mapClasses = cc(['react-flow__controls', className]);
const onZoomInHandler = useCallback(() => { const onZoomInHandler = useCallback(() => {
@@ -62,14 +51,19 @@ const Controls: FC<ControlProps> = ({
}, [zoomOut, onZoomOut]); }, [zoomOut, onZoomOut]);
const onFitViewHandler = useCallback(() => { const onFitViewHandler = useCallback(() => {
fitView?.(fitViewParams); fitView?.(fitViewOptions);
onFitView?.(); onFitView?.();
}, [fitView, fitViewParams, onFitView]); }, [fitView, fitViewOptions, onFitView]);
const onInteractiveChangeHandler = useCallback(() => { const onInteractiveChangeHandler = useCallback(() => {
setInteractive?.(!isInteractive); store.setState({
nodesDraggable: !isInteractive,
nodesConnectable: !isInteractive,
elementsSelectable: !isInteractive,
});
onInteractiveChange?.(!isInteractive); onInteractiveChange?.(!isInteractive);
}, [isInteractive, setInteractive, onInteractiveChange]); }, [isInteractive, onInteractiveChange]);
useEffect(() => { useEffect(() => {
setIsVisible(true); setIsVisible(true);
@@ -83,21 +77,41 @@ const Controls: FC<ControlProps> = ({
<div className={mapClasses} style={style}> <div className={mapClasses} style={style}>
{showZoom && ( {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 /> <PlusIcon />
</ControlButton> </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 /> <MinusIcon />
</ControlButton> </ControlButton>
</> </>
)} )}
{showFitView && ( {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 /> <FitviewIcon />
</ControlButton> </ControlButton>
)} )}
{showInteractive && ( {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 />} {isInteractive ? <UnlockIcon /> : <LockIcon />}
</ControlButton> </ControlButton>
)} )}
@@ -26,7 +26,7 @@ const MiniMapNode = ({
strokeWidth, strokeWidth,
className, className,
borderRadius, borderRadius,
shapeRendering shapeRendering,
}: MiniMapNodeProps) => { }: MiniMapNodeProps) => {
const { background, backgroundColor } = style || {}; const { background, backgroundColor } = style || {};
const fill = (color || background || backgroundColor) as string; const fill = (color || background || backgroundColor) as string;
+48 -42
View File
@@ -1,27 +1,27 @@
import React, { memo, HTMLAttributes } from 'react'; import React, { memo } from 'react';
import cc from 'classcat'; 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'; 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> { import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types';
nodeColor?: string | StringFunc;
nodeStrokeColor?: string | StringFunc;
nodeClassName?: string | StringFunc;
nodeBorderRadius?: number;
nodeStrokeWidth?: number;
maskColor?: string;
}
declare const window: any; declare const window: any;
const defaultWidth = 200; const defaultWidth = 200;
const defaultHeight = 150; const defaultHeight = 150;
const selector = (s: ReactFlowState) => ({
width: s.width,
height: s.height,
transform: s.transform,
nodeInternals: s.nodeInternals,
});
const MiniMap = ({ const MiniMap = ({
style, style,
className, className,
@@ -32,20 +32,22 @@ const MiniMap = ({
nodeStrokeWidth = 2, nodeStrokeWidth = 2,
maskColor = 'rgb(240, 242, 243, 0.7)', maskColor = 'rgb(240, 242, 243, 0.7)',
}: MiniMapProps) => { }: MiniMapProps) => {
const containerWidth = useStoreState((s) => s.width); const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
const containerHeight = useStoreState((s) => s.height); const [tX, tY, tScale] = transform;
const [tX, tY, tScale] = useStoreState((s) => s.transform);
const nodes = useStoreState((s) => s.nodes);
const mapClasses = cc(['react-flow__minimap', className]); const mapClasses = cc(['react-flow__minimap', className]);
const elementWidth = (style?.width || defaultWidth)! as number; const elementWidth = (style?.width || defaultWidth)! as number;
const elementHeight = (style?.height || defaultHeight)! as number; const elementHeight = (style?.height || defaultHeight)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function const nodeStrokeColorFunc = (
? nodeStrokeColor nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
: () => nodeStrokeColor) as StringFunc; ) as GetMiniMapNodeAttribute;
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc; const nodeClassNameFunc = (
const hasNodes = nodes && nodes.length; 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 bb = getRectOfNodes(nodes);
const viewBB: Rect = { const viewBB: Rect = {
x: -tX / tScale, x: -tX / tScale,
@@ -64,7 +66,7 @@ const MiniMap = ({
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
const width = viewWidth + offset * 2; const width = viewWidth + offset * 2;
const height = viewHeight + 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 ( return (
<svg <svg
@@ -74,24 +76,28 @@ const MiniMap = ({
style={style} style={style}
className={mapClasses} className={mapClasses}
> >
{nodes {Array.from(nodeInternals)
.filter((node) => !node.isHidden) .filter(([_, node]) => !node.hidden && node.width && node.height)
.map((node) => ( .map(([_, node]) => {
<MiniMapNode const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
key={node.id}
x={node.__rf.position.x} return (
y={node.__rf.position.y} <MiniMapNode
width={node.__rf.width} key={node.id}
height={node.__rf.height} x={positionAbsolute?.x || 0}
style={node.style} y={positionAbsolute?.y || 0}
className={nodeClassNameFunc(node)} width={node.width!}
color={nodeColorFunc(node)} height={node.height!}
borderRadius={nodeBorderRadius} style={node.style}
strokeColor={nodeStrokeColorFunc(node)} className={nodeClassNameFunc(node)}
strokeWidth={nodeStrokeWidth} color={nodeColorFunc(node)}
shapeRendering={shapeRendering} borderRadius={nodeBorderRadius}
/> strokeColor={nodeStrokeColorFunc(node)}
))} strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
/>
);
})}
<path <path
className="react-flow__minimap-mask" className="react-flow__minimap-mask"
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
@@ -1,16 +1,8 @@
import React, { FC, useMemo } from 'react'; import React, { FC, PropsWithChildren } from 'react';
import { Provider } from 'react-redux';
import { initialState } from '../../store'; import { Provider, createStore } from '../../store';
import configureStore from '../../store/configure-store';
const ReactFlowProvider: FC = ({ children }) => { const ReactFlowProvider: FC<PropsWithChildren<{}>> = ({ children }) => <Provider createStore={createStore}>{children}</Provider>;
const store = useMemo(() => {
return configureStore(initialState);
}, []);
return <Provider store={store}>{children}</Provider>;
};
ReactFlowProvider.displayName = 'ReactFlowProvider'; ReactFlowProvider.displayName = 'ReactFlowProvider';
+1 -1
View File
@@ -1,5 +1,5 @@
// These components are not used by React Flow directly // 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 MiniMap } from './MiniMap';
export { default as Controls, ControlButton } from './Controls'; export { default as Controls, ControlButton } from './Controls';
+35
View File
@@ -0,0 +1,35 @@
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-sponsor' ||
proOptions?.account === 'paid-enterprise' ||
proOptions?.account === 'paid-custom') &&
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 are subscribed to React Flow Pro: https://pro.reactflow.dev/pricing"
>
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
React Flow
</a>
</div>
);
}
export default Attribution;
+87 -64
View File
@@ -1,39 +1,25 @@
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 { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
ElementId, import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
Node,
Transform,
HandleElement,
Position,
ConnectionLineType,
ConnectionLineComponent,
HandleType,
} from '../../types';
interface ConnectionLineProps { interface ConnectionLineProps {
connectionNodeId: ElementId; connectionNodeId: string;
connectionHandleId: ElementId | null; connectionHandleId: string | null;
connectionHandleType: HandleType; connectionHandleType: HandleType;
connectionPositionX: number; connectionPositionX: number;
connectionPositionY: number; connectionPositionY: number;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
nodes: Node[];
transform: Transform;
isConnectable: boolean; isConnectable: boolean;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
CustomConnectionLineComponent?: ConnectionLineComponent; CustomConnectionLineComponent?: ConnectionLineComponent;
} }
const getSourceHandle = (handleId: ElementId | null, sourceNode: Node, connectionHandleType: HandleType) => { const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound =
sourceNode.__rf.handleBounds[connectionHandleType] || sourceNode.__rf.handleBounds[handleTypeInverted];
return handleId ? handleBound.find((d: HandleElement) => d.id === handleId) : handleBound[0];
};
export default ({ export default ({
connectionNodeId, connectionNodeId,
@@ -43,35 +29,76 @@ export default ({
connectionPositionX, connectionPositionX,
connectionPositionY, connectionPositionY,
connectionLineType = ConnectionLineType.Bezier, connectionLineType = ConnectionLineType.Bezier,
nodes = [],
transform,
isConnectable, isConnectable,
CustomConnectionLineComponent, CustomConnectionLineComponent,
}: ConnectionLineProps) => { }: ConnectionLineProps) => {
const [sourceNode, setSourceNode] = useState<Node | null>(null);
const nodeId = connectionNodeId; const nodeId = connectionNodeId;
const handleId = connectionHandleId; const handleId = connectionHandleId;
useEffect(() => { const { nodeInternals, transform } = useStore(selector, shallow);
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null; const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
setSourceNode(nextSourceNode);
}, []);
if (!sourceNode || !isConnectable) { if (!fromNode.current || !isConnectable || !fromNode.current.handleBounds?.[connectionHandleType]) {
return null; return null;
} }
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType); const handleBound = fromNode.current.handleBounds?.[connectionHandleType];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2; const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0];
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2;
const sourceX = sourceNode.__rf.position.x + sourceHandleX; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0;
const sourceY = sourceNode.__rf.position.y + sourceHandleY; const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX;
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2]; const toX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2]; const toY = (connectionPositionY - transform[1]) / transform[2];
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right; const fromPosition = fromHandle?.position;
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
let toPosition: Position | undefined;
switch (fromPosition) {
case Position.Left:
toPosition = Position.Right;
break;
case Position.Right:
toPosition = Position.Left;
break;
case Position.Top:
toPosition = Position.Bottom;
break;
case Position.Bottom:
toPosition = Position.Top;
break;
}
let sourceX: number,
sourceY: number,
sourcePosition: Position | undefined,
targetX: number,
targetY: number,
targetPosition: Position | undefined;
switch (connectionHandleType) {
case 'source':
{
sourceX = fromX;
sourceY = fromY;
sourcePosition = fromPosition;
targetX = toX;
targetY = toY;
targetPosition = toPosition;
}
break;
case 'target':
{
sourceX = toX;
sourceY = toY;
sourcePosition = toPosition;
targetX = fromX;
targetY = fromY;
targetPosition = fromPosition;
}
break;
}
if (CustomConnectionLineComponent) { if (CustomConnectionLineComponent) {
return ( return (
@@ -79,14 +106,17 @@ export default ({
<CustomConnectionLineComponent <CustomConnectionLineComponent
sourceX={sourceX} sourceX={sourceX}
sourceY={sourceY} sourceY={sourceY}
sourcePosition={sourceHandle?.position} sourcePosition={sourcePosition}
targetX={targetX} targetX={targetX}
targetY={targetY} targetY={targetY}
targetPosition={targetPosition} targetPosition={targetPosition}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode} fromNode={fromNode.current}
sourceHandle={sourceHandle} fromHandle={fromHandle}
// backward compatibility, mark as deprecated?
sourceNode={fromNode.current}
sourceHandle={fromHandle}
/> />
</g> </g>
); );
@@ -94,34 +124,27 @@ export default ({
let dAttr: string = ''; let dAttr: string = '';
const pathParams = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) { if (connectionLineType === ConnectionLineType.Bezier) {
dAttr = getBezierPath({ // we assume the destination position is opposite to the source position
sourceX, dAttr = getBezierPath(pathParams);
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
} else if (connectionLineType === ConnectionLineType.Step) { } else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({ dAttr = getSmoothStepPath({
sourceX, ...pathParams,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
borderRadius: 0, borderRadius: 0,
}); });
} else if (connectionLineType === ConnectionLineType.SmoothStep) { } else if (connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath({ dAttr = getSmoothStepPath(pathParams);
sourceX, } else if (connectionLineType === ConnectionLineType.SimpleBezier) {
sourceY, dAttr = getSimpleBezierPath(pathParams);
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
});
} else { } else {
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`; dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
} }
+39
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}
</>
);
};
+122 -43
View File
@@ -1,19 +1,63 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import EdgeText from './EdgeText';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeProps, Position } from '../../types'; import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
interface GetBezierPathParams { export interface GetBezierPathParams {
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
sourcePosition?: Position; sourcePosition?: Position;
targetX: number; targetX: number;
targetY: number; targetY: number;
targetPosition?: Position; targetPosition?: Position;
centerX?: number; curvature?: number;
centerY?: number; }
interface GetControlWithCurvatureParams {
pos: Position;
x1: number;
y1: number;
x2: number;
y2: number;
c: number;
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance;
} else {
return curvature * 25 * Math.sqrt(-distance);
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
{
ctX = x1 - calculateControlOffset(x1 - x2, c);
ctY = y1;
}
break;
case Position.Right:
{
ctX = x1 + calculateControlOffset(x2 - x1, c);
ctY = y1;
}
break;
case Position.Top:
{
ctX = x1;
ctY = y1 - calculateControlOffset(y1 - y2, c);
}
break;
case Position.Bottom:
{
ctX = x1;
ctY = y1 + calculateControlOffset(y2 - y1, c);
}
break;
}
return [ctX, ctY];
} }
export function getBezierPath({ export function getBezierPath({
@@ -23,26 +67,63 @@ export function getBezierPath({
targetX, targetX,
targetY, targetY,
targetPosition = Position.Top, targetPosition = Position.Top,
centerX, curvature = 0.25,
centerY,
}: GetBezierPathParams): string { }: GetBezierPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY }); const [sourceControlX, sourceControlY] = getControlWithCurvature({
const leftAndRight = [Position.Left, Position.Right]; pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
});
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
const cX = typeof centerX !== 'undefined' ? centerX : _centerX; // @TODO: this function will recalculate the control points
const cY = typeof centerY !== 'undefined' ? centerY : _centerY; // one option is to let getXXXPath() return center points
// but will introduce breaking changes
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`; // the getCenter() of other types of edges might need to change, too
export function getBezierCenter({
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { sourceX,
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`; sourceY,
} else if (leftAndRight.includes(targetPosition)) { sourcePosition = Position.Bottom,
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`; targetX,
} else if (leftAndRight.includes(sourcePosition)) { targetY,
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`; targetPosition = Position.Top,
} curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
return path; const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
});
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
});
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const xOffset = Math.abs(centerX - sourceX);
const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
} }
export default memo( export default memo(
@@ -60,39 +141,37 @@ export default memo(
labelBgPadding, labelBgPadding,
labelBgBorderRadius, labelBgBorderRadius,
style, style,
arrowHeadType, markerEnd,
markerEndId, markerStart,
curvature,
}: EdgeProps) => { }: EdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); const params = {
const path = getBezierPath({
sourceX, sourceX,
sourceY, sourceY,
sourcePosition, sourcePosition,
targetX, targetX,
targetY, targetY,
targetPosition, targetPosition,
}); curvature,
};
const path = getBezierPath(params);
const [centerX, centerY] = getBezierCenter(params);
const text = label ? ( return (
<EdgeText <BaseEdge
x={centerX} path={path}
y={centerY} centerX={centerX}
centerY={centerY}
label={label} label={label}
labelStyle={labelStyle} labelStyle={labelStyle}
labelShowBg={labelShowBg} labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle} labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding} labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius} labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/> />
) : null;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
return (
<>
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
{text}
</>
); );
} }
); );
+149
View File
@@ -0,0 +1,149 @@
import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
export interface GetSimpleBezierPathParams {
sourceX: number;
sourceY: number;
sourcePosition?: Position;
targetX: number;
targetY: number;
targetPosition?: Position;
}
interface GetControlParams {
pos: Position;
x1: number;
y1: number;
x2: number;
y2: number;
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
case Position.Right:
{
ctX = 0.5 * (x1 + x2);
ctY = y1;
}
break;
case Position.Top:
case Position.Bottom:
{
ctX = x1;
ctY = 0.5 * (y1 + y2);
}
break;
}
return [ctX, ctY];
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const xOffset = Math.abs(centerX - sourceX);
const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
}: EdgeProps) => {
const params = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
const path = getSimpleBezierPath(params);
const [centerX, centerY] = getSimpleBezierCenter(params);
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}
/>
);
}
);
+13 -18
View File
@@ -1,8 +1,8 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import EdgeText from './EdgeText'; import { getCenter } from './utils';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeSmoothStepProps, Position } from '../../types'; import { EdgeSmoothStepProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
// These are some helper methods for drawing the round corners // These are some helper methods for drawing the round corners
// The name indicates the direction of the path. "bottomLeftCorner" goes // 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 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}`; 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; sourceX: number;
sourceY: number; sourceY: number;
sourcePosition?: Position; sourcePosition?: Position;
@@ -130,8 +130,8 @@ export default memo(
style, style,
sourcePosition = Position.Bottom, sourcePosition = Position.Bottom,
targetPosition = Position.Top, targetPosition = Position.Top,
arrowHeadType, markerEnd,
markerEndId, markerStart,
borderRadius = 5, borderRadius = 5,
}: EdgeSmoothStepProps) => { }: EdgeSmoothStepProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
@@ -146,26 +146,21 @@ export default memo(
borderRadius, borderRadius,
}); });
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); return (
<BaseEdge
const text = label ? ( path={path}
<EdgeText centerX={centerX}
x={centerX} centerY={centerY}
y={centerY}
label={label} label={label}
labelStyle={labelStyle} labelStyle={labelStyle}
labelShowBg={labelShowBg} labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle} labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding} labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius} labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/> />
) : null;
return (
<>
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
{text}
</>
); );
} }
); );
+12 -21
View File
@@ -1,7 +1,6 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import EdgeText from './EdgeText'; import BaseEdge from './BaseEdge';
import { getMarkerEnd } from './utils';
import { EdgeProps } from '../../types'; import { EdgeProps } from '../../types';
export default memo( export default memo(
@@ -17,39 +16,31 @@ export default memo(
labelBgPadding, labelBgPadding,
labelBgBorderRadius, labelBgBorderRadius,
style, style,
arrowHeadType, markerEnd,
markerEndId, markerStart,
}: EdgeProps) => { }: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2; const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset; const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
const xOffset = Math.abs(targetX - sourceX) / 2; const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset; const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
const text = label ? ( return (
<EdgeText <BaseEdge
x={centerX} path={path}
y={centerY} centerX={centerX}
centerY={centerY}
label={label} label={label}
labelStyle={labelStyle} labelStyle={labelStyle}
labelShowBg={labelShowBg} labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle} labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding} labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius} 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}
</>
); );
} }
); );
+3 -2
View File
@@ -1,4 +1,5 @@
export { default as BezierEdge } from './BezierEdge'; export { default as SimpleBezierEdge } from './SimpleBezierEdge';
export { default as StepEdge } from './StepEdge';
export { default as SmoothStepEdge } from './SmoothStepEdge'; export { default as SmoothStepEdge } from './SmoothStepEdge';
export { default as StepEdge } from './StepEdge';
export { default as StraightEdge } from './StraightEdge'; export { default as StraightEdge } from './StraightEdge';
export { default as BezierEdge } from './BezierEdge';
+3 -3
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) { if (typeof markerEndId !== 'undefined' && markerEndId) {
return `url(#${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 { export interface GetCenterParams {
+35 -20
View File
@@ -1,10 +1,17 @@
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react'; import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks'; import { useStore, useStoreApi } from '../../store';
import { Edge, EdgeProps, WrapEdgeProps } from '../../types'; import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
import { onMouseDown } from '../../components/Handle/handler'; import { onMouseDown } from '../../components/Handle/handler';
import { EdgeAnchor } from './EdgeAnchor'; import { EdgeAnchor } from './EdgeAnchor';
import { getMarkerId } from '../../utils/graph';
const selector = (s: ReactFlowState) => ({
addSelectedEdges: s.addSelectedEdges,
connectionMode: s.connectionMode,
});
export default (EdgeComponent: ComponentType<EdgeProps>) => { export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({ const EdgeWrapper = ({
@@ -23,7 +30,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgPadding, labelBgPadding,
labelBgBorderRadius, labelBgBorderRadius,
style, style,
arrowHeadType,
source, source,
target, target,
sourceX, sourceX,
@@ -33,29 +39,27 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
sourcePosition, sourcePosition,
targetPosition, targetPosition,
elementsSelectable, elementsSelectable,
markerEndId, hidden,
isHidden,
sourceHandleId, sourceHandleId,
targetHandleId, targetHandleId,
handleEdgeUpdate,
onConnectEdge,
onContextMenu, onContextMenu,
onMouseEnter, onMouseEnter,
onMouseMove, onMouseMove,
onMouseLeave, onMouseLeave,
edgeUpdaterRadius, edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart, onEdgeUpdateStart,
onEdgeUpdateEnd, onEdgeUpdateEnd,
markerEnd,
markerStart,
}: WrapEdgeProps): JSX.Element | null => { }: WrapEdgeProps): JSX.Element | null => {
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const store = useStoreApi();
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
const connectionMode = useStoreState((state) => state.connectionMode);
const [updating, setUpdating] = useState<boolean>(false); const [updating, setUpdating] = useState<boolean>(false);
const inactive = !elementsSelectable && !onClick; const inactive = !elementsSelectable && !onClick;
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
const edgeClasses = cc([ const edgeClasses = cc([
'react-flow__edge', 'react-flow__edge',
`react-flow__edge-${type}`, `react-flow__edge-${type}`,
@@ -89,8 +93,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeClick = useCallback( const onEdgeClick = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => { (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
if (elementsSelectable) { if (elementsSelectable) {
unsetNodesSelection(); store.setState({ nodesSelectionActive: false });
addSelectedElements(edgeElement); addSelectedEdges([edgeElement.id]);
} }
onClick?.(event, edgeElement); onClick?.(event, edgeElement);
@@ -147,21 +151,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType) ? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement, handleType)
: undefined; : undefined;
const onConnectEdge = (connection: Connection) => {
const { edges } = store.getState();
const edge = edges.find((e) => e.id === id);
if (edge && onEdgeUpdate) {
onEdgeUpdate(edge, connection);
}
};
onMouseDown( onMouseDown(
event, event,
handleId, handleId,
nodeId, nodeId,
setConnectionNodeId, store.setState,
setPosition,
onConnectEdge, onConnectEdge,
isTarget, isTarget,
isValidConnection, isValidConnection,
connectionMode, connectionMode,
handleType, handleType,
_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( const onEdgeUpdaterSourceMouseDown = useCallback(
@@ -180,8 +193,10 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]); const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [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; return null;
} }
@@ -209,16 +224,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
labelBgBorderRadius={labelBgBorderRadius} labelBgBorderRadius={labelBgBorderRadius}
data={data} data={data}
style={style} style={style}
arrowHeadType={arrowHeadType}
sourceX={sourceX} sourceX={sourceX}
sourceY={sourceY} sourceY={sourceY}
targetX={targetX} targetX={targetX}
targetY={targetY} targetY={targetY}
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
markerEndId={markerEndId}
sourceHandleId={sourceHandleId} sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId} targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
/> />
{handleEdgeUpdate && ( {handleEdgeUpdate && (
<g <g
-20
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;
+34 -33
View File
@@ -1,24 +1,19 @@
import { MouseEvent as ReactMouseEvent } from 'react'; import { MouseEvent as ReactMouseEvent } from 'react';
import { SetState } from 'zustand';
import { getHostForElement } from '../../utils'; import { getHostForElement } from '../../utils';
import { import {
ElementId, OnConnect,
XYPosition, OnConnectStart,
OnConnectFunc, OnConnectStop,
OnConnectStartFunc, OnConnectEnd,
OnConnectStopFunc,
OnConnectEndFunc,
ConnectionMode, ConnectionMode,
SetConnectionId,
Connection, Connection,
HandleType, HandleType,
ReactFlowState,
} from '../../types'; } from '../../types';
type ValidConnectionFunc = (connection: Connection) => boolean; type ValidConnectionFunc = (connection: Connection) => boolean;
export type SetSourceIdFunc = (params: SetConnectionId) => void;
export type SetPosition = (pos: XYPosition) => void;
type Result = { type Result = {
elementBelow: Element | null; 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 } // 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, event: MouseEvent,
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
isTarget: boolean, isTarget: boolean,
nodeId: ElementId, nodeId: string,
handleId: ElementId | null, handleId: string | null,
isValidConnection: ValidConnectionFunc, isValidConnection: ValidConnectionFunc,
doc: Document | ShadowRoot 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 elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false; const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
const elementBelowIsSource = elementBelow?.classList.contains('source') || false; const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
@@ -91,19 +84,18 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown( export function onMouseDown(
event: ReactMouseEvent, event: ReactMouseEvent,
handleId: ElementId | null, handleId: string | null,
nodeId: ElementId, nodeId: string,
setConnectionNodeId: SetSourceIdFunc, setState: SetState<ReactFlowState>,
setPosition: SetPosition, onConnect: OnConnect,
onConnect: OnConnectFunc,
isTarget: boolean, isTarget: boolean,
isValidConnection: ValidConnectionFunc, isValidConnection: ValidConnectionFunc,
connectionMode: ConnectionMode, connectionMode: ConnectionMode,
elementEdgeUpdaterType?: HandleType, elementEdgeUpdaterType?: HandleType,
onEdgeUpdateEnd?: (evt: MouseEvent) => void, onEdgeUpdateEnd?: (evt: MouseEvent) => void,
onConnectStart?: OnConnectStartFunc, onConnectStart?: OnConnectStart,
onConnectStop?: OnConnectStopFunc, onConnectStop?: OnConnectStop,
onConnectEnd?: OnConnectEndFunc onConnectEnd?: OnConnectEnd
): void { ): void {
const reactFlowNode = (event.target as Element).closest('.react-flow'); const reactFlowNode = (event.target as Element).closest('.react-flow');
// when react-flow is used inside a shadow root we can't use document // when react-flow is used inside a shadow root we can't use document
@@ -113,7 +105,6 @@ export function onMouseDown(
return; return;
} }
// @ts-ignore
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY); const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
const elementBelowIsTarget = elementBelow?.classList.contains('target'); const elementBelowIsTarget = elementBelow?.classList.contains('target');
const elementBelowIsSource = elementBelow?.classList.contains('source'); const elementBelowIsSource = elementBelow?.classList.contains('source');
@@ -126,18 +117,24 @@ export function onMouseDown(
const containerBounds = reactFlowNode.getBoundingClientRect(); const containerBounds = reactFlowNode.getBoundingClientRect();
let recentHoveredHandle: Element; let recentHoveredHandle: Element;
setPosition({ setState({
x: event.clientX - containerBounds.left, connectionPosition: {
y: event.clientY - containerBounds.top, 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 }); onConnectStart?.(event, { nodeId, handleId, handleType });
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
setPosition({ setState({
x: event.clientX - containerBounds.left, connectionPosition: {
y: event.clientY - containerBounds.top, x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top,
},
}); });
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid( const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
@@ -187,7 +184,11 @@ export function onMouseDown(
} }
resetRecentHandle(recentHoveredHandle); resetRecentHandle(recentHoveredHandle);
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null }); setState({
connectionNodeId: null,
connectionHandleId: null,
connectionHandleType: null,
});
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject); doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject); doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
+107 -33
View File
@@ -1,16 +1,29 @@
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react'; import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
import cc from 'classcat'; 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 NodeIdContext from '../../contexts/NodeIdContext';
import { HandleProps, Connection, ElementId, Position } from '../../types'; import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
import { checkElementBelowIsValid, onMouseDown } from './handler';
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler'; import { getHostForElement } from '../../utils';
import { addEdge } from '../../utils/graph';
const alwaysValid = () => true; const alwaysValid = () => true;
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>; 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>( const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
( (
{ {
@@ -26,49 +39,64 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
}, },
ref ref
) => { ) => {
const nodeId = useContext(NodeIdContext) as ElementId; const store = useStoreApi();
const setPosition = useStoreActions((actions) => actions.setConnectionPosition); const nodeId = useContext(NodeIdContext) as string;
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId); const {
const onConnectAction = useStoreState((state) => state.onConnect); onConnectAction,
const onConnectStart = useStoreState((state) => state.onConnectStart); onConnectStart,
const onConnectStop = useStoreState((state) => state.onConnectStop); onConnectStop,
const onConnectEnd = useStoreState((state) => state.onConnectEnd); onConnectEnd,
const connectionMode = useStoreState((state) => state.connectionMode); connectionMode,
connectionStartHandle,
connectOnClick,
hasDefaultEdges,
} = useStore(selector, shallow);
const handleId = id || null; const handleId = id || null;
const isTarget = type === 'target'; const isTarget = type === 'target';
const onConnectExtended = useCallback( const onConnectExtended = useCallback(
(params: Connection) => { (params: Connection) => {
onConnectAction?.(params); const { defaultEdgeOptions } = store.getState();
onConnect?.(params);
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( const onMouseDownHandler = useCallback(
(event: React.MouseEvent) => { (event: React.MouseEvent) => {
onMouseDown( if (event.button === 0) {
event, onMouseDown(
handleId, event,
nodeId, handleId,
setConnectionNodeId as unknown as SetSourceIdFunc, nodeId,
setPosition as unknown as SetPosition, store.setState,
onConnectExtended, onConnectExtended,
isTarget, isTarget,
isValidConnection, isValidConnection,
connectionMode, connectionMode,
undefined, undefined,
undefined, undefined,
onConnectStart, onConnectStart,
onConnectStop, onConnectStop,
onConnectEnd onConnectEnd
); );
}
}, },
[ [
handleId, handleId,
nodeId, nodeId,
setConnectionNodeId,
setPosition,
onConnectExtended, onConnectExtended,
isTarget, isTarget,
isValidConnection, 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([ const handleClasses = cc([
'react-flow__handle', 'react-flow__handle',
`react-flow__handle-${position}`, `react-flow__handle-${position}`,
@@ -88,6 +157,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
source: !isTarget, source: !isTarget,
target: isTarget, target: isTarget,
connectable: isConnectable, connectable: isConnectable,
connecting:
connectionStartHandle?.nodeId === nodeId &&
connectionStartHandle?.handleId === handleId &&
connectionStartHandle?.type === type,
}, },
]); ]);
@@ -98,6 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
data-handlepos={position} data-handlepos={position}
className={handleClasses} className={handleClasses}
onMouseDown={onMouseDownHandler} onMouseDown={onMouseDownHandler}
onClick={connectOnClick ? onClick : undefined}
ref={ref} ref={ref}
{...rest} {...rest}
> >
+9 -7
View File
@@ -8,13 +8,15 @@ const DefaultNode = ({
isConnectable, isConnectable,
targetPosition = Position.Top, targetPosition = Position.Top,
sourcePosition = Position.Bottom, sourcePosition = Position.Bottom,
}: NodeProps) => ( }: NodeProps) => {
<> return (
<Handle type="target" position={targetPosition} isConnectable={isConnectable} /> <>
{data.label} <Handle type="target" position={targetPosition} isConnectable={isConnectable} />
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} /> {data?.label}
</> <Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
); </>
);
};
DefaultNode.displayName = 'DefaultNode'; DefaultNode.displayName = 'DefaultNode';
+5
View File
@@ -0,0 +1,5 @@
const GroupNode = () => null;
GroupNode.displayName = 'GroupNode';
export default GroupNode;

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