Merge pull request #1555 from wbkd/v10

next major release v10
This commit is contained in:
Moritz Klack
2022-03-14 19:46:45 +01:00
committed by GitHub
152 changed files with 34026 additions and 26397 deletions
+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"]
+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
+64 -40
View File
@@ -1,52 +1,62 @@
[![react-flow](https://user-images.githubusercontent.com/2857535/95224198-b2540000-07fa-11eb-832d-361d72d60345.jpg)](https://reactflow.dev) ![readme-header-dark](https://user-images.githubusercontent.com/3797215/156384064-08a889d6-73c0-4cbf-8ff3-28dc601d1f5f.svg#gh-dark-mode-only)
![readme-header](https://user-images.githubusercontent.com/3797215/156259138-fb9f59f8-52f2-474a-b78c-6570867e4ead.svg#gh-light-mode-only)
React Flow is a library for building node based graphs. You can easily implement custom node types and it comes with components like a mini map and graph controls. Feel free to check out the [examples](https://reactflow.dev/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started. <div align="center">
## 👉 Next Major Release 👈 ![GitHub License MIT](https://img.shields.io/github/license/wbkd/react-flow?color=%23FF0072)
![npm downloads](https://img.shields.io/npm/dt/react-flow-renderer?color=%23FF0072&label=downloads)
![GitHub Repo stars](https://img.shields.io/github/stars/wbkd/react-flow?color=%23FF0072)
![GitHub release (latest by date)](https://img.shields.io/github/v/release/wbkd/react-flow?color=%23FF0072)
We are currently working on the next version. You can find the installation instructions and breaking changes in [this PR](https://github.com/wbkd/react-flow/pull/1555). Your feedback is highly appreciated :) ### Wire Your Ideas With React Flow!
A highly customizable React component for building interactive graphs and node-based editors.
- **Website:** https://reactflow.dev [🚀 Getting Started](https://reactflow.dev/docs/getting-started/installation) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/Bqt6xrs) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
- **Documentation:** https://reactflow.dev/docs
- **Examples:** https://reactflow.dev/examples </div>
- **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. ----
## Key Features
- **Easy to use:** Seamless zooming and panning, single- and multi selection of graph elements and keyboard shortcuts are supported out of the box
- **Customizable:** Different [node](https://reactflow.dev/docs/api/nodes/node-types) and [edge types](https://reactflow.dev/docs/api/edges/edge-types) and support for custom nodes with multiple handles and custom edges
- **Fast rendering:** Only nodes that have changed are re-rendered and only those in the viewport are displayed
- **Hooks and Utils:** [Hooks](https://reactflow.dev/docs/api/hooks/use-react-flow) for handling nodes, edges and the viewport and graph [helper functions](https://reactflow.dev/docs/api/graph-util-functions)
- **Plugin Components:** [Background](https://reactflow.dev/docs/api/plugin-components/background), [MiniMap](https://reactflow.dev/docs/api/plugin-components/minimap) and [Controls](https://reactflow.dev/docs/api/plugin-components/controls)
- **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
## Installation ## 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
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
## Development
Before you start you need to install the React Flow dependencies via `npm install` and the ones of the examples `cd example && npm install`. 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:
@@ -71,19 +81,33 @@ Testing is done with cypress. You can find the tests in the [`integration/flow`]
npm run test npm run test
``` ```
## Maintainer ## Attribution
Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick) Since v10.0.0 the React Flow renderer includes a small attribution that links to the React Flow website. It is only allowed to remove the attribution if you are subscribed to one of the [pro plans](https://pro.reactflow.dev/pricing). A subscription also provides more features such as a private Discord channel with the creators of React Flow or access to the source code of Datablocks (a node-based data editor built with React Flow).
## Support ## Maintainers
If you need custom support or features for your application we are [happy to hear from you](https://webkid.io/contact). React Flow is developed and maintained by [webkid](https://webkid.io), a web development agency with the focus on data driven applications from Berlin. If you need help or want to talk to us about a collaboration, feel free to contact us:
* Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
* Christopher Möller • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze)
### Thanks! You can also use our [contact form](https://webkid.io/contact/) or join the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
Special thanks to [Andy Lindemann](https://github.com/AndyLnd) for a lot of helpful contributions! ## Community Packages
--- * [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
React Flow is maintained by [webkid](https://webkid.io), a data visualization agency from Berlin. If you need help or want to develop react-based tools or data visualizations, [get in touch](https://webkid.io/contact)! ## Credits
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in your browser. Under the hood, React Flow depends on these great libraries:
* [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
* [react-draggable](https://github.com/react-grid-layout/react-draggable) - used for making the nodes draggable
* [zustand](https://github.com/pmndrs/zustand) - internal state management
## License
React Flow is [MIT licensed](https://github.com/wbkd/react-flow/blob/main/LICENSE).
+3 -2
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
}
+27612 -21376
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} />
+32 -25
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 => {
@@ -132,4 +139,4 @@ const CustomNodeFlow = () => {
); );
}; };
export default CustomNodeFlow; export default CustomNodeFlow;
+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}
+2 -5
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, getEdgeCenter } from 'react-flow-renderer';
const CustomEdge: FC<EdgeProps> = ({ const CustomEdge: FC<EdgeProps> = ({
id, id,
@@ -10,11 +10,8 @@ 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] = getEdgeCenter({ const [centerX, centerY] = getEdgeCenter({
sourceX, sourceX,
sourceY, sourceY,
@@ -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
+52 -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,13 @@ 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}
deleteKeyCode={null}
> >
<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>
); );
+33 -30
View File
@@ -1,58 +1,61 @@
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,
} 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(30, 30);
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));
}, []);
return ( return (
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}> <ReactFlow nodes={nodes} edges={edges} onInit={onInit} onConnect={onConnect} onNodesChange={onNodesChange}>
<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,16 +1,19 @@
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';
const initialElements: Elements = [ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
@@ -46,29 +49,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) => console.log('start update', edge); const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge);
const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge); const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', 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)} />
@@ -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>
+30 -20
View File
@@ -1,42 +1,46 @@
import React, { useState, useCallback } from 'react'; import { useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
removeElements, Node,
addEdge, addEdge,
Background, Background,
MiniMap, MiniMap,
useZoomPanHelper, useReactFlow,
ReactFlowProvider, ReactFlowProvider,
Elements,
ElementId,
Connection, Connection,
Edge, Edge,
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 }, 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' },
]; ];
let id = 5; let id = 5;
const getId = (): ElementId => `${id++}`;
const getId = () => `${id++}`;
const UseZoomPanHelperFlow = () => { const UseZoomPanHelperFlow = () => {
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));
const { project, setCenter, zoomIn, zoomOut } = useZoomPanHelper(); const { project, setCenter, zoomIn, zoomOut, fitView } = useReactFlow();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => { (evt) => {
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 }); const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
setElements((els) => setNodes((nds) =>
els.concat({ nds.concat({
id: getId(), id: getId(),
position: projectedPosition, position: projectedPosition,
data: { data: {
@@ -48,25 +52,31 @@ const UseZoomPanHelperFlow = () => {
[project] [project]
); );
const onElementClick = useCallback( const onNodeClick = useCallback(
(_, element) => { (_, element) => {
const { x, y } = element.position; const { x, y } = element.position;
setCenter(x, y, 1); setCenter(x, y, { zoom: 1, duration: 1200 });
}, },
[setCenter] [setCenter]
); );
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}
fitView
fitViewOptions={{ duration: 1200, padding: 0.2 }}
maxZoom={Infinity}
> >
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}> <div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}>
<button onClick={() => zoomIn(1200)}>zoomIn</button> <button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button>
<button onClick={() => zoomOut(0)}>zoomOut</button> <button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button>
</div> </div>
<Background /> <Background />
<MiniMap /> <MiniMap />
+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;
+122 -95
View File
@@ -1,89 +1,106 @@
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 UseZoomPanHelper from './UseZoomPanHelper';
import MultiFlows from './MultiFlows'; import Validation from './Validation';
import DragHandle from './DragHandle';
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,
@@ -93,28 +110,40 @@ const routes = [
component: NodeTypesObjectChange, component: NodeTypesObjectChange,
}, },
{ {
path: '/updatable-edge', path: '/provider',
component: UpdatableEdge, component: Provider,
},
{
path: '/update-node',
component: UpdateNode,
}, },
{ {
path: '/save-restore', path: '/save-restore',
component: SaveRestore, component: SaveRestore,
}, },
{ {
path: '/drag-and-drop', path: '/stress',
component: DragNDrop, component: Stress,
}, },
{ {
path: '/layouting', path: '/subflow',
component: Layout, component: Subflow,
}, },
{ {
path: '/switch', path: '/switch',
component: SwitchFlows, component: SwitchFlow,
},
{
path: '/touch-device',
component: TouchDevice,
},
{
path: '/undirectional',
component: Undirectional,
},
{
path: '/updatable-edge',
component: UpdatableEdge,
},
{
path: '/update-node',
component: UpdateNode,
}, },
{ {
path: '/usezoompanhelper', path: '/usezoompanhelper',
@@ -125,46 +154,44 @@ const routes = [
component: UseUpdateNodeInternals, component: UseUpdateNodeInternals,
}, },
{ {
path: '/undirectional', path: '/validation',
component: Undirectional, component: Validation,
}, },
{ {
path: '/multiflows', path: '/controlled-uncontrolled',
component: MultiFlows, component: ControlledUncontrolled,
},
{
path: '/draghandle',
component: DragHandle,
}, },
]; ];
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>
<a className="logo" href="https://github.com/wbkd/react-flow"> <a className="logo" href="https://github.com/wbkd/react-flow">
React Flow Dev React Flow Dev
</a> </a>
<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"
} }
+622 -1048
View File
File diff suppressed because it is too large Load Diff
+22 -24
View File
@@ -1,12 +1,13 @@
{ {
"name": "react-flow-renderer", "name": "react-flow-renderer",
"version": "9.7.4", "version": "10.0.0-next.51",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
"main": "dist/ReactFlow.js", "type": "module",
"module": "dist/ReactFlow.esm.js", "main": "dist/esm/index.js",
"types": "dist/index.d.ts", "module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"license": "MIT", "license": "MIT",
"repository": { "repository": {
"type": "git", "type": "git",
@@ -28,51 +29,48 @@
"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.2",
"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", "@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", "rollup": "^2.70.1",
"rollup-plugin-bundle-size": "^1.0.3",
"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-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
};
+35 -22
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 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 pkg from './package.json'; import pkg from './package.json';
@@ -14,38 +14,51 @@ 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 } = {}) => ({ export const baseConfig = ({ outputDir = 'dist/esm', injectCSS = true } = {}) => ({
input: 'src/index.ts', input: [
external: ['react', 'react-dom', (id) => id.includes('@babel/runtime')], 'src/index.ts',
'src/additional-components/Controls/index.tsx',
'src/additional-components/Background/index.tsx',
'src/additional-components/MiniMap/index.tsx',
'src/hooks/useReactFlow.ts',
'src/hooks/useNodes.ts',
'src/hooks/useEdges.ts',
'src/hooks/useViewport.ts',
'src/hooks/useUpdateNodeInternals.ts',
],
external: [
'react',
'react-dom',
'classcat',
'd3-selection',
'd3-zoom',
'react-draggable',
'zustand',
'zustand/shallow',
'zustand/context',
(id) => id.includes('@babel/runtime'),
],
onwarn(warning, rollupWarn) { onwarn(warning, rollupWarn) {
if (warning.code !== 'CIRCULAR_DEPENDENCY') { if (warning.code !== 'CIRCULAR_DEPENDENCY') {
rollupWarn(warning); rollupWarn(warning);
} }
}, },
output: [ output: {
{ dir: outputDir,
file: mainFile, format: 'esm',
format: 'cjs', sourcemap: true,
sourcemap: true, },
exports: 'named',
},
{
file: moduleFile,
format: 'esm',
sourcemap: true,
exports: 'named',
},
],
plugins: [ plugins: [
replace({ replace({
__ENV__: JSON.stringify(processEnv), __ENV__: JSON.stringify(processEnv),
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version), __REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
__INJECT_STYLES__: injectCSS,
preventAssignment: true, preventAssignment: true,
}), }),
bundleSize(),
postcss({ postcss({
minimize: isProd, minimize: isProd,
inject: injectCSS, inject: false,
}), }),
svgr(), svgr(),
resolve(), resolve(),
@@ -60,6 +73,7 @@ export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injec
exclude: 'node_modules/**', exclude: 'node_modules/**',
babelHelpers: 'runtime', babelHelpers: 'runtime',
}), }),
visualizer(),
], ],
}); });
@@ -67,8 +81,7 @@ export default isProd && !isTesting
? [ ? [
baseConfig(), baseConfig(),
baseConfig({ baseConfig({
mainFile: 'dist/nocss/ReactFlow-nocss.js', outputDir: 'dist/nocss',
moduleFile: 'dist/nocss/ReactFlow-nocss.esm.js',
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;
+43 -29
View File
@@ -1,7 +1,8 @@
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react'; import React, { memo, useCallback, FC, useEffect, useState } from 'react';
import cc from 'classcat'; import 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 '../../../assets/icons/plus.svg';
import MinusIcon from '../../../assets/icons/minus.svg'; import MinusIcon from '../../../assets/icons/minus.svg';
@@ -9,21 +10,7 @@ import FitviewIcon from '../../../assets/icons/fitview.svg';
import LockIcon from '../../../assets/icons/lock.svg'; import LockIcon from '../../../assets/icons/lock.svg';
import UnlockIcon from '../../../assets/icons/unlock.svg'; import UnlockIcon from '../../../assets/icons/unlock.svg';
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 } 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 = ({ 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';
+33
View File
@@ -0,0 +1,33 @@
import React from 'react';
import cc from 'classcat';
import { AttributionPosition, ProOptions } from '../../types';
type AttributionProps = {
proOptions?: ProOptions;
position?: AttributionPosition;
};
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if (
(proOptions?.account === 'paid-starter' || proOptions?.account === 'paid-enterprise') &&
proOptions?.hideAttribution
) {
return null;
}
const positionClasses = `${position}`.split('-');
return (
<div
className={cc(['react-flow__attribution', ...positionClasses])}
data-message="Please only hide this attribution when you have a pro account: https://pro.reactflow.dev/plans"
>
<a href="https://reactflow.dev" target="_blank" rel="noopener noreferrer">
React Flow
</a>
</div>
);
}
export default Attribution;
+43 -50
View File
@@ -1,38 +1,39 @@
import React, { useEffect, useState, CSSProperties } from 'react'; import React, { useRef, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge'; import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { import {
ElementId,
Node,
Transform,
HandleElement, HandleElement,
Position,
ConnectionLineType, ConnectionLineType,
ConnectionLineComponent, ConnectionLineComponent,
HandleType, HandleType,
Node,
ReactFlowState,
Position,
} from '../../types'; } from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
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]; const getSourceHandle = (handleId: string | null, sourceNode: Node, connectionHandleType: HandleType) => {
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound = sourceNode.handleBounds?.[connectionHandleType] || sourceNode.handleBounds?.[handleTypeInverted];
return handleId ? handleBound?.find((d: HandleElement) => d.id === handleId) : handleBound?.[0];
}; };
export default ({ export default ({
@@ -43,29 +44,29 @@ 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 sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
setSourceNode(nextSourceNode);
}, []);
if (!sourceNode || !isConnectable) { if (
!sourceNode.current ||
!sourceNode.current ||
!isConnectable ||
!sourceNode.current.handleBounds?.[connectionHandleType]
) {
return null; return null;
} }
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType); const sourceHandle = getSourceHandle(handleId, sourceNode.current, connectionHandleType);
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (sourceNode.current?.width ?? 0) / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height; const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.current?.height ?? 0;
const sourceX = sourceNode.__rf.position.x + sourceHandleX; const sourceX = (sourceNode.current.positionAbsolute?.x || 0) + sourceHandleX;
const sourceY = sourceNode.__rf.position.y + sourceHandleY; const sourceY = (sourceNode.current.positionAbsolute?.y || 0) + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2]; const targetX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2]; const targetY = (connectionPositionY - transform[1]) / transform[2];
@@ -85,7 +86,7 @@ export default ({
targetPosition={targetPosition} targetPosition={targetPosition}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode} sourceNode={sourceNode.current as Node}
sourceHandle={sourceHandle} sourceHandle={sourceHandle}
/> />
</g> </g>
@@ -94,34 +95,26 @@ export default ({
let dAttr: string = ''; let dAttr: string = '';
const pathParams = {
sourceX,
sourceY,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,
};
if (connectionLineType === ConnectionLineType.Bezier) { if (connectionLineType === ConnectionLineType.Bezier) {
dAttr = getBezierPath({ dAttr = getBezierPath(pathParams);
sourceX,
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}
</>
);
};
+111 -98
View File
@@ -1,98 +1,111 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import EdgeText from './EdgeText'; import BaseEdge from './BaseEdge';
import { getCenter } from './utils';
import { getMarkerEnd, getCenter } from './utils';
import { EdgeProps, Position } from '../../types'; export interface GetBezierPathParams {
sourceX: number;
interface GetBezierPathParams { sourceY: number;
sourceX: number; sourcePosition?: Position;
sourceY: number; targetX: number;
sourcePosition?: Position; targetY: number;
targetX: number; targetPosition?: Position;
targetY: number; curvature?: number;
targetPosition?: Position; centerX?: number;
centerX?: number; centerY?: number;
centerY?: number; }
}
export function getBezierPath({
export function getBezierPath({ sourceX,
sourceX, sourceY,
sourceY, sourcePosition = Position.Bottom,
sourcePosition = Position.Bottom, targetX,
targetX, targetY,
targetY, targetPosition = Position.Top,
targetPosition = Position.Top, curvature = 0.25,
centerX, centerX,
centerY, centerY,
}: GetBezierPathParams): string { }: GetBezierPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY }); const leftAndRight = [Position.Left, Position.Right];
const leftAndRight = [Position.Left, Position.Right]; const hasCurvature = curvature > 0;
let cX,
const cX = typeof centerX !== 'undefined' ? centerX : _centerX; cY = 0;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { cX = typeof centerX !== 'undefined' ? centerX : _centerX;
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`; const distanceX = targetX - sourceX;
} else if (leftAndRight.includes(targetPosition)) { const absDistanceX = Math.abs(Math.min(0, distanceX));
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`; const amtX = (Math.sqrt(absDistanceX) / 2) * (50 * curvature);
} else if (leftAndRight.includes(sourcePosition)) {
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`; const hx1 = hasCurvature && distanceX < 0 ? sourceX + amtX : cX;
} const hx2 = hasCurvature && distanceX < 0 ? targetX - amtX : cX;
return path; return `M${sourceX},${sourceY} C${hx1},${sourceY} ${hx2},${targetY}, ${targetX},${targetY}`;
} } else if (leftAndRight.includes(targetPosition)) {
return `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
export default memo( } else if (leftAndRight.includes(sourcePosition)) {
({ return `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
sourceX, }
sourceY,
targetX, cY = typeof centerY !== 'undefined' ? centerY : _centerY;
targetY, const distanceY = targetY - sourceY;
sourcePosition = Position.Bottom,
targetPosition = Position.Top, const absDistanceY = Math.abs(Math.min(0, distanceY));
label, const amtY = (Math.sqrt(absDistanceY) / 2) * (50 * curvature);
labelStyle,
labelShowBg, const hy1 = hasCurvature && distanceY < 0 ? sourceY + amtY : cY;
labelBgStyle, const hy2 = hasCurvature && distanceY < 0 ? targetY - amtY : cY;
labelBgPadding,
labelBgBorderRadius, return `M${sourceX},${sourceY} C${sourceX},${hy1} ${targetX},${hy2} ${targetX},${targetY}`;
style, }
arrowHeadType,
markerEndId, export default memo(
}: EdgeProps) => { ({
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); sourceX,
const path = getBezierPath({ sourceY,
sourceX, targetX,
sourceY, targetY,
sourcePosition, sourcePosition = Position.Bottom,
targetX, targetPosition = Position.Top,
targetY, label,
targetPosition, labelStyle,
}); labelShowBg,
labelBgStyle,
const text = label ? ( labelBgPadding,
<EdgeText labelBgBorderRadius,
x={centerX} style,
y={centerY} markerEnd,
label={label} markerStart,
labelStyle={labelStyle} curvature,
labelShowBg={labelShowBg} }: EdgeProps) => {
labelBgStyle={labelBgStyle} const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
labelBgPadding={labelBgPadding} const path = getBezierPath({
labelBgBorderRadius={labelBgBorderRadius} sourceX,
/> sourceY,
) : null; sourcePosition,
targetX,
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); targetY,
targetPosition,
return ( curvature,
<> });
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
{text} 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}
/>
);
}
);
+12
View File
@@ -0,0 +1,12 @@
import React, { memo } from 'react';
import BezierEdge, { getBezierPath, GetBezierPathParams } from './BezierEdge';
import { EdgeProps } from '../../types';
export function getSimpleBezierPath(props: GetBezierPathParams): string {
return getBezierPath({ ...props, curvature: 0 });
}
export default memo((props: EdgeProps) => {
return <BezierEdge {...props} curvature={0} />;
});
+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 {
+36 -21
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);
@@ -146,21 +150,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement) ? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement)
: 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,
isSourceHandle ? 'target' : 'source', isSourceHandle ? 'target' : 'source',
_onEdgeUpdate _onEdgeUpdate,
store.getState
); );
}, },
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge] [id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
); );
const onEdgeUpdaterSourceMouseDown = useCallback( const onEdgeUpdaterSourceMouseDown = useCallback(
@@ -179,8 +192,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;
} }
@@ -208,16 +223,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;
+1 -1
View File
@@ -5,7 +5,7 @@ import { NodeProps, Position } from '../../types';
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => ( const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
<> <>
{data.label} {data?.label}
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} /> <Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
</> </>
); );
+1 -1
View File
@@ -6,7 +6,7 @@ import { NodeProps, Position } from '../../types';
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => ( const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
<> <>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} /> <Handle type="target" position={targetPosition} isConnectable={isConnectable} />
{data.label} {data?.label}
</> </>
); );
@@ -0,0 +1,25 @@
import { MouseEvent, useCallback } from 'react';
import { GetState } from 'zustand';
import { ReactFlowState, Node } from '../../types';
function useMemoizedMouseHandler(
id: string,
dragging: boolean,
getState: GetState<ReactFlowState>,
handler?: (event: MouseEvent, node: Node) => void
) {
const memoizedHandler = useCallback(
(event: MouseEvent) => {
if (typeof handler !== 'undefined' && !dragging) {
const node = getState().nodeInternals.get(id)!;
handler(event, { ...node });
}
},
[handler, dragging, id]
);
return memoizedHandler;
}
export default useMemoizedMouseHandler;
+14 -16
View File
@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
export const getHandleBoundsByHandleType = ( export const getHandleBoundsByHandleType = (
selector: string, selector: string,
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
parentBounds: ClientRect | DOMRect, parentBounds: DOMRect,
k: number k: number
): HandleElement[] | null => { ): HandleElement[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(selector);
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
const handlesArray = Array.from(handles) as HTMLDivElement[]; const handlesArray = Array.from(handles) as HTMLDivElement[];
return handlesArray.map( return handlesArray.map((handle): HandleElement => {
(handle): HandleElement => { const bounds = handle.getBoundingClientRect();
const bounds = handle.getBoundingClientRect(); const dimensions = getDimensions(handle);
const dimensions = getDimensions(handle); const handleId = handle.getAttribute('data-handleid');
const handleId = handle.getAttribute('data-handleid'); const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
return { return {
id: handleId, id: handleId,
position: handlePosition, position: handlePosition,
x: (bounds.left - parentBounds.left) / k, x: (bounds.left - parentBounds.left) / k,
y: (bounds.top - parentBounds.top) / k, y: (bounds.top - parentBounds.top) / k,
...dimensions, ...dimensions,
}; };
} });
);
}; };
+120 -124
View File
@@ -1,22 +1,21 @@
import React, { import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
useEffect,
useLayoutEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
} from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreActions } from '../../store/hooks'; import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext'; import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps } from '../../types'; import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
export default (NodeComponent: ComponentType<NodeComponentProps>) => { const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions,
});
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({ const NodeWrapper = ({
id, id,
type, type,
@@ -42,192 +41,188 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
selectNodesOnDrag, selectNodesOnDrag,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
isHidden, hidden,
isInitialized,
snapToGrid, snapToGrid,
snapGrid, snapGrid,
isDragging, dragging,
resizeObserver, resizeObserver,
dragHandle, dragHandle,
zIndex,
isParent,
noPanClassName,
noDragClassName,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const store = useStoreApi();
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); selector,
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); shallow
);
const nodeElement = useRef<HTMLDivElement>(null); const nodeElement = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]); const prevTargetPosition = useRef(targetPosition);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]); const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const nodeStyle: CSSProperties = useMemo( const nodeStyle: CSSProperties = useMemo(
() => ({ () => ({
zIndex: selected ? 10 : 3, zIndex,
transform: `translate(${xPos}px,${yPos}px)`, transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents: pointerEvents: hasPointerEvents ? 'all' : 'none',
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
// prevents jumping of nodes on start
opacity: isInitialized ? 1 : 0,
...style, ...style,
}), }),
[ [zIndex, xPos, yPos, hasPointerEvents, style]
selected,
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
]
); );
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
return;
}
return (event: MouseEvent) => onMouseEnter(event, node); const grid = useMemo(
}, [onMouseEnter, isDragging, node]); () => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
);
const onMouseMoveHandler = useMemo(() => { const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
if (!onMouseMove || isDragging) { const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
return; const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
} const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, isDragging, node]);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || isDragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, isDragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onSelectNodeHandler = useCallback( const onSelectNodeHandler = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
if (!isDraggable) { if (!isDraggable) {
if (isSelectable) { if (isSelectable) {
unsetNodesSelection(); store.setState({ nodesSelectionActive: false });
if (!selected) { if (!selected) {
addSelectedElements(node); addSelectedNodes([id]);
} }
} }
onClick?.(event, node); if (onClick) {
const node = store.getState().nodeInternals.get(id)!;
onClick(event, { ...node });
}
} }
}, },
[isSelectable, selected, isDraggable, onClick, node] [isSelectable, selected, isDraggable, onClick, id]
); );
const onDragStart = useCallback( const onDragStart = useCallback(
(event: DraggableEvent) => { (event: DraggableEvent) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) { if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection(); store.setState({ nodesSelectionActive: false });
if (!selected) { if (!selected) {
addSelectedElements(node); addSelectedNodes([id]);
} }
} else if (!selectNodesOnDrag && !selected && isSelectable) { } else if (!selectNodesOnDrag && !selected && isSelectable) {
unsetNodesSelection(); unselectNodesAndEdges();
addSelectedElements([]); store.setState({ nodesSelectionActive: false });
}
if (onNodeDragStart) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStart(event as MouseEvent, { ...node });
} }
}, },
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] [id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
); );
const onDrag = useCallback( const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => { (event: DraggableEvent, draggableData: DraggableData) => {
if (onNodeDrag) { updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
onNodeDrag(event as MouseEvent, node);
}
updateNodePosDiff({ if (onNodeDrag) {
id, const node = store.getState().nodeInternals.get(id)!;
diff: { onNodeDrag(event as MouseEvent, {
x: draggableData.deltaX, ...node,
y: draggableData.deltaY, dragging: true,
}, position: {
isDragging: true, x: node.position.x + draggableData.deltaX,
}); y: node.position.y + draggableData.deltaY,
},
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
},
});
}
}, },
[id, node, onNodeDrag] [id, onNodeDrag]
); );
const onDragStop = useCallback( const onDragStop = useCallback(
(event: DraggableEvent) => { (event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node. // onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here // Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) { let node;
if (onClick || onNodeDragStop) {
node = store.getState().nodeInternals.get(id)!;
}
if (!dragging) {
if (isSelectable && !selectNodesOnDrag && !selected) { if (isSelectable && !selectNodesOnDrag && !selected) {
addSelectedElements(node); addSelectedNodes([id]);
} }
onClick?.(event as MouseEvent, node); if (onClick && node) {
onClick(event as MouseEvent, { ...node });
}
return; return;
} }
updateNodePosDiff({ updateNodePosition({
id: node.id, id,
isDragging: false, dragging: false,
}); });
onNodeDragStop?.(event as MouseEvent, node); if (onNodeDragStop && node) {
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
}
}, },
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected] [id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
); );
const onNodeDoubleClickHandler = useCallback(
(event: MouseEvent) => {
onNodeDoubleClick?.(event, node);
},
[node, onNodeDoubleClick]
);
useLayoutEffect(() => {
if (nodeElement.current && !isHidden) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition]);
useEffect(() => { useEffect(() => {
if (nodeElement.current) { if (nodeElement.current && !hidden) {
const currNode = nodeElement.current; const currNode = nodeElement.current;
resizeObserver?.observe(currNode); resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode); return () => resizeObserver?.unobserve(currNode);
} }
}, []); }, [hidden]);
if (isHidden) { useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== type;
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, type, sourcePosition, targetPosition]);
if (hidden) {
return null; return null;
} }
const nodeClasses = cc([ const nodeClasses = cc([
'react-flow__node', 'react-flow__node',
`react-flow__node-${type}`, `react-flow__node-${type}`,
noPanClassName,
className, className,
{ {
selected, selected,
selectable: isSelectable, selectable: isSelectable,
parent: isParent,
}, },
]); ]);
@@ -238,7 +233,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
onStop={onDragStop} onStop={onDragStop}
scale={scale} scale={scale}
disabled={!isDraggable} disabled={!isDraggable}
cancel=".nodrag" cancel={`.${noDragClassName}`}
nodeRef={nodeElement} nodeRef={nodeElement}
grid={grid} grid={grid}
enableUserSelectHack={false} enableUserSelectHack={false}
@@ -267,8 +262,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
isConnectable={isConnectable} isConnectable={isConnectable}
sourcePosition={sourcePosition} sourcePosition={sourcePosition}
targetPosition={targetPosition} targetPosition={targetPosition}
isDragging={isDragging} dragging={dragging}
dragHandle={dragHandle} dragHandle={dragHandle}
zIndex={zIndex}
/> />
</Provider> </Provider>
</div> </div>
+46 -52
View File
@@ -1,57 +1,53 @@
/** /**
* The nodes selection rectangle gets displayed when a user * The nodes selection rectangle gets displayed when a user
* made a selectio with on or several nodes * made a selection with on or several nodes
*/ */
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react'; import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
import ReactDraggable, { DraggableData } from 'react-draggable'; import { DraggableCore, DraggableData } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStoreState, useStoreActions } from '../../store/hooks'; import { useStore } from '../../store';
import { isNode } from '../../utils/graph'; import { Node, ReactFlowState } from '../../types';
import { Node } from '../../types'; import { getRectOfNodes } from '../../utils/graph';
export interface NodesSelectionProps { export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
} }
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals)
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
updateNodePosition: s.updateNodePosition,
});
export default ({ function NodesSelection({
onSelectionDragStart, onSelectionDragStart,
onSelectionDrag, onSelectionDrag,
onSelectionDragStop, onSelectionDragStop,
onSelectionContextMenu, onSelectionContextMenu,
}: NodesSelectionProps) => { noPanClassName,
const [tX, tY, tScale] = useStoreState((state) => state.transform); }: NodesSelectionProps) {
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox); const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
const selectionActive = useStoreState((state) => state.selectionActive); selector,
const selectedElements = useStoreState((state) => state.selectedElements); shallow
const snapToGrid = useStoreState((state) => state.snapToGrid); );
const snapGrid = useStoreState((state) => state.snapGrid); const [tX, tY, tScale] = transform;
const nodes = useStoreState((state) => state.nodes);
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
const nodeRef = useRef(null); const nodeRef = useRef(null);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]); const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const selectedNodes = useMemo(
() =>
selectedElements
? selectedElements.filter(isNode).map((selectedNode) => {
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
return {
...matchingNode,
position: matchingNode?.__rf.position,
} as Node;
})
: [],
[selectedElements, nodes]
);
const style = useMemo( const style = useMemo(
() => ({ () => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`, transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
@@ -59,6 +55,8 @@ export default ({
[tX, tY, tScale] [tX, tY, tScale]
); );
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo( const innerStyle = useMemo(
() => ({ () => ({
width: selectedNodesBbox.width, width: selectedNodesBbox.width,
@@ -78,25 +76,23 @@ export default ({
const onDrag = useCallback( const onDrag = useCallback(
(event: MouseEvent, data: DraggableData) => { (event: MouseEvent, data: DraggableData) => {
if (onSelectionDrag) { updateNodePosition({
onSelectionDrag(event, selectedNodes);
}
updateNodePosDiff({
diff: { diff: {
x: data.deltaX, x: data.deltaX,
y: data.deltaY, y: data.deltaY,
}, },
isDragging: true, dragging: true,
}); });
onSelectionDrag?.(event, selectedNodes);
}, },
[onSelectionDrag, selectedNodes, updateNodePosDiff] [onSelectionDrag, selectedNodes, updateNodePosition]
); );
const onStop = useCallback( const onStop = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
updateNodePosDiff({ updateNodePosition({
isDragging: false, dragging: false,
}); });
onSelectionDragStop?.(event, selectedNodes); onSelectionDragStop?.(event, selectedNodes);
@@ -106,22 +102,18 @@ export default ({
const onContextMenu = useCallback( const onContextMenu = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => {
const selectedNodes = selectedElements
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
: [];
onSelectionContextMenu?.(event, selectedNodes); onSelectionContextMenu?.(event, selectedNodes);
}, },
[onSelectionContextMenu] [onSelectionContextMenu, selectedNodes]
); );
if (!selectedElements || selectionActive) { if (!selectedNodes?.length || userSelectionActive) {
return null; return null;
} }
return ( return (
<div className="react-flow__nodesselection" style={style}> <div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<ReactDraggable <DraggableCore
scale={tScale} scale={tScale}
grid={grid} grid={grid}
onStart={(event) => onStart(event as MouseEvent)} onStart={(event) => onStart(event as MouseEvent)}
@@ -136,7 +128,9 @@ export default ({
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
style={innerStyle} style={innerStyle}
/> />
</ReactDraggable> </DraggableCore>
</div> </div>
); );
}; }
export default memo(NodesSelection);
+28 -10
View File
@@ -1,20 +1,38 @@
import { useEffect } from 'react'; import { memo, useEffect } from 'react';
import shallow from 'zustand/shallow';
import { Elements } from '../../types'; import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
import { useStoreState } from '../../store/hooks'; import { useStore } from '../../store';
interface SelectionListenerProps { interface SelectionListenerProps {
onSelectionChange: (elements: Elements | null) => void; onSelectionChange: OnSelectionChangeFunc;
} }
// This is a helper component for calling the onSelectionChange listener const selector = (s: ReactFlowState) => ({
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
selectedEdges: s.edges.filter((e) => e.selected),
});
export default ({ onSelectionChange }: SelectionListenerProps) => { const areEqual = (objA: any, objB: any) => {
const selectedElements = useStoreState((s) => s.selectedElements); const selectedNodeIdsA = objA.selectedNodes.map((n: Node) => n.id);
const selectedNodeIdsB = objB.selectedNodes.map((n: Node) => n.id);
const selectedEdgeIdsA = objA.selectedEdges.map((e: Edge) => e.id);
const selectedEdgeIdsB = objB.selectedEdges.map((e: Edge) => e.id);
return shallow(selectedNodeIdsA, selectedNodeIdsB) && shallow(selectedEdgeIdsA, selectedEdgeIdsB);
};
// This is just a helper component for calling the onSelectionChange listener.
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
useEffect(() => { useEffect(() => {
onSelectionChange(selectedElements); onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
}, [selectedElements]); }, [selectedNodes, selectedEdges]);
return null; return null;
}; }
export default memo(SelectionListener);
+161
View File
@@ -0,0 +1,161 @@
import { useEffect } from 'react';
import { SetState } from 'zustand';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import {
Node,
Edge,
ReactFlowState,
OnConnect,
OnConnectStart,
OnConnectStop,
OnConnectEnd,
CoordinateExtent,
OnNodesChange,
OnEdgesChange,
ConnectionMode,
SnapGrid,
DefaultEdgeOptions,
FitViewOptions,
OnNodesDelete,
OnEdgesDelete
} from '../../types';
interface StoreUpdaterProps {
nodes?: Node[];
edges?: Edge[];
defaultNodes?: Node[];
defaultEdges?: Edge[];
onConnect?: OnConnect;
onConnectStart?: OnConnectStart;
onConnectStop?: OnConnectStop;
onConnectEnd?: OnConnectEnd;
nodesDraggable?: boolean;
nodesConnectable?: boolean;
minZoom?: number;
maxZoom?: number;
nodeExtent?: CoordinateExtent;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
elementsSelectable?: boolean;
connectionMode?: ConnectionMode;
snapToGrid?: boolean;
snapGrid?: SnapGrid;
translateExtent?: CoordinateExtent;
connectOnClick: boolean;
defaultEdgeOptions?: DefaultEdgeOptions;
fitView?: boolean;
fitViewOptions?: FitViewOptions;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
}
const selector = (s: ReactFlowState) => ({
setNodes: s.setNodes,
setEdges: s.setEdges,
setDefaultNodesAndEdges: s.setDefaultNodesAndEdges,
setMinZoom: s.setMinZoom,
setMaxZoom: s.setMaxZoom,
setTranslateExtent: s.setTranslateExtent,
setNodeExtent: s.setNodeExtent,
reset: s.reset,
});
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
useEffect(() => {
if (typeof value !== 'undefined') {
setStoreState(value);
}
}, [value]);
}
function useDirectStoreUpdater(key: keyof ReactFlowState, value: any, setState: SetState<ReactFlowState>) {
useEffect(() => {
if (typeof value !== 'undefined') {
// @ts-ignore
setState({ [key]: value });
}
}, [value]);
}
const StoreUpdater = ({
nodes,
edges,
defaultNodes,
defaultEdges,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
nodesDraggable,
nodesConnectable,
minZoom,
maxZoom,
nodeExtent,
onNodesChange,
onEdgesChange,
elementsSelectable,
connectionMode,
snapGrid,
snapToGrid,
translateExtent,
connectOnClick,
defaultEdgeOptions,
fitView,
fitViewOptions,
onNodesDelete,
onEdgesDelete,
}: StoreUpdaterProps) => {
const {
setNodes,
setEdges,
setDefaultNodesAndEdges,
setMinZoom,
setMaxZoom,
setTranslateExtent,
setNodeExtent,
reset,
} = useStore(selector, shallow);
const store = useStoreApi();
useEffect(() => {
setDefaultNodesAndEdges(defaultNodes, defaultEdges);
return () => {
reset();
};
}, []);
useDirectStoreUpdater('defaultEdgeOptions', defaultEdgeOptions, store.setState);
useDirectStoreUpdater('connectionMode', connectionMode, store.setState);
useDirectStoreUpdater('onConnect', onConnect, store.setState);
useDirectStoreUpdater('onConnectStart', onConnectStart, store.setState);
useDirectStoreUpdater('onConnectStop', onConnectStop, store.setState);
useDirectStoreUpdater('onConnectEnd', onConnectEnd, store.setState);
useDirectStoreUpdater('nodesDraggable', nodesDraggable, store.setState);
useDirectStoreUpdater('nodesConnectable', nodesConnectable, store.setState);
useDirectStoreUpdater('elementsSelectable', elementsSelectable, store.setState);
useDirectStoreUpdater('snapToGrid', snapToGrid, store.setState);
useDirectStoreUpdater('snapGrid', snapGrid, store.setState);
useDirectStoreUpdater('onNodesChange', onNodesChange, store.setState);
useDirectStoreUpdater('onEdgesChange', onEdgesChange, store.setState);
useDirectStoreUpdater('connectOnClick', connectOnClick, store.setState);
useDirectStoreUpdater('fitViewOnInit', fitView, store.setState);
useDirectStoreUpdater('fitViewOnInitOptions', fitViewOptions, store.setState);
useDirectStoreUpdater('onNodesDelete', onNodesDelete, store.setState);
useDirectStoreUpdater('onEdgesDelete', onEdgesDelete, store.setState);
useStoreUpdater<Node[]>(nodes, setNodes);
useStoreUpdater<Edge[]>(edges, setEdges);
useStoreUpdater<Node[]>(defaultNodes, setNodes);
useStoreUpdater<Edge[]>(defaultEdges, setEdges);
useStoreUpdater<number>(minZoom, setMinZoom);
useStoreUpdater<number>(maxZoom, setMaxZoom);
useStoreUpdater<CoordinateExtent>(translateExtent, setTranslateExtent);
useStoreUpdater<CoordinateExtent>(nodeExtent, setNodeExtent);
return null;
};
export default StoreUpdater;
+125 -65
View File
@@ -2,100 +2,160 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/ */
import React, { memo } from 'react'; import React, { memo, useState, useRef, useCallback } from 'react';
import shallow from 'zustand/shallow';
import { useStoreActions, useStoreState } from '../../store/hooks'; import { useStore, useStoreApi } from '../../store';
import { XYPosition } from '../../types'; import { getSelectionChanges } from '../../utils/changes';
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
type SelectionRect = Rect & {
startX: number;
startY: number;
draw: boolean;
};
type UserSelectionProps = { type UserSelectionProps = {
selectionKeyPressed: boolean; selectionKeyPressed: boolean;
}; };
function getMousePosition(event: React.MouseEvent): XYPosition | void { function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
const reactFlowNode = (event.target as Element).closest('.react-flow');
if (!reactFlowNode) {
return;
}
const containerBounds = reactFlowNode.getBoundingClientRect();
return { return {
x: event.clientX - containerBounds.left, x: event.clientX - containerBounds.left,
y: event.clientY - containerBounds.top, y: event.clientY - containerBounds.top,
}; };
} }
const SelectionRect = () => { const selector = (s: ReactFlowState) => ({
const userSelectionRect = useStoreState((state) => state.userSelectionRect); userSelectionActive: s.userSelectionActive,
elementsSelectable: s.elementsSelectable,
});
if (!userSelectionRect.draw) { const initialRect: SelectionRect = {
return null; startX: 0,
} startY: 0,
x: 0,
return ( y: 0,
<div width: 0,
className="react-flow__selection" height: 0,
style={{ draw: false,
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
);
}; };
export default memo(({ selectionKeyPressed }: UserSelectionProps) => { export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
const selectionActive = useStoreState((state) => state.selectionActive); const store = useStoreApi();
const elementsSelectable = useStoreState((state) => state.elementsSelectable); const prevSelectedNodesCount = useRef<number>(0);
const prevSelectedEdgesCount = useRef<number>(0);
const containerBounds = useRef<DOMRect>();
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
const { userSelectionActive, elementsSelectable } = useStore(selector, shallow);
const setUserSelection = useStoreActions((actions) => actions.setUserSelection); const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection); const resetUserSelection = useCallback(() => {
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); setUserSelectionRect(initialRect);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
store.setState({ userSelectionActive: false });
prevSelectedNodesCount.current = 0;
prevSelectedEdgesCount.current = 0;
}, []);
const onMouseDown = useCallback((event: React.MouseEvent): void => {
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
containerBounds.current = reactFlowNode.getBoundingClientRect();
const mousePos = getMousePosition(event, containerBounds.current!);
setUserSelectionRect({
width: 0,
height: 0,
startX: mousePos.x,
startY: mousePos.y,
x: mousePos.x,
y: mousePos.y,
draw: true,
});
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
}, []);
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
return;
}
const mousePos = getMousePosition(event, containerBounds.current!);
const startX = userSelectionRect.startX ?? 0;
const startY = userSelectionRect.startY ?? 0;
const nextUserSelectRect = {
...userSelectionRect,
x: mousePos.x < startX ? mousePos.x : startX,
y: mousePos.y < startY ? mousePos.y : startY,
width: Math.abs(mousePos.x - startX),
height: Math.abs(mousePos.y - startY),
};
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
const selectedNodeIds = selectedNodes.map((n) => n.id);
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
prevSelectedNodesCount.current = selectedNodeIds.length;
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
if (changes.length) {
onNodesChange?.(changes);
}
}
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
prevSelectedEdgesCount.current = selectedEdgeIds.length;
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
if (changes.length) {
onEdgesChange?.(changes);
}
}
setUserSelectionRect(nextUserSelectRect);
};
const onMouseUp = useCallback(() => {
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
resetUserSelection();
}, []);
const onMouseLeave = useCallback(() => {
store.setState({ nodesSelectionActive: false });
resetUserSelection();
}, []);
if (!elementsSelectable || !renderUserSelectionPane) { if (!elementsSelectable || !renderUserSelectionPane) {
return null; return null;
} }
const onMouseDown = (event: React.MouseEvent): void => {
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
setUserSelection(mousePos);
};
const onMouseMove = (event: React.MouseEvent): void => {
if (!selectionKeyPressed || !selectionActive) {
return;
}
const mousePos = getMousePosition(event);
if (!mousePos) {
return;
}
updateUserSelection(mousePos);
};
const onMouseUp = () => unsetUserSelection();
const onMouseLeave = () => {
unsetUserSelection();
unsetNodesSelection();
};
return ( return (
<div <div
className="react-flow__selectionpane" className="react-flow__selectionpane react-flow__container"
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onMouseMove={onMouseMove} onMouseMove={onMouseMove}
onMouseUp={onMouseUp} onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
<SelectionRect /> {userSelectionRect.draw && (
<div
className="react-flow__selection react-flow__container"
style={{
width: userSelectionRect.width,
height: userSelectionRect.height,
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
}}
/>
)}
</div> </div>
); );
}); });

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