1
.babelrc
1
.babelrc
@@ -6,6 +6,7 @@
|
||||
"modules": false
|
||||
}
|
||||
],
|
||||
"@babel/preset-typescript",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
|
||||
7
.postcssrc.json
Normal file
7
.postcssrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
{
|
||||
"plugins": {
|
||||
"autoprefixer" : {},
|
||||
"postcss-nested": {}
|
||||
}
|
||||
}
|
||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
},
|
||||
"hooks": {
|
||||
"after:bump": "npm run build",
|
||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
||||
"after:release": "echo Successfully released ${name} v${version}."
|
||||
}
|
||||
}
|
||||
|
||||
24
.travis.yml
24
.travis.yml
@@ -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
|
||||
@@ -27,5 +27,5 @@ Please use a meaningful commit message and add a little description of your chan
|
||||
|
||||
1. Install dependencies `npm install` and example dependencies `cd example && npm install`
|
||||
2. Run library start task `npm start` and example start task `cd example && npm start`
|
||||
3. No you can test your changes with the exisiting examples. Feel free to add a new one if it's needed for your changes
|
||||
3. Now you can test your changes with the existing examples. Feel free to add a new one if it's needed for your changes
|
||||
4. Run tests `npm test` and add new ones if you are introducing a new feature
|
||||
|
||||
104
README.md
104
README.md
@@ -1,52 +1,62 @@
|
||||
[](https://reactflow.dev)
|
||||

|
||||

|
||||
|
||||
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 👈
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
We are currently working on the next version. You can find the installation instructions and breaking changes in [this PR](https://github.com/wbkd/react-flow/pull/1555). Your feedback is highly appreciated :)
|
||||
### Wire Your Ideas With React Flow!
|
||||
A highly customizable React component for building interactive graphs and node-based editors.
|
||||
|
||||
- **Website:** https://reactflow.dev
|
||||
- **Documentation:** https://reactflow.dev/docs
|
||||
- **Examples:** https://reactflow.dev/examples
|
||||
- **Website/Docs Repository:** https://github.com/wbkd/react-flow-docs
|
||||
- **Community:** https://discord.gg/Bqt6xrs
|
||||
- **Sponsorship:** https://github.com/sponsors/wbkd
|
||||
[🚀 Getting Started](https://reactflow.dev/docs/getting-started/installation) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/Bqt6xrs) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
|
||||
|
||||
</div>
|
||||
|
||||
React Flow was initially created for [datablocks](https://datablocks.pro). A node-based editor for transforming, analyzing and visualizing data.
|
||||
----
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Easy to use:** Seamless zooming and panning, single- and multi selection of graph elements and keyboard shortcuts are supported out of the box
|
||||
- **Customizable:** Different [node](https://reactflow.dev/docs/api/nodes/node-types) and [edge types](https://reactflow.dev/docs/api/edges/edge-types) and support for custom nodes with multiple handles and custom edges
|
||||
- **Fast rendering:** Only nodes that have changed are re-rendered and only those in the viewport are displayed
|
||||
- **Hooks and Utils:** [Hooks](https://reactflow.dev/docs/api/hooks/use-react-flow) for handling nodes, edges and the viewport and graph [helper functions](https://reactflow.dev/docs/api/graph-util-functions)
|
||||
- **Plugin Components:** [Background](https://reactflow.dev/docs/api/plugin-components/background), [MiniMap](https://reactflow.dev/docs/api/plugin-components/minimap) and [Controls](https://reactflow.dev/docs/api/plugin-components/controls)
|
||||
- **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
The easiest way to get the latest version of React Flow is to install it via npm:
|
||||
|
||||
```bash
|
||||
npm install react-flow-renderer
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
This is a very basic example of how to use React Flow. You can find more advanced examples on the [website](https://reactflow.dev/examples).
|
||||
This is only a very basic usage example of React Flow. To see everything that is possible with the library, please refer to the [website](https://reactflow.dev) for [guides](https://reactflow.dev/docs/guides/custom-nodes), [examples](https://reactflow.dev/docs/examples/overview) and [API reference](https://reactflow.dev/docs/api/react-flow-props).
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import ReactFlow from 'react-flow-renderer';
|
||||
```jsx
|
||||
import ReactFlow, { MiniMap, Controls } from 'react-flow-renderer';
|
||||
|
||||
const elements = [
|
||||
{ id: '1', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
// you can also pass a React component as a label
|
||||
{ id: '2', data: { label: <div>Node 2</div> }, position: { x: 100, y: 100 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
];
|
||||
|
||||
const BasicFlow = () => <ReactFlow elements={elements} />;
|
||||
function Flow({ nodes, edges, onNodesChange, onEdgesChange, onConnect }) {
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Community Packages
|
||||
|
||||
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
|
||||
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
|
||||
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
|
||||
|
||||
## Development
|
||||
|
||||
Before you start you need to install the React Flow dependencies via `npm install` and the ones of the examples `cd example && npm install`.
|
||||
|
||||
If you want to contribute or develop custom features the easiest way is to start the dev server:
|
||||
@@ -71,19 +81,33 @@ Testing is done with cypress. You can find the tests in the [`integration/flow`]
|
||||
npm run test
|
||||
```
|
||||
|
||||
## Maintainer
|
||||
## Attribution
|
||||
|
||||
Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
|
||||
Since v10.0.0 the React Flow renderer includes a small attribution that links to the React Flow website. It is only allowed to remove the attribution if you are subscribed to one of the [pro plans](https://pro.reactflow.dev/pricing). A subscription also provides more features such as a private Discord channel with the creators of React Flow or access to the source code of Datablocks (a node-based data editor built with React Flow).
|
||||
|
||||
## Support
|
||||
## Maintainers
|
||||
|
||||
If you need custom support or features for your application we are [happy to hear from you](https://webkid.io/contact).
|
||||
React Flow is developed and maintained by [webkid](https://webkid.io), a web development agency with the focus on data driven applications from Berlin. If you need help or want to talk to us about a collaboration, feel free to contact us:
|
||||
|
||||
* Moritz Klack • [Twitter](https://twitter.com/moklick) • [Github](https://github.com/moklick)
|
||||
* Christopher Möller • [Twitter](https://twitter.com/chrtze) • [Github](https://github.com/chrtze)
|
||||
|
||||
### Thanks!
|
||||
You can also use our [contact form](https://webkid.io/contact/) or join the [React Flow Discord Server](https://discord.gg/Bqt6xrs).
|
||||
|
||||
Special thanks to [Andy Lindemann](https://github.com/AndyLnd) for a lot of helpful contributions!
|
||||
## Community Packages
|
||||
|
||||
---
|
||||
* [useUndoable](https://github.com/Infinium8/useUndoable) - Hook for undo/redo functionality with an explicit React Flow example
|
||||
* [react-flow-smart-edge](https://github.com/tisoap/react-flow-smart-edge) - Custom edge that doesn't intersect with nodes
|
||||
* [Feliz.ReactFlow](https://github.com/tforkmann/Feliz.ReactFlow) - Feliz React Bindings for React Flow
|
||||
|
||||
React Flow is maintained by [webkid](https://webkid.io), a data visualization agency from Berlin. If you need help or want to develop react-based tools or data visualizations, [get in touch](https://webkid.io/contact)!
|
||||
## Credits
|
||||
|
||||
React Flow was initially developed for [datablocks](https://datablocks.pro), a graph-based editor for transforming, analyzing and visualizing data in your browser. Under the hood, React Flow depends on these great libraries:
|
||||
|
||||
* [d3-zoom](https://github.com/d3/d3-zoom) - used for zoom, pan and drag interactions with the graph canvas
|
||||
* [react-draggable](https://github.com/react-grid-layout/react-draggable) - used for making the nodes draggable
|
||||
* [zustand](https://github.com/pmndrs/zustand) - internal state management
|
||||
|
||||
## License
|
||||
|
||||
React Flow is [MIT licensed](https://github.com/wbkd/react-flow/blob/main/LICENSE).
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"viewportWidth": 1280,
|
||||
"viewportHeight": 720,
|
||||
"video": false
|
||||
}
|
||||
"video": false,
|
||||
"pluginsFile": false
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Basic Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('deselects node', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomRight');
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__node:first').should('not.have.class', 'selected');
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('Basic Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('deselects edge', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomRight');
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__edge:first').should('not.have.class', 'selected');
|
||||
});
|
||||
|
||||
@@ -50,9 +50,9 @@ describe('Basic Flow Rendering', () => {
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, force: true })
|
||||
.trigger('mousemove', 800, 75, { which: 1 })
|
||||
.trigger('mouseup', 'bottomRight', { force: true })
|
||||
.trigger('mousedown', 1000, 1, { which: 1, force: true })
|
||||
.trigger('mousemove', 1, 200, { which: 1 })
|
||||
.trigger('mouseup', 1, 200, { force: true })
|
||||
.get('.react-flow__node')
|
||||
.first()
|
||||
.should('have.class', 'selected')
|
||||
@@ -68,9 +68,9 @@ describe('Basic Flow Rendering', () => {
|
||||
cy.get('body')
|
||||
.type('{shift}', { release: false })
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, force: true })
|
||||
.trigger('mousemove', 'bottomRight', { which: 1 })
|
||||
.trigger('mouseup', 'bottomRight', { force: true })
|
||||
.trigger('mousedown', 'topRight', { which: 1, force: true })
|
||||
.trigger('mousemove', 'bottomLeft', { which: 1 })
|
||||
.trigger('mouseup', 'bottomLeft', { force: true })
|
||||
.get('.react-flow__node')
|
||||
.should('have.class', 'selected')
|
||||
.get('.react-flow__nodesselection-rect');
|
||||
@@ -79,7 +79,7 @@ describe('Basic Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('removes selection', () => {
|
||||
cy.get('.react-flow__renderer').click('bottomRight');
|
||||
cy.get('.react-flow__renderer').click('bottomLeft');
|
||||
cy.get('.react-flow__nodesselection-rect').should('not.exist');
|
||||
});
|
||||
|
||||
@@ -108,12 +108,12 @@ describe('Basic Flow Rendering', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 3')
|
||||
.find('.react-flow__handle.source')
|
||||
.trigger('mousedown', { which: 1 });
|
||||
.trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 4')
|
||||
.find('.react-flow__handle.target')
|
||||
.trigger('mousemove')
|
||||
.trigger('mousemove', { force: true })
|
||||
.trigger('mouseup', { force: true });
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 2);
|
||||
@@ -127,29 +127,29 @@ describe('Basic Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('drags the pane', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms the pane', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,30 +8,30 @@ describe('Controls Testing', () => {
|
||||
});
|
||||
|
||||
it('zooms in', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomin')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
it('zooms out', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-zoomout')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
// view is already fitted so we drag the pane to un-fit it
|
||||
it('drags the pane', () => {
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
@@ -41,19 +41,19 @@ describe('Controls Testing', () => {
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('fits view', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__controls-fitview')
|
||||
.click()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,18 +20,13 @@ describe('Empty Flow Rendering', () => {
|
||||
cy.get('body').type('{shift}', { release: true });
|
||||
});
|
||||
|
||||
it('renders an empty mini map', () => {
|
||||
cy.get('.react-flow__minimap');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
});
|
||||
|
||||
it('adds two nodes', () => {
|
||||
cy.contains('add node').click();
|
||||
cy.contains('add node').click();
|
||||
});
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { which: 1 });
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.last()
|
||||
@@ -41,9 +36,4 @@ describe('Empty Flow Rendering', () => {
|
||||
|
||||
cy.get('.react-flow__edge').should('have.length', 1);
|
||||
});
|
||||
|
||||
it('renders mini map with two nodes', () => {
|
||||
cy.get('.react-flow__minimap');
|
||||
cy.get('.react-flow__minimap-node').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isNode, isEdge, getOutgoers, getIncomers, removeElements, addEdge } from '../../../dist/ReactFlow.js';
|
||||
import { isNode, isEdge, getOutgoers, getIncomers, addEdge } from '../../../dist/esm/index.js';
|
||||
|
||||
const nodes = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
@@ -13,8 +13,6 @@ const edges = [
|
||||
{ id: 'e2-3', source: '2', target: '3' },
|
||||
];
|
||||
|
||||
const elements = [...nodes, ...edges];
|
||||
|
||||
describe('Graph Utils Testing', () => {
|
||||
it('tests isNode function', () => {
|
||||
expect(isNode(nodes[0])).to.be.true;
|
||||
@@ -27,41 +25,42 @@ describe('Graph Utils Testing', () => {
|
||||
});
|
||||
|
||||
it('tests getOutgoers function', () => {
|
||||
const outgoers = getOutgoers(nodes[0], elements);
|
||||
const outgoers = getOutgoers(nodes[0], nodes, edges);
|
||||
|
||||
expect(outgoers.length).to.be.equal(2);
|
||||
|
||||
const noOutgoers = getOutgoers(nodes[2], elements);
|
||||
const noOutgoers = getOutgoers(nodes[2], nodes, edges);
|
||||
expect(noOutgoers.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('tests getIncomers function', () => {
|
||||
const incomers = getIncomers(nodes[2], elements);
|
||||
const incomers = getIncomers(nodes[2], nodes, edges);
|
||||
expect(incomers.length).to.be.equal(2);
|
||||
|
||||
const noIncomers = getIncomers(nodes[0], elements);
|
||||
const noIncomers = getIncomers(nodes[0], nodes, edges);
|
||||
expect(noIncomers.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
describe('tests addEdge function', () => {
|
||||
it('adds edge', () => {
|
||||
const newEdge = { source: '1', target: '4' };
|
||||
const nextElements = addEdge(newEdge, elements);
|
||||
const nextEdges = addEdge(newEdge, edges);
|
||||
|
||||
expect(nextElements.length).to.be.equal(elements.length + 1);
|
||||
expect(nextEdges.length).to.be.equal(edges.length + 1);
|
||||
});
|
||||
|
||||
it('tries to add existing edge', () => {
|
||||
const newEdge = { source: '2', target: '3' };
|
||||
const nextElements = addEdge(newEdge, elements);
|
||||
const nextEdges = addEdge(newEdge, edges);
|
||||
|
||||
expect(nextElements.length).to.be.equal(elements.length);
|
||||
expect(nextEdges.length).to.be.equal(edges.length);
|
||||
});
|
||||
|
||||
it('tries to add invalid edge', () => {
|
||||
const newEdge = { nosource: '1', notarget: '3' };
|
||||
|
||||
try {
|
||||
addEdge(newEdge, elements);
|
||||
addEdge(newEdge, edges);
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
|
||||
@@ -69,66 +68,4 @@ describe('Graph Utils Testing', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('tests removeElements function', () => {
|
||||
it('removes a node', () => {
|
||||
const nextElements = removeElements([nodes[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 1);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
|
||||
it('removes multiple nodes', () => {
|
||||
const elementsToRemove = [nodes[0], nodes[1]];
|
||||
const nextElements = removeElements(elementsToRemove, elements);
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 2);
|
||||
expect(nextEdges.length).to.be.equal(0);
|
||||
});
|
||||
|
||||
it('removes no node', () => {
|
||||
const nextElementsNoRemove = removeElements([], elements);
|
||||
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
|
||||
});
|
||||
|
||||
it('tries to removes node that does not exist', () => {
|
||||
const nextElementsNoRemove = removeElements([{ id: 'id-that-does-not-exist' }], elements);
|
||||
expect(nextElementsNoRemove.length).to.be.equal(elements.length);
|
||||
});
|
||||
|
||||
it('removes an edge', () => {
|
||||
const nextElements = removeElements([edges[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 1);
|
||||
});
|
||||
|
||||
it('removes multiple edges', () => {
|
||||
const nextElements = removeElements([edges[0], edges[1]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
|
||||
it('removes node and edge', () => {
|
||||
const nextElements = removeElements([nodes[0], edges[0]], elements);
|
||||
|
||||
const nextNodes = nextElements.filter((e) => isNode(e));
|
||||
const nextEdges = nextElements.filter((e) => isEdge(e));
|
||||
|
||||
expect(nextNodes.length).to.be.equal(nodes.length - 1);
|
||||
expect(nextEdges.length).to.be.equal(edges.length - 2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,16 @@ describe('Hidden Flow Rendering', () => {
|
||||
cy.visit('/hidden');
|
||||
});
|
||||
|
||||
it('renders empty flow', () => {
|
||||
cy.get('.react-flow__node').should('not.exist');
|
||||
cy.get('.react-flow__edge').should('not.exist');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
});
|
||||
|
||||
it('toggles isHidden mode', () => {
|
||||
cy.get('.react-flow__ishidden').click();
|
||||
});
|
||||
|
||||
it('renders initial flow', () => {
|
||||
cy.get('.react-flow__renderer');
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
@@ -10,7 +20,7 @@ describe('Hidden Flow Rendering', () => {
|
||||
cy.get('.react-flow__minimap-node').should('have.length', 4);
|
||||
});
|
||||
|
||||
it('toggles isHidden mode', () => {
|
||||
it('toggles isHidden mode again', () => {
|
||||
cy.get('.react-flow__ishidden').click();
|
||||
});
|
||||
|
||||
@@ -19,14 +29,4 @@ describe('Hidden Flow Rendering', () => {
|
||||
cy.get('.react-flow__edge').should('not.exist');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
});
|
||||
|
||||
it('toggles isHidden mode again', () => {
|
||||
cy.get('.react-flow__ishidden').click();
|
||||
});
|
||||
|
||||
it('renders initial flow', () => {
|
||||
cy.get('.react-flow__node').should('have.length', 4);
|
||||
cy.get('.react-flow__edge').should('have.length', 3);
|
||||
cy.get('.react-flow__minimap-node').should('have.length', 4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,7 +93,8 @@ describe('Interaction Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('selects an edge by click', () => {
|
||||
cy.get('.react-flow__edge:first').click({ force: true }).should('have.class', 'selected');
|
||||
cy.get('.react-flow__edge:first').click({ force: true });
|
||||
cy.get('.react-flow__edge:first').should('have.class', 'selected');
|
||||
});
|
||||
|
||||
it('toggles connectable mode', () => {
|
||||
@@ -104,7 +105,7 @@ describe('Interaction Flow Rendering', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 3')
|
||||
.find('.react-flow__handle.source')
|
||||
.trigger('mousedown', { which: 1 });
|
||||
.trigger('mousedown', { button: 0 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.contains('Node 4')
|
||||
@@ -120,12 +121,12 @@ describe('Interaction Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('zooms by scroll', () => {
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: 200 })
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
@@ -136,12 +137,12 @@ describe('Interaction Flow Rendering', () => {
|
||||
|
||||
it('zooms by double click', () => {
|
||||
cy.get('.react-flow__controls-zoomout').click();
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleBeforeZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.dblclick()
|
||||
.then(() => {
|
||||
const styleAfterZoom = Cypress.$('.react-flow__nodes').css('transform');
|
||||
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
|
||||
expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('Minimap Testing', () => {
|
||||
const viewBoxBeforeZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
|
||||
const maskPathBeforeZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('wheel', 'topLeft', { deltaY: -200 })
|
||||
.then(() => {
|
||||
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
|
||||
@@ -33,16 +33,14 @@ describe('Minimap Testing', () => {
|
||||
it('changes node position', () => {
|
||||
const xPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
|
||||
const yPosBeforeDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
|
||||
const maskPathBeforeDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
cy.drag('.react-flow__node:first', { x: 500, y: 25 }).then(($el) => {
|
||||
cy.wait(1000);
|
||||
const xPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('x');
|
||||
const yPosAfterDrag = Cypress.$('.react-flow__minimap-node:first').attr('y');
|
||||
const maskPathAfterDrag = Cypress.$('.react-flow__minimap-mask').attr('d');
|
||||
|
||||
expect(xPosBeforeDrag).to.not.equal(xPosAfterDrag);
|
||||
expect(yPosBeforeDrag).to.not.equal(yPosAfterDrag);
|
||||
expect(maskPathBeforeDrag).to.not.equal(maskPathAfterDrag);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,7 +51,7 @@ describe('Minimap Testing', () => {
|
||||
// for d3 we have to pass the window to the event
|
||||
// https://github.com/cypress-io/cypress/issues/3441
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__renderer')
|
||||
cy.get('.react-flow__pane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
48988
example/package-lock.json
generated
48988
example/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,9 @@
|
||||
"react": "file:../node_modules/react",
|
||||
"react-dom": "file:../node_modules/react-dom",
|
||||
"react-flow-renderer": "file:../",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"typescript": "^4.4.3"
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.5.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -34,10 +34,10 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/dagre": "^0.7.46",
|
||||
"@types/dagre": "^0.7.47",
|
||||
"@types/localforage": "0.0.34",
|
||||
"@types/react": "^17.0.27",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-router-dom": "^5.3.0"
|
||||
"@types/react": "file:../node_modules/@types/react",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-router-dom": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,80 +1,69 @@
|
||||
import React, { useState, MouseEvent } from 'react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
isNode,
|
||||
ReactFlowProvider,
|
||||
Background,
|
||||
Elements,
|
||||
BackgroundVariant,
|
||||
FlowElement,
|
||||
Node,
|
||||
Edge,
|
||||
Connection,
|
||||
OnLoadParams,
|
||||
useReactFlow,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const BasicFlow = () => {
|
||||
const [rfInstance, setRfInstance] = useState<OnLoadParams | null>(null);
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => setRfInstance(reactFlowInstance);
|
||||
const instance = useReactFlow();
|
||||
|
||||
const updatePos = () => {
|
||||
setElements((elms) => {
|
||||
return elms.map((el) => {
|
||||
if (isNode(el)) {
|
||||
el.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
}
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return el;
|
||||
});
|
||||
});
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const logToObject = () => console.log(rfInstance?.toObject());
|
||||
const resetTransform = () => rfInstance?.setTransform({ x: 0, y: 0, zoom: 1 });
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const toggleClassnames = () => {
|
||||
setElements((elms) => {
|
||||
return elms.map((el) => {
|
||||
if (isNode(el)) {
|
||||
el.className = el.className === 'light' ? 'dark' : 'light';
|
||||
}
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.className = node.className === 'light' ? 'dark' : 'light';
|
||||
|
||||
return el;
|
||||
});
|
||||
});
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
defaultNodes={initialNodes}
|
||||
defaultEdges={initialEdges}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
className="react-flow-basic-example"
|
||||
defaultZoom={1.5}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
fitView
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
@@ -94,4 +83,10 @@ const BasicFlow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicFlow;
|
||||
export default function App() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<BasicFlow />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
98
example/src/ControlledUncontrolled/index.tsx
Normal file
98
example/src/ControlledUncontrolled/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import ReactFlow, {
|
||||
useReactFlow,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const defaultNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||
];
|
||||
|
||||
const defaultEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const defaultEdgeOptions = {
|
||||
animated: true,
|
||||
};
|
||||
|
||||
// This is bad practise. You should either use a controlled or an uncontrolled component.
|
||||
// This is just an example for testing the API.
|
||||
const ControlledUncontrolled = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(defaultNodes);
|
||||
const [edges, , onEdgesChange] = useEdgesState(defaultEdges);
|
||||
const instance = useReactFlow();
|
||||
|
||||
const logToObject = () => console.log(instance.toObject());
|
||||
const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 });
|
||||
|
||||
const updateNodePositions = () => {
|
||||
instance.setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateEdgeColors = () => {
|
||||
instance.setEdges((edges) =>
|
||||
edges.map((edge) => {
|
||||
edge.style = {
|
||||
stroke: '#ff5050',
|
||||
};
|
||||
|
||||
return edge;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultNodes={defaultNodes}
|
||||
defaultEdges={defaultEdges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
defaultEdgeOptions={defaultEdgeOptions}
|
||||
fitView
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={resetTransform} style={{ marginRight: 5 }}>
|
||||
reset transform
|
||||
</button>
|
||||
<button onClick={updateNodePositions} style={{ marginRight: 5 }}>
|
||||
change pos
|
||||
</button>
|
||||
<button onClick={updateEdgeColors} style={{ marginRight: 5 }}>
|
||||
red edges
|
||||
</button>
|
||||
<button onClick={logToObject}>toObject</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ControlledUncontrolled />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +1,31 @@
|
||||
import React, { useState } from 'react';
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
Node,
|
||||
addEdge,
|
||||
Background,
|
||||
BackgroundVariant,
|
||||
Elements,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import ConnectionLine from './ConnectionLine';
|
||||
|
||||
const initialElements: Elements = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
|
||||
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } }];
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const ConnectionLineFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
connectionLineComponent={ConnectionLine}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
import React, { useState, useEffect, MouseEvent } from 'react';
|
||||
import { useState, useEffect, MouseEvent } from 'react';
|
||||
import { ChangeEvent } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
isEdge,
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Node,
|
||||
FlowElement,
|
||||
OnLoadParams,
|
||||
Elements,
|
||||
ReactFlowInstance,
|
||||
Position,
|
||||
SnapGrid,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import ColorSelectorNode from './ColorSelectorNode';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
console.log('flow loaded:', reactFlowInstance);
|
||||
reactFlowInstance.fitView();
|
||||
};
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initBgColor = '#1A192B';
|
||||
|
||||
const connectionLineStyle = { stroke: '#fff' };
|
||||
const snapGrid: SnapGrid = [16, 16];
|
||||
|
||||
const nodeTypes = {
|
||||
selectorNode: ColorSelectorNode,
|
||||
};
|
||||
|
||||
const CustomNodeFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>([]);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const [bgColor, setBgColor] = useState<string>(initBgColor);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setElements((els) =>
|
||||
els.map((e) => {
|
||||
if (isEdge(e) || e.id !== '2') {
|
||||
return e;
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id !== '2') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const color = event.target.value;
|
||||
@@ -48,9 +51,9 @@ const CustomNodeFlow = () => {
|
||||
setBgColor(color);
|
||||
|
||||
return {
|
||||
...e,
|
||||
...node,
|
||||
data: {
|
||||
...e.data,
|
||||
...node.data,
|
||||
color,
|
||||
},
|
||||
};
|
||||
@@ -58,7 +61,7 @@ const CustomNodeFlow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
setElements([
|
||||
setNodes([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
@@ -87,31 +90,35 @@ const CustomNodeFlow = () => {
|
||||
position: { x: 550, y: 100 },
|
||||
targetPosition: Position.Left,
|
||||
},
|
||||
]);
|
||||
|
||||
setEdges([
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true, style: { stroke: '#fff' } },
|
||||
{ id: 'e2a-3', source: '2', sourceHandle: 'a', target: '3', animated: true, style: { stroke: '#fff' } },
|
||||
{ id: 'e2b-4', source: '2', sourceHandle: 'b', target: '4', animated: true, style: { stroke: '#fff' } },
|
||||
]);
|
||||
}, []);
|
||||
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) =>
|
||||
setElements((els) => addEdge({ ...params, animated: true, style: { stroke: '#fff' } }, els));
|
||||
const onConnect = (connection: Connection) =>
|
||||
setEdges((eds) => addEdge({ ...connection, animated: true, style: { stroke: '#fff' } }, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
style={{ background: bgColor }}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
snapToGrid={true}
|
||||
snapGrid={snapGrid}
|
||||
defaultZoom={1.5}
|
||||
fitView
|
||||
>
|
||||
<MiniMap
|
||||
nodeStrokeColor={(n: Node): string => {
|
||||
@@ -132,4 +139,4 @@ const CustomNodeFlow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomNodeFlow;
|
||||
export default CustomNodeFlow;
|
||||
|
||||
83
example/src/DefaultNodes/index.tsx
Normal file
83
example/src/DefaultNodes/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import ReactFlow from 'react-flow-renderer';
|
||||
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
|
||||
|
||||
import DragHandleNode from './DragHandleNode';
|
||||
|
||||
@@ -6,16 +6,24 @@ const nodeTypes = {
|
||||
dragHandleNode: DragHandleNode,
|
||||
};
|
||||
|
||||
const elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '2',
|
||||
type: 'dragHandleNode',
|
||||
dragHandle: '.custom-drag-handle',
|
||||
style: { border: '1px solid #ddd', padding: '20px 40px' },
|
||||
position: { x: 200, y: 200 },
|
||||
data: null,
|
||||
},
|
||||
];
|
||||
|
||||
const DragHandleFlow = () => <ReactFlow elements={elements} nodeTypes={nodeTypes} />;
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
const DragHandleFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges] = useEdgesState(initialEdges);
|
||||
|
||||
return <ReactFlow nodes={nodes} onNodesChange={onNodesChange} edges={edges} nodeTypes={nodeTypes} />;
|
||||
};
|
||||
|
||||
export default DragHandleFlow;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React, { useState, DragEvent } from 'react';
|
||||
import { useState, DragEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
removeElements,
|
||||
Controls,
|
||||
OnLoadParams,
|
||||
Elements,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
ElementId,
|
||||
Node,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
import './dnd.css';
|
||||
|
||||
const initialElements = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }];
|
||||
const initialNodes: Node[] = [{ id: '1', type: 'input', data: { label: 'input node' }, position: { x: 250, y: 5 } }];
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -24,15 +23,15 @@ const onDragOver = (event: DragEvent) => {
|
||||
};
|
||||
|
||||
let id = 0;
|
||||
const getId = (): ElementId => `dndnode_${id++}`;
|
||||
const getId = () => `dndnode_${id++}`;
|
||||
|
||||
const DnDFlow = () => {
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState<OnLoadParams>();
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState<ReactFlowInstance>();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onLoad = (_reactFlowInstance: OnLoadParams) => setReactFlowInstance(_reactFlowInstance);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const onInit = (rfi: ReactFlowInstance) => setReactFlowInstance(rfi);
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -47,7 +46,7 @@ const DnDFlow = () => {
|
||||
data: { label: `${type} node` },
|
||||
};
|
||||
|
||||
setElements((es) => es.concat(newNode));
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,10 +55,12 @@ const DnDFlow = () => {
|
||||
<ReactFlowProvider>
|
||||
<div className="reactflow-wrapper">
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodesChange={onNodesChange}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
>
|
||||
|
||||
@@ -1,40 +1,49 @@
|
||||
/**
|
||||
* Example for checking the different edge types and source and target positions
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
OnLoadParams,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
Elements,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
import { getElements } from './utils';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => {
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getElements());
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
|
||||
const initialElements = getElements();
|
||||
const { nodes: initialNodes, edges: initialEdges } = getElements();
|
||||
|
||||
const multiSelectionKeyCode = ['ShiftLeft', 'ShiftRight'];
|
||||
const deleteKeyCode = ['AltLeft+KeyD', 'Backspace'];
|
||||
|
||||
const EdgeTypesFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
onConnect={onConnect}
|
||||
minZoom={0.2}
|
||||
zoomOnScroll={false}
|
||||
selectionKeyCode="a+s"
|
||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||
deleteKeyCode={deleteKeyCode}
|
||||
zoomActivationKeyCode="z"
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ElementId, Elements, Position } from 'react-flow-renderer';
|
||||
import { Edge, Node, Position } from 'react-flow-renderer';
|
||||
|
||||
const nodeWidth = 80;
|
||||
const nodeGapWidth = nodeWidth * 2;
|
||||
@@ -9,10 +9,10 @@ const sourceTargetPositions = [
|
||||
{ source: Position.Right, target: Position.Left },
|
||||
];
|
||||
const nodeColors = [
|
||||
['#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
|
||||
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8'],
|
||||
['#0c5956', '#1e9e99', '#4cb3ac', '#6ec9c0', '#8ddfd4'],
|
||||
['#0f4c75', '#1b5d8b', '#276fa1', '#3282b8', '#4fa6e0'],
|
||||
];
|
||||
const edgeTypes = ['default', 'step', 'smoothstep', 'straight'];
|
||||
const edgeTypes = ['default', 'step', 'smoothstep', 'straight', 'simplebezier'];
|
||||
const offsets = [
|
||||
{
|
||||
x: 0,
|
||||
@@ -49,10 +49,10 @@ const offsets = [
|
||||
];
|
||||
|
||||
let id = 0;
|
||||
const getNodeId = (): ElementId => (id++).toString();
|
||||
const getNodeId = (): string => (id++).toString();
|
||||
|
||||
export function getElements(): Elements {
|
||||
const initialElements = [];
|
||||
export function getElements(): { nodes: Node[]; edges: Edge[] } {
|
||||
const initialElements = { nodes: [] as Node[], edges: [] as Edge[] };
|
||||
|
||||
for (let sourceTargetIndex = 0; sourceTargetIndex < sourceTargetPositions.length; sourceTargetIndex++) {
|
||||
const currSourceTargetPos = sourceTargetPositions[sourceTargetIndex];
|
||||
@@ -70,7 +70,7 @@ export function getElements(): Elements {
|
||||
};
|
||||
const sourceId = getNodeId();
|
||||
const sourceData = { label: `Source ${sourceId}` };
|
||||
const sourceNode = {
|
||||
const sourceNode: Node = {
|
||||
id: sourceId,
|
||||
style,
|
||||
data: sourceData,
|
||||
@@ -85,7 +85,7 @@ export function getElements(): Elements {
|
||||
x: sourcePosition.x + currOffset.x,
|
||||
y: sourcePosition.y + currOffset.y,
|
||||
};
|
||||
const targetNode = {
|
||||
const targetNode: Node = {
|
||||
id: targetId,
|
||||
style,
|
||||
data: targetData,
|
||||
@@ -94,10 +94,15 @@ export function getElements(): Elements {
|
||||
targetPosition: currSourceTargetPos.target,
|
||||
};
|
||||
|
||||
initialElements.push(sourceNode);
|
||||
initialElements.push(targetNode);
|
||||
initialElements.nodes.push(sourceNode);
|
||||
initialElements.nodes.push(targetNode);
|
||||
|
||||
initialElements.push({ id: `${sourceId}-${targetId}`, source: sourceId, target: targetId, type: currEdgeType });
|
||||
initialElements.edges.push({
|
||||
id: `${sourceId}-${targetId}`,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: currEdgeType,
|
||||
} as Edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react';
|
||||
import { EdgeProps, getBezierPath, getMarkerEnd } from 'react-flow-renderer';
|
||||
import { EdgeProps, getBezierPath } from 'react-flow-renderer';
|
||||
|
||||
const CustomEdge: FC<EdgeProps> = ({
|
||||
id,
|
||||
@@ -10,15 +10,12 @@ const CustomEdge: FC<EdgeProps> = ({
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}) => {
|
||||
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} />
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} />
|
||||
<text>
|
||||
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
|
||||
{data.text}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react';
|
||||
import { EdgeProps, getBezierPath, getMarkerEnd, EdgeText, getEdgeCenter } from 'react-flow-renderer';
|
||||
import { EdgeProps, getBezierPath, EdgeText, getEdgeCenter } from 'react-flow-renderer';
|
||||
|
||||
const CustomEdge: FC<EdgeProps> = ({
|
||||
id,
|
||||
@@ -10,11 +10,8 @@ const CustomEdge: FC<EdgeProps> = ({
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
data,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}) => {
|
||||
const edgePath = getBezierPath({ sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition });
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
const [centerX, centerY] = getEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
@@ -24,7 +21,7 @@ const CustomEdge: FC<EdgeProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} />
|
||||
<path id={id} className="react-flow__edge-path" d={edgePath} />
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
|
||||
@@ -1,32 +1,31 @@
|
||||
import React, { useState, MouseEvent } from 'react';
|
||||
|
||||
import { MouseEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
OnLoadParams,
|
||||
FlowElement,
|
||||
EdgeTypesType,
|
||||
Elements,
|
||||
Connection,
|
||||
Controls,
|
||||
Edge,
|
||||
ArrowHeadType,
|
||||
EdgeTypes,
|
||||
MarkerType,
|
||||
MiniMap,
|
||||
Node,
|
||||
Position,
|
||||
ReactFlowInstance,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import CustomEdge from './CustomEdge';
|
||||
import CustomEdge2 from './CustomEdge2';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
|
||||
{ id: '2a', data: { label: 'Node 2a' }, position: { x: 0, y: 180 } },
|
||||
{ id: '2b', data: { label: 'Node 2b' }, position: { x: -40, y: 300 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
|
||||
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
|
||||
@@ -35,12 +34,48 @@ const initialElements: Elements = [
|
||||
{ id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } },
|
||||
{ id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } },
|
||||
{ id: '9', type: 'output', data: { label: 'Output 9' }, position: { x: 675, y: 500 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge (default)', className: 'normal-edge' },
|
||||
{ id: 'e2-2a', source: '2', target: '2a', type: 'smoothstep', label: 'smoothstep edge' },
|
||||
{
|
||||
id: 'e2a-2b',
|
||||
source: '2a',
|
||||
target: '2b',
|
||||
type: 'simplebezier',
|
||||
label: 'simple bezier edge',
|
||||
},
|
||||
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
|
||||
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
|
||||
{ id: 'e3-3a', source: '3', target: '3a', type: 'straight', label: 'label only edge', style: { stroke: 'none' } },
|
||||
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
label: 'label with styled bg',
|
||||
labelBgPadding: [8, 4],
|
||||
labelBgBorderRadius: 4,
|
||||
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'e5-8',
|
||||
source: '5',
|
||||
target: '8',
|
||||
type: 'custom',
|
||||
data: { text: 'custom edge' },
|
||||
},
|
||||
{
|
||||
id: 'e5-9',
|
||||
source: '5',
|
||||
target: '9',
|
||||
type: 'custom2',
|
||||
data: { text: 'custom edge 2' },
|
||||
},
|
||||
{
|
||||
id: 'e5-6',
|
||||
source: '5',
|
||||
@@ -54,54 +89,46 @@ const initialElements: Elements = [
|
||||
</>
|
||||
),
|
||||
labelStyle: { fill: 'red', fontWeight: 700 },
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e5-7',
|
||||
source: '5',
|
||||
target: '7',
|
||||
label: 'label with styled bg',
|
||||
labelBgPadding: [8, 4],
|
||||
labelBgBorderRadius: 4,
|
||||
labelBgStyle: { fill: '#FFCC00', color: '#fff', fillOpacity: 0.7 },
|
||||
arrowHeadType: ArrowHeadType.ArrowClosed,
|
||||
},
|
||||
{
|
||||
id: 'e5-8',
|
||||
source: '5',
|
||||
target: '8',
|
||||
type: 'custom',
|
||||
data: { text: 'custom edge' },
|
||||
arrowHeadType: ArrowHeadType.ArrowClosed,
|
||||
},
|
||||
{
|
||||
id: 'e5-9',
|
||||
source: '5',
|
||||
target: '9',
|
||||
type: 'custom2',
|
||||
data: { text: 'custom edge 2' },
|
||||
style: { stroke: '#ffcc00' },
|
||||
markerEnd: {
|
||||
type: MarkerType.Arrow,
|
||||
color: '#FFCC00',
|
||||
markerUnits: 'userSpaceOnUse',
|
||||
width: 20,
|
||||
height: 20,
|
||||
strokeWidth: 2,
|
||||
},
|
||||
markerStart: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
color: '#FFCC00',
|
||||
orient: 'auto-start-reverse',
|
||||
markerUnits: 'userSpaceOnUse',
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const edgeTypes: EdgeTypesType = {
|
||||
const edgeTypes: EdgeTypes = {
|
||||
custom: CustomEdge,
|
||||
custom2: CustomEdge2,
|
||||
};
|
||||
|
||||
const EdgesFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
edgeTypes={edgeTypes}
|
||||
>
|
||||
|
||||
@@ -1,52 +1,51 @@
|
||||
import React, { useState, MouseEvent, CSSProperties } from 'react';
|
||||
import { MouseEvent, CSSProperties } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
OnLoadParams,
|
||||
Elements,
|
||||
ElementId,
|
||||
Node,
|
||||
FlowElement,
|
||||
BackgroundVariant,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
ReactFlowInstance,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('flow loaded:', reactFlowInstance);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
|
||||
const buttonStyle: CSSProperties = { position: 'absolute', left: 10, top: 10, zIndex: 4 };
|
||||
|
||||
const EmptyFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>([]);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
|
||||
const addRandomNode = () => {
|
||||
const nodeId: ElementId = (elements.length + 1).toString();
|
||||
const nodeId = (nodes.length + 1).toString();
|
||||
const newNode: Node = {
|
||||
id: nodeId,
|
||||
data: { label: `Node: ${nodeId}` },
|
||||
position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight },
|
||||
};
|
||||
setElements((els) => els.concat(newNode));
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onInit={onInit}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={(p) => onConnect(p)}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onlyRenderVisibleElements={false}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
|
||||
@@ -16,7 +16,9 @@ const FloatingConnectionLine: FC<ConnectionLineComponentProps> = ({
|
||||
|
||||
const targetNode = {
|
||||
id: 'connection-target',
|
||||
__rf: { width: 1, height: 1, position: { x: targetX, y: targetY } },
|
||||
width: 1,
|
||||
height: 1,
|
||||
position: { x: targetX, y: targetY },
|
||||
} as Node;
|
||||
|
||||
const { sx, sy } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { FC, useMemo, CSSProperties } from 'react';
|
||||
import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer';
|
||||
import { EdgeProps, useStore, getBezierPath, ReactFlowState } from 'react-flow-renderer';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
|
||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, markerEndId, style }) => {
|
||||
const nodes = useStoreState((state) => state.nodes);
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
const nodeSelector = (s: ReactFlowState) => s.nodeInternals;
|
||||
|
||||
const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]);
|
||||
const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]);
|
||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
||||
const nodeInternals = useStore(nodeSelector);
|
||||
|
||||
const sourceNode = useMemo(() => nodeInternals.get(source), [source, nodeInternals]);
|
||||
const targetNode = useMemo(() => nodeInternals.get(target), [target, nodeInternals]);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
@@ -27,7 +28,7 @@ const FloatingEdge: FC<EdgeProps> = ({ id, source, target, arrowHeadType, marker
|
||||
|
||||
return (
|
||||
<g className="react-flow__connection">
|
||||
<path id={id} className="react-flow__edge-path" d={d} markerEnd={markerEnd} style={style as CSSProperties} />
|
||||
<path id={id} className="react-flow__edge-path" d={d} style={style as CSSProperties} />
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
Background,
|
||||
OnLoadParams,
|
||||
EdgeTypesType,
|
||||
Elements,
|
||||
ReactFlowInstance,
|
||||
EdgeTypes,
|
||||
Connection,
|
||||
Edge,
|
||||
ArrowHeadType,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import './style.css';
|
||||
@@ -18,29 +16,31 @@ import FloatingEdge from './FloatingEdge';
|
||||
import FloatingConnectionLine from './FloatingConnectionLine';
|
||||
import { createElements } from './utils';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
|
||||
const initialElements: Elements = createElements();
|
||||
const { nodes: initialNodes, edges: initialEdges } = createElements();
|
||||
|
||||
const edgeTypes: EdgeTypesType = {
|
||||
const edgeTypes: EdgeTypes = {
|
||||
floating: FloatingEdge,
|
||||
};
|
||||
|
||||
const NodeAsHandleFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const FloatingEdges = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
|
||||
const onConnect = (params: Connection | Edge) =>
|
||||
setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els));
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="floatingedges">
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineComponent={FloatingConnectionLine}
|
||||
>
|
||||
@@ -50,4 +50,4 @@ const NodeAsHandleFlow = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeAsHandleFlow;
|
||||
export default FloatingEdges;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer';
|
||||
import { Position, XYPosition, Node, Edge } from 'react-flow-renderer';
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
position: intersectionNodePosition,
|
||||
} = intersectionNode.__rf;
|
||||
const targetPosition = targetNode.__rf.position;
|
||||
} = intersectionNode;
|
||||
const targetPosition = targetNode.position;
|
||||
|
||||
const w = intersectionNodeWidth / 2;
|
||||
const h = intersectionNodeHeight / 2;
|
||||
const w = (intersectionNodeWidth ?? 0) / 2;
|
||||
const h = (intersectionNodeHeight ?? 0) / 2;
|
||||
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
@@ -32,7 +33,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.__rf.position, ...node.__rf };
|
||||
const n = { ...node.position, ...node };
|
||||
const nx = Math.round(n.x);
|
||||
const ny = Math.round(n.y);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
@@ -41,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.width - 1) {
|
||||
if (px >= nx + (n.width ?? 0) - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + n.height - 1) {
|
||||
if (py >= n.y + (n.height ?? 0) - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
@@ -72,11 +73,18 @@ export function getEdgeParams(source: Node, target: Node) {
|
||||
};
|
||||
}
|
||||
|
||||
export function createElements() {
|
||||
const elements = [];
|
||||
type NodesAndEdges = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export function createElements(): NodesAndEdges {
|
||||
const nodes: Node[] = [];
|
||||
const edges: Edge[] = [];
|
||||
|
||||
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
|
||||
elements.push({ id: 'target', data: { label: 'Target' }, position: center });
|
||||
nodes.push({ id: 'target', data: { label: 'Target' }, position: center });
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const degrees = i * (360 / 8);
|
||||
@@ -84,16 +92,15 @@ export function createElements() {
|
||||
const x = 250 * Math.cos(radians) + center.x;
|
||||
const y = 250 * Math.sin(radians) + center.y;
|
||||
|
||||
elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
|
||||
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
|
||||
|
||||
elements.push({
|
||||
edges.push({
|
||||
id: `edge-${i}`,
|
||||
target: 'target',
|
||||
source: `${i}`,
|
||||
type: 'floating',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
});
|
||||
}
|
||||
|
||||
return elements;
|
||||
return { nodes, edges };
|
||||
}
|
||||
|
||||
@@ -1,34 +1,53 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import ReactFlow, { addEdge, MiniMap, Controls, Connection, Edge, Elements } from 'react-flow-renderer';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Connection,
|
||||
Edge,
|
||||
Node,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements: Elements = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', hidden: true, data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
{ id: '2', hidden: true, data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', hidden: true, data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', hidden: true, data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4' },
|
||||
];
|
||||
|
||||
const setHidden = (hidden: boolean) => (els: any[]) =>
|
||||
els.map((e: any) => {
|
||||
e.hidden = hidden;
|
||||
return e;
|
||||
});
|
||||
|
||||
const HiddenFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const [isHidden, setIsHidden] = useState<boolean>(false);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes] = useNodesState(initialNodes);
|
||||
const [edges, setEdges] = useEdgesState(initialEdges);
|
||||
|
||||
const [isHidden, setIsHidden] = useState<boolean>(true);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setElements((els) =>
|
||||
els.map((e) => {
|
||||
e.isHidden = isHidden;
|
||||
return e;
|
||||
})
|
||||
);
|
||||
setNodes(setHidden(isHidden));
|
||||
setEdges(setHidden(isHidden));
|
||||
}, [isHidden]);
|
||||
|
||||
return (
|
||||
<ReactFlow elements={elements} onConnect={onConnect}>
|
||||
<ReactFlow nodes={nodes} edges={edges} onConnect={onConnect}>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
|
||||
@@ -1,37 +1,42 @@
|
||||
import React, { useState, MouseEvent, WheelEvent } from 'react';
|
||||
import { useState, MouseEvent as ReactMouseEvent, WheelEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Elements,
|
||||
Node,
|
||||
FlowElement,
|
||||
Connection,
|
||||
Edge,
|
||||
PanOnScrollMode,
|
||||
FlowTransform,
|
||||
Viewport,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onPaneClick = (event: MouseEvent) => console.log('onPaneClick', event);
|
||||
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('click', node);
|
||||
const onEdgeClick = (_: ReactMouseEvent, edge: Edge) => console.log('click', edge);
|
||||
const onPaneClick = (event: ReactMouseEvent) => console.log('onPaneClick', event);
|
||||
const onPaneScroll = (event?: WheelEvent) => console.log('onPaneScroll', event);
|
||||
const onPaneContextMenu = (event: MouseEvent) => console.log('onPaneContextMenu', event);
|
||||
const onMoveEnd = (flowTranasform?: FlowTransform) => console.log('onMoveEnd', flowTranasform);
|
||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('onPaneContextMenu', event);
|
||||
const onMoveEnd = (_: TouchEvent | MouseEvent, viewport: Viewport) => console.log('onMoveEnd', viewport);
|
||||
|
||||
const InteractionFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((els) => addEdge(params, els));
|
||||
|
||||
const [isSelectable, setIsSelectable] = useState<boolean>(false);
|
||||
const [isDraggable, setIsDraggable] = useState<boolean>(false);
|
||||
@@ -41,14 +46,17 @@ const InteractionFlow = () => {
|
||||
const [panOnScroll, setPanOnScroll] = useState<boolean>(false);
|
||||
const [panOnScrollMode, setPanOnScrollMode] = useState<PanOnScrollMode>(PanOnScrollMode.Free);
|
||||
const [zoomOnDoubleClick, setZoomOnDoubleClick] = useState<boolean>(false);
|
||||
const [paneMoveable, setPaneMoveable] = useState<boolean>(true);
|
||||
const [panOnDrag, setPanOnDrag] = useState<boolean>(true);
|
||||
const [captureZoomClick, setCaptureZoomClick] = useState<boolean>(false);
|
||||
const [captureZoomScroll, setCaptureZoomScroll] = useState<boolean>(false);
|
||||
const [captureElementClick, setCaptureElementClick] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
elementsSelectable={isSelectable}
|
||||
nodesConnectable={isConnectable}
|
||||
nodesDraggable={isDraggable}
|
||||
@@ -58,10 +66,11 @@ const InteractionFlow = () => {
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
onConnect={onConnect}
|
||||
onElementClick={captureElementClick ? onElementClick : undefined}
|
||||
onNodeClick={captureElementClick ? onNodeClick : undefined}
|
||||
onEdgeClick={captureElementClick ? onEdgeClick : undefined}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
paneMoveable={paneMoveable}
|
||||
panOnDrag={panOnDrag}
|
||||
onPaneClick={captureZoomClick ? onPaneClick : undefined}
|
||||
onPaneScroll={captureZoomScroll ? onPaneScroll : undefined}
|
||||
onPaneContextMenu={captureZoomClick ? onPaneContextMenu : undefined}
|
||||
@@ -171,14 +180,14 @@ const InteractionFlow = () => {
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="panemoveable">
|
||||
paneMoveable
|
||||
<label htmlFor="panondrag">
|
||||
panOnDrag
|
||||
<input
|
||||
id="panemoveable"
|
||||
id="panondrag"
|
||||
type="checkbox"
|
||||
checked={paneMoveable}
|
||||
onChange={(event) => setPaneMoveable(event.target.checked)}
|
||||
className="react-flow__panemoveable"
|
||||
checked={panOnDrag}
|
||||
onChange={(event) => setPanOnDrag(event.target.checked)}
|
||||
className="react-flow__panondrag"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -1,74 +1,75 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
removeElements,
|
||||
Controls,
|
||||
isNode,
|
||||
Elements,
|
||||
Connection,
|
||||
Edge,
|
||||
NodeExtent,
|
||||
CoordinateExtent,
|
||||
Position,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
import dagre from 'dagre';
|
||||
|
||||
import initialElements from './initial-elements';
|
||||
import initialItems from './initial-elements';
|
||||
|
||||
import './layouting.css';
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const nodeExtent: NodeExtent = [
|
||||
const nodeExtent: CoordinateExtent = [
|
||||
[0, 0],
|
||||
[1000, 1000],
|
||||
];
|
||||
|
||||
const LayoutFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialItems.nodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialItems.edges);
|
||||
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
|
||||
const onLayout = (direction: string) => {
|
||||
const isHorizontal = direction === 'LR';
|
||||
dagreGraph.setGraph({ rankdir: direction });
|
||||
|
||||
elements.forEach((el) => {
|
||||
if (isNode(el)) {
|
||||
dagreGraph.setNode(el.id, { width: 150, height: 50 });
|
||||
} else {
|
||||
dagreGraph.setEdge(el.source, el.target);
|
||||
}
|
||||
nodes.forEach((node) => {
|
||||
dagreGraph.setNode(node.id, { width: 150, height: 50 });
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
dagreGraph.setEdge(edge.source, edge.target);
|
||||
});
|
||||
|
||||
dagre.layout(dagreGraph);
|
||||
|
||||
const layoutedElements = elements.map((el) => {
|
||||
if (isNode(el)) {
|
||||
const nodeWithPosition = dagreGraph.node(el.id);
|
||||
el.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
||||
el.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
||||
// we need to pass a slightly different position in order to notify react flow about the change
|
||||
// @TODO how can we change the position handling so that we dont need this hack?
|
||||
el.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
|
||||
}
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id);
|
||||
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
|
||||
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
|
||||
// we need to pass a slightly different position in order to notify react flow about the change
|
||||
// @TODO how can we change the position handling so that we dont need this hack?
|
||||
node.position = { x: nodeWithPosition.x + Math.random() / 1000, y: nodeWithPosition.y };
|
||||
|
||||
return el;
|
||||
return node;
|
||||
});
|
||||
|
||||
setElements(layoutedElements);
|
||||
setNodes(layoutedNodes);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layoutflow">
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodeExtent={nodeExtent}
|
||||
onLoad={() => onLayout('TB')}
|
||||
onInit={() => onLayout('TB')}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Elements, XYPosition } from 'react-flow-renderer';
|
||||
import { Node, Edge, XYPosition } from 'react-flow-renderer';
|
||||
|
||||
const position: XYPosition = { x: 0, y: 0 };
|
||||
|
||||
const elements: Elements = [
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
@@ -56,6 +56,9 @@ const elements: Elements = [
|
||||
position,
|
||||
},
|
||||
{ id: '7', type: 'output', data: { label: 'output' }, position: { x: 400, y: 450 } },
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{ id: 'e12', source: '1', target: '2', type: 'smoothstep' },
|
||||
{ id: 'e13', source: '1', target: '3', type: 'smoothstep' },
|
||||
{ id: 'e22a', source: '2', target: '2a', type: 'smoothstep' },
|
||||
@@ -68,4 +71,6 @@ const elements: Elements = [
|
||||
{ id: 'e57', source: '5', target: '7', type: 'smoothstep' },
|
||||
];
|
||||
|
||||
export default elements;
|
||||
const nodesAndEdges = { nodes, edges };
|
||||
|
||||
export default nodesAndEdges;
|
||||
|
||||
@@ -1,34 +1,45 @@
|
||||
import React, { useState, FC } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
Background,
|
||||
Elements,
|
||||
Node,
|
||||
Edge,
|
||||
Connection,
|
||||
ReactFlowProvider,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import './multiflows.css';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const Flow: FC = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Edge | Connection) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = (params: Edge | Connection) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
>
|
||||
<Background />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
|
||||
170
example/src/NestedNodes/index.tsx
Normal file
170
example/src/NestedNodes/index.tsx
Normal 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;
|
||||
@@ -1,10 +1,16 @@
|
||||
import React, { useState, CSSProperties } from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
import ReactFlow, { addEdge, isEdge, OnLoadParams, Elements, Position, Connection, Edge } from 'react-flow-renderer';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Node,
|
||||
Position,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
sourcePosition: Position.Right,
|
||||
@@ -20,31 +26,40 @@ const initialElements: Elements = [
|
||||
data: { label: 'A Node' },
|
||||
position: { x: 250, y: 0 },
|
||||
},
|
||||
{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', type: 'smoothstep', target: '2', animated: true }];
|
||||
|
||||
const buttonStyle: CSSProperties = { position: 'absolute', right: 10, top: 30, zIndex: 4 };
|
||||
|
||||
const NodeTypeChangeFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const changeType = () => {
|
||||
setElements((elms) =>
|
||||
elms.map((el) => {
|
||||
if (isEdge(el) || el.type === 'input') {
|
||||
return el;
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.type === 'input') {
|
||||
return node;
|
||||
}
|
||||
|
||||
return {
|
||||
...el,
|
||||
type: el.type === 'default' ? 'output' : 'default',
|
||||
...node,
|
||||
type: node.type === 'default' ? 'output' : 'default',
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow elements={elements} onConnect={onConnect} onLoad={onLoad}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<button onClick={changeType} style={buttonStyle}>
|
||||
change type
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import React, { useState, CSSProperties, FC } from 'react';
|
||||
import { useState, CSSProperties, FC } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Elements,
|
||||
Node,
|
||||
Position,
|
||||
Connection,
|
||||
Edge,
|
||||
NodeProps,
|
||||
NodeTypesType,
|
||||
NodeTypes,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
sourcePosition: Position.Right,
|
||||
@@ -41,7 +43,7 @@ const NodeB: FC<NodeProps> = () => {
|
||||
};
|
||||
|
||||
type NodeTypesObject = {
|
||||
[key: string]: NodeTypesType;
|
||||
[key: string]: NodeTypes;
|
||||
};
|
||||
|
||||
const nodeTypesObjects: NodeTypesObject = {
|
||||
@@ -55,16 +57,19 @@ const nodeTypesObjects: NodeTypesObject = {
|
||||
|
||||
const NodeTypeChangeFlow = () => {
|
||||
const [nodeTypesId, setNodeTypesId] = useState<string>('a');
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const changeType = () => setNodeTypesId((nt) => (nt === 'a' ? 'b' : 'a'));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypesObjects[nodeTypesId]}
|
||||
nodeTypesId={nodeTypesId}
|
||||
>
|
||||
<button onClick={changeType} style={buttonStyle}>
|
||||
change type
|
||||
|
||||
@@ -1,54 +1,52 @@
|
||||
import React, { useState, MouseEvent, CSSProperties } from 'react';
|
||||
|
||||
import { MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
Controls,
|
||||
Background,
|
||||
isNode,
|
||||
Node,
|
||||
Elements,
|
||||
FlowElement,
|
||||
OnLoadParams,
|
||||
FlowTransform,
|
||||
Viewport,
|
||||
SnapGrid,
|
||||
ArrowHeadType,
|
||||
Connection,
|
||||
Edge,
|
||||
ReactFlowInstance,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
OnSelectionChangeParams,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStart = (_: MouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node);
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeDoubleClick = (_: MouseEvent, node: Node) => console.log('node double click', node);
|
||||
const onPaneClick = (event: MouseEvent) => console.log('pane click', event);
|
||||
const onPaneScroll = (event?: MouseEvent) => console.log('pane scroll', event);
|
||||
const onPaneContextMenu = (event: MouseEvent) => console.log('pane context menu', event);
|
||||
const onSelectionDrag = (_: MouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
||||
const onSelectionDragStart = (_: MouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||
const onSelectionDragStop = (_: MouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||
const onSelectionContextMenu = (event: MouseEvent, nodes: Node[]) => {
|
||||
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
|
||||
const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node);
|
||||
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
|
||||
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
|
||||
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
|
||||
const onPaneContextMenu = (event: ReactMouseEvent) => console.log('pane context menu', event);
|
||||
const onSelectionDrag = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag', nodes);
|
||||
const onSelectionDragStart = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag start', nodes);
|
||||
const onSelectionDragStop = (_: ReactMouseEvent, nodes: Node[]) => console.log('selection drag stop', nodes);
|
||||
const onSelectionContextMenu = (event: ReactMouseEvent, nodes: Node[]) => {
|
||||
event.preventDefault();
|
||||
console.log('selection context menu', nodes);
|
||||
};
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) =>
|
||||
console.log(`${isNode(element) ? 'node' : 'edge'} click:`, element);
|
||||
const onSelectionChange = (elements: Elements | null) => console.log('selection change', elements);
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => {
|
||||
console.log('flow loaded:', reactFlowInstance);
|
||||
reactFlowInstance.fitView();
|
||||
const onNodeClick = (_: ReactMouseEvent, node: Node) => console.log('node click:', node);
|
||||
|
||||
const onSelectionChange = ({ nodes, edges }: OnSelectionChangeParams) => console.log('selection change', nodes, edges);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
console.log('pane ready:', reactFlowInstance);
|
||||
};
|
||||
|
||||
const onMoveStart = (transform?: FlowTransform) => console.log('zoom/move start', transform);
|
||||
const onMoveEnd = (transform?: FlowTransform) => console.log('zoom/move end', transform);
|
||||
const onEdgeContextMenu = (_: MouseEvent, edge: Edge) => console.log('edge context menu', edge);
|
||||
const onEdgeMouseEnter = (_: MouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
|
||||
const onEdgeMouseMove = (_: MouseEvent, edge: Edge) => console.log('edge mouse move', edge);
|
||||
const onEdgeMouseLeave = (_: MouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
||||
const onEdgeDoubleClick = (_: MouseEvent, edge: Edge) => console.log('edge double click', edge);
|
||||
const onMoveStart = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move start', viewport);
|
||||
const onMoveEnd = (_: MouseEvent | TouchEvent, viewport: Viewport) => console.log('zoom/move end', viewport);
|
||||
const onEdgeContextMenu = (_: ReactMouseEvent, edge: Edge) => console.log('edge context menu', edge);
|
||||
const onEdgeMouseEnter = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse enter', edge);
|
||||
const onEdgeMouseMove = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse move', edge);
|
||||
const onEdgeMouseLeave = (_: ReactMouseEvent, edge: Edge) => console.log('edge mouse leave', edge);
|
||||
const onEdgeDoubleClick = (_: ReactMouseEvent, edge: Edge) => console.log('edge double click', edge);
|
||||
const onNodesDelete = (nodes: Node[]) => console.log('nodes delete', nodes);
|
||||
const onEdgesDelete = (edges: Edge[]) => console.log('edges delete', edges);
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
@@ -122,10 +120,13 @@ const initialElements: Elements = [
|
||||
position: { x: 100, y: 480 },
|
||||
},
|
||||
{ id: '7', type: 'output', data: { label: 'Another output node' }, position: { x: 400, y: 450 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'this is an edge label' },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
{ id: 'e3-4', source: '3', target: '4', animated: true, label: 'animated edge' },
|
||||
{ id: 'e4-5', source: '4', target: '5', arrowHeadType: ArrowHeadType.Arrow, label: 'edge with arrow head' },
|
||||
{ id: 'e4-5', source: '4', target: '5', label: 'edge with arrow head' },
|
||||
{ id: 'e5-6', source: '5', target: '6', type: 'smoothstep', label: 'smooth step edge' },
|
||||
{
|
||||
id: 'e5-7',
|
||||
@@ -158,15 +159,17 @@ const nodeColor = (n: Node): string => {
|
||||
};
|
||||
|
||||
const OverviewFlow = () => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
onPaneScroll={onPaneScroll}
|
||||
@@ -182,7 +185,7 @@ const OverviewFlow = () => {
|
||||
onSelectionChange={onSelectionChange}
|
||||
onMoveStart={onMoveStart}
|
||||
onMoveEnd={onMoveEnd}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
snapToGrid={true}
|
||||
snapGrid={snapGrid}
|
||||
@@ -191,6 +194,13 @@ const OverviewFlow = () => {
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
attributionPosition="top-right"
|
||||
maxZoom={Infinity}
|
||||
onNodesDelete={onNodesDelete}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
deleteKeyCode={null}
|
||||
>
|
||||
<MiniMap nodeStrokeColor={nodeStrokeColor} nodeColor={nodeColor} nodeBorderRadius={2} />
|
||||
<Controls />
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useStoreState, useStoreActions } from 'react-flow-renderer';
|
||||
import { useStore, useStoreApi } from 'react-flow-renderer';
|
||||
|
||||
const Sidebar = () => {
|
||||
const nodes = useStoreState((store) => store.nodes);
|
||||
const transform = useStoreState((store) => store.transform);
|
||||
const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements);
|
||||
const store = useStoreApi();
|
||||
const nodeInternals = useStore((store) => store.nodeInternals);
|
||||
const transform = useStore((store) => store.transform);
|
||||
|
||||
const selectAll = () => {
|
||||
setSelectedElements(nodes.map((node) => ({ id: node.id, type: node.type })));
|
||||
nodeInternals.forEach((node) => (node.selected = true));
|
||||
store.setState({ nodeInternals: new Map(nodeInternals) });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -20,9 +20,9 @@ const Sidebar = () => {
|
||||
[{transform[0].toFixed(2)}, {transform[1].toFixed(2)}, {transform[2].toFixed(2)}]
|
||||
</div>
|
||||
<div className="title">Nodes</div>
|
||||
{nodes.map((node) => (
|
||||
{Array.from(nodeInternals).map(([, node]) => (
|
||||
<div key={node.id}>
|
||||
Node {node.id} - x: {node.__rf.position.x.toFixed(2)}, y: {node.__rf.position.y.toFixed(2)}
|
||||
Node {node.id} - x: {node.position.x.toFixed(2)}, y: {node.position.y.toFixed(2)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
import React, { useState, MouseEvent } from 'react';
|
||||
import { MouseEvent } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
addEdge,
|
||||
removeElements,
|
||||
Node,
|
||||
Controls,
|
||||
OnLoadParams,
|
||||
FlowElement,
|
||||
Connection,
|
||||
Edge,
|
||||
Elements,
|
||||
ConnectionMode,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
ReactFlowInstance,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import Sidebar from './Sidebar';
|
||||
|
||||
import './provider.css';
|
||||
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => console.log('flow loaded:', reactFlowInstance);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => console.log('pane ready:', reactFlowInstance);
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
const ProviderFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<div className="providerflow">
|
||||
@@ -39,11 +42,13 @@ const ProviderFlow = () => {
|
||||
<Sidebar />
|
||||
<div className="reactflow-wrapper">
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onLoad={onLoad}
|
||||
onInit={onInit}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
>
|
||||
<Controls />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { memo, useCallback, Dispatch, FC } from 'react';
|
||||
import { useZoomPanHelper, OnLoadParams, Elements, FlowExportObject } from 'react-flow-renderer';
|
||||
import { useReactFlow, ReactFlowInstance, Edge, Node, ReactFlowJsonObject } from 'react-flow-renderer';
|
||||
import localforage from 'localforage';
|
||||
|
||||
localforage.config({
|
||||
@@ -12,12 +12,13 @@ const flowKey = 'example-flow';
|
||||
const getNodeId = () => `randomnode_${+new Date()}`;
|
||||
|
||||
type ControlsProps = {
|
||||
rfInstance?: OnLoadParams;
|
||||
setElements: Dispatch<React.SetStateAction<Elements<any>>>;
|
||||
rfInstance?: ReactFlowInstance;
|
||||
setNodes: Dispatch<React.SetStateAction<Node<any>[]>>;
|
||||
setEdges: Dispatch<React.SetStateAction<Edge<any>[]>>;
|
||||
};
|
||||
|
||||
const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
|
||||
const { transform } = useZoomPanHelper();
|
||||
const Controls: FC<ControlsProps> = ({ rfInstance, setNodes, setEdges }) => {
|
||||
const { setViewport } = useReactFlow();
|
||||
|
||||
const onSave = useCallback(() => {
|
||||
if (rfInstance) {
|
||||
@@ -28,17 +29,18 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
|
||||
|
||||
const onRestore = useCallback(() => {
|
||||
const restoreFlow = async () => {
|
||||
const flow: FlowExportObject | null = await localforage.getItem(flowKey);
|
||||
const flow: ReactFlowJsonObject | null = await localforage.getItem(flowKey);
|
||||
|
||||
if (flow) {
|
||||
const [x = 0, y = 0] = flow.position;
|
||||
setElements(flow.elements || []);
|
||||
transform({ x, y, zoom: flow.zoom || 0 });
|
||||
const { x, y, zoom } = flow.viewport;
|
||||
setNodes(flow.nodes || []);
|
||||
setEdges(flow.edges || []);
|
||||
setViewport({ x, y, zoom: zoom || 0 });
|
||||
}
|
||||
};
|
||||
|
||||
restoreFlow();
|
||||
}, [setElements, transform]);
|
||||
}, [setNodes, setEdges, setViewport]);
|
||||
|
||||
const onAdd = useCallback(() => {
|
||||
const newNode = {
|
||||
@@ -46,8 +48,8 @@ const Controls: FC<ControlsProps> = ({ rfInstance, setElements }) => {
|
||||
data: { label: 'Added node' },
|
||||
position: { x: Math.random() * window.innerWidth - 100, y: Math.random() * window.innerHeight },
|
||||
};
|
||||
setElements((els) => els.concat(newNode));
|
||||
}, [setElements]);
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
}, [setNodes]);
|
||||
|
||||
return (
|
||||
<div className="save__controls">
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import ReactFlow, {
|
||||
ReactFlowProvider,
|
||||
removeElements,
|
||||
Node,
|
||||
addEdge,
|
||||
Elements,
|
||||
Connection,
|
||||
Edge,
|
||||
OnLoadParams,
|
||||
ReactFlowInstance,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import Controls from './Controls';
|
||||
|
||||
import './save.css';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', data: { label: 'Node 1' }, position: { x: 100, y: 100 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
|
||||
const SaveRestore = () => {
|
||||
const [rfInstance, setRfInstance] = useState<OnLoadParams>();
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance>();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow elements={elements} onElementsRemove={onElementsRemove} onConnect={onConnect} onLoad={setRfInstance}>
|
||||
<Controls rfInstance={rfInstance} setElements={setElements} />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onInit={setRfInstance}
|
||||
>
|
||||
<Controls rfInstance={rfInstance} setNodes={setNodes} setEdges={setEdges} />
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
import React, { useState, CSSProperties } from 'react';
|
||||
import { useState, CSSProperties, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
addEdge,
|
||||
MiniMap,
|
||||
isNode,
|
||||
Controls,
|
||||
Background,
|
||||
OnLoadParams,
|
||||
Elements,
|
||||
Connection,
|
||||
ReactFlowInstance,
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
applyNodeChanges,
|
||||
Connection,
|
||||
addEdge,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import { getElements } from './utils';
|
||||
import { getNodesAndEdges } from './utils';
|
||||
|
||||
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 4 };
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => {
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
console.log(reactFlowInstance.getElements());
|
||||
console.log(reactFlowInstance.getNodes());
|
||||
};
|
||||
|
||||
const initialElements: Elements = getElements(30, 30);
|
||||
const { nodes: initialNodes, edges: initialEdges } = getNodesAndEdges(30, 30);
|
||||
|
||||
const StressFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const onConnect = useCallback((connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
}, []);
|
||||
const updatePos = () => {
|
||||
setElements((elms) => {
|
||||
return elms.map((el) => {
|
||||
if (isNode(el)) {
|
||||
return {
|
||||
...el,
|
||||
position: {
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * window.innerHeight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return el;
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
x: Math.random() * window.innerWidth,
|
||||
y: Math.random() * window.innerHeight,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateElements = () => {
|
||||
const grid = Math.ceil(Math.random() * 10);
|
||||
setElements(getElements(grid, grid));
|
||||
const initialElements = getNodesAndEdges(grid, grid);
|
||||
setNodes(initialElements.nodes);
|
||||
setEdges(initialElements.edges);
|
||||
};
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactFlow elements={elements} onLoad={onLoad} onElementsRemove={onElementsRemove} onConnect={onConnect}>
|
||||
<ReactFlow nodes={nodes} edges={edges} onInit={onInit} onConnect={onConnect} onNodesChange={onNodesChange}>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<Background />
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Elements } from 'react-flow-renderer';
|
||||
import { Node, Edge } from 'react-flow-renderer';
|
||||
|
||||
export function getElements(xElements: number = 10, yElements: number = 10): Elements {
|
||||
const initialElements = [];
|
||||
type ElementsCollection = {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
};
|
||||
|
||||
export function getNodesAndEdges(xElements: number = 10, yElements: number = 10): ElementsCollection {
|
||||
const initialNodes = [];
|
||||
const initialEdges: Edge[] = [];
|
||||
let nodeId = 1;
|
||||
let recentNodeId = null;
|
||||
|
||||
@@ -15,10 +21,10 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
|
||||
data,
|
||||
position,
|
||||
};
|
||||
initialElements.push(node);
|
||||
initialNodes.push(node);
|
||||
|
||||
if (recentNodeId && nodeId <= xElements * yElements) {
|
||||
initialElements.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
|
||||
initialEdges.push({ id: `${x}-${y}`, source: recentNodeId.toString(), target: nodeId.toString() });
|
||||
}
|
||||
|
||||
recentNodeId = nodeId;
|
||||
@@ -26,5 +32,8 @@ export function getElements(xElements: number = 10, yElements: number = 10): Ele
|
||||
}
|
||||
}
|
||||
|
||||
return initialElements;
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges,
|
||||
};
|
||||
}
|
||||
|
||||
27
example/src/Subflow/DebugNode.tsx
Normal file
27
example/src/Subflow/DebugNode.tsx
Normal 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
example/src/Subflow/index.tsx
Normal file
197
example/src/Subflow/index.tsx
Normal 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;
|
||||
@@ -1,26 +1,30 @@
|
||||
import React, { useState, MouseEvent } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge, Node, FlowElement, Elements, Connection, Edge } from 'react-flow-renderer';
|
||||
import { MouseEvent } from 'react';
|
||||
import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
|
||||
const onElementClick = (_: MouseEvent, element: FlowElement) => console.log('click', element);
|
||||
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
|
||||
|
||||
const elementsA: Elements = [
|
||||
const nodesA: Node[] = [
|
||||
{ id: '1a', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||
{ id: '2a', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||
{ id: '3a', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4a', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||
];
|
||||
|
||||
const edgesA: Edge[] = [
|
||||
{ id: 'e1-2', source: '1a', target: '2a' },
|
||||
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||
];
|
||||
|
||||
const elementsB: Elements = [
|
||||
const nodesB: Node[] = [
|
||||
{ id: 'inputb', type: 'input', data: { label: 'Input' }, position: { x: 300, y: 5 }, className: 'light' },
|
||||
{ id: '1b', data: { label: 'Node 1' }, position: { x: 0, y: 100 }, className: 'light' },
|
||||
{ id: '2b', data: { label: 'Node 2' }, position: { x: 200, y: 100 }, className: 'light' },
|
||||
{ id: '3b', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4b', data: { label: 'Node 4' }, position: { x: 600, y: 100 }, className: 'light' },
|
||||
];
|
||||
|
||||
const edgesB: Edge[] = [
|
||||
{ id: 'e1b', source: 'inputb', target: '1b' },
|
||||
{ id: 'e2b', source: 'inputb', target: '2b' },
|
||||
{ id: 'e3b', source: 'inputb', target: '3b' },
|
||||
@@ -28,23 +32,39 @@ const elementsB: Elements = [
|
||||
];
|
||||
|
||||
const BasicFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(elementsA);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(nodesA);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA);
|
||||
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
>
|
||||
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
|
||||
<button onClick={() => setElements(elementsA)} style={{ marginRight: 5 }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNodes(nodesA);
|
||||
setEdges(edgesA);
|
||||
}}
|
||||
style={{ marginRight: 5 }}
|
||||
>
|
||||
flow a
|
||||
</button>
|
||||
<button onClick={() => setElements(elementsB)}>flow b</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setNodes(nodesB);
|
||||
setEdges(edgesB);
|
||||
}}
|
||||
>
|
||||
flow b
|
||||
</button>
|
||||
</div>
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
50
example/src/TouchDevice/index.tsx
Normal file
50
example/src/TouchDevice/index.tsx
Normal 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
example/src/TouchDevice/touch-device.css
Normal file
19
example/src/TouchDevice/touch-device.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,79 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
NodeTypesType,
|
||||
useReactFlow,
|
||||
NodeTypes,
|
||||
addEdge,
|
||||
useZoomPanHelper,
|
||||
ReactFlowProvider,
|
||||
Elements,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
ElementId,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
updateEdge,
|
||||
ArrowHeadType,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
import CustomNode from './CustomNode';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '00',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '01',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 50 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '02',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 50 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '03',
|
||||
type: 'custom',
|
||||
position: { x: 500, y: 500 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '04',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 500 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 5 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
type: 'custom',
|
||||
position: { x: 600, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '30',
|
||||
type: 'custom',
|
||||
position: { x: 300, y: 600 },
|
||||
data: null,
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
type: 'custom',
|
||||
position: { x: 5, y: 250 },
|
||||
data: null,
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{
|
||||
id: 'e0-1a',
|
||||
source: '00',
|
||||
@@ -69,7 +81,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'bottom',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-1b',
|
||||
@@ -78,7 +89,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'right',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-2a',
|
||||
@@ -87,7 +97,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'left',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-2b',
|
||||
@@ -96,7 +105,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'bottom',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-3a',
|
||||
@@ -105,7 +113,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'top',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-3b',
|
||||
@@ -114,7 +121,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'left',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-4a',
|
||||
@@ -123,7 +129,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'right',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-4b',
|
||||
@@ -132,7 +137,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'top',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-10',
|
||||
@@ -141,7 +145,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'top',
|
||||
targetHandle: 'bottom',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-20',
|
||||
@@ -150,7 +153,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'right',
|
||||
targetHandle: 'left',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-30',
|
||||
@@ -159,7 +161,6 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'bottom',
|
||||
targetHandle: 'top',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
{
|
||||
id: 'e0-40',
|
||||
@@ -168,31 +169,33 @@ const initialElements: Elements = [
|
||||
sourceHandle: 'left',
|
||||
targetHandle: 'right',
|
||||
type: 'default',
|
||||
arrowHeadType: ArrowHeadType.Arrow,
|
||||
},
|
||||
];
|
||||
|
||||
const nodeTypes: NodeTypesType = {
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
let id = 4;
|
||||
const getId = (): ElementId => `${id++}`;
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge({ ...params, type: 'default' }, els));
|
||||
const { project } = useZoomPanHelper();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
|
||||
const { project } = useReactFlow();
|
||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setElements((els) => updateEdge(oldEdge, newConnection, els));
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt) =>
|
||||
setElements((els) =>
|
||||
els.concat({
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
type: 'custom',
|
||||
data: null,
|
||||
})
|
||||
),
|
||||
[project]
|
||||
@@ -200,7 +203,10 @@ const UpdateNodeInternalsFlow = () => {
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Controls,
|
||||
updateEdge,
|
||||
addEdge,
|
||||
Elements,
|
||||
OnLoadParams,
|
||||
applyNodeChanges,
|
||||
applyEdgeChanges,
|
||||
ReactFlowInstance,
|
||||
Connection,
|
||||
Edge,
|
||||
removeElements,
|
||||
Node,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
@@ -46,29 +49,41 @@ const initialElements: Elements = [
|
||||
position: { x: 400, y: 100 },
|
||||
style: { background: '#D6D5E6', color: '#333', border: '1px solid #222138', width: 180 },
|
||||
},
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' },
|
||||
];
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
|
||||
const initialEdges = [{ id: 'e1-2', source: '1', target: '2', label: 'This is a draggable edge' }];
|
||||
|
||||
const onInit = (reactFlowInstance: ReactFlowInstance) => reactFlowInstance.fitView();
|
||||
const onEdgeUpdateStart = (_: React.MouseEvent, edge: Edge) => console.log('start update', edge);
|
||||
const onEdgeUpdateEnd = (_: MouseEvent, edge: Edge) => console.log('end update', edge);
|
||||
|
||||
const UpdatableEdge = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const onEdgeUpdate = (oldEdge: Edge, newConnection: Connection) =>
|
||||
setElements((els) => updateEdge(oldEdge, newConnection, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||
|
||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||
console.log(changes);
|
||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||
}, []);
|
||||
|
||||
const onEdgesChange = useCallback((changes: EdgeChange[]) => {
|
||||
setEdges((es) => applyEdgeChanges(changes, es));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onInit={onInit}
|
||||
snapToGrid={true}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onConnect={onConnect}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
>
|
||||
<Controls />
|
||||
|
||||
@@ -1,64 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactFlow, { Elements } from 'react-flow-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'react-flow-renderer';
|
||||
|
||||
import './updatenode.css';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', data: { label: '-' }, position: { x: 100, y: 100 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2' },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [{ id: 'e1-2', source: '1', target: '2' }];
|
||||
|
||||
const UpdateNode = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const [nodeName, setNodeName] = useState<string>('Node 1');
|
||||
const [nodeBg, setNodeBg] = useState<string>('#eee');
|
||||
const [nodeHidden, setNodeHidden] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setElements((els) =>
|
||||
els.map((el) => {
|
||||
if (el.id === '1') {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
el.data = {
|
||||
...el.data,
|
||||
n.data = {
|
||||
...n.data,
|
||||
label: nodeName,
|
||||
};
|
||||
}
|
||||
|
||||
return el;
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeName, setElements]);
|
||||
}, [nodeName]);
|
||||
|
||||
useEffect(() => {
|
||||
setElements((els) =>
|
||||
els.map((el) => {
|
||||
if (el.id === '1') {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1') {
|
||||
// it's important that you create a new object here in order to notify react flow about the change
|
||||
el.style = { ...el.style, backgroundColor: nodeBg };
|
||||
n.style = { ...n.style, backgroundColor: nodeBg };
|
||||
}
|
||||
|
||||
return el;
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeBg, setElements]);
|
||||
}, [nodeBg]);
|
||||
|
||||
useEffect(() => {
|
||||
setElements((els) =>
|
||||
els.map((el) => {
|
||||
if (el.id === '1' || el.id === 'e1-2') {
|
||||
setNodes((nds) =>
|
||||
nds.map((n) => {
|
||||
if (n.id === '1' || n.id === 'e1-2') {
|
||||
// when you update a simple type you can just update the value
|
||||
el.isHidden = nodeHidden;
|
||||
n.hidden = nodeHidden;
|
||||
}
|
||||
|
||||
return el;
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, [nodeHidden, setElements]);
|
||||
}, [nodeHidden]);
|
||||
|
||||
return (
|
||||
<ReactFlow elements={elements} defaultZoom={1.5} minZoom={0.2} maxZoom={4}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultZoom={1.5}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<div className="updatenode__controls">
|
||||
<label>label:</label>
|
||||
<input value={nodeName} onChange={(evt) => setNodeName(evt.target.value)} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { memo, FC, useMemo, CSSProperties } from 'react';
|
||||
|
||||
import { Handle, Position, NodeProps } from 'react-flow-renderer';
|
||||
|
||||
const nodeStyles: CSSProperties = { padding: 10, border: '1px solid #ddd' };
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, { useState, useCallback, CSSProperties } from 'react';
|
||||
import { useCallback, CSSProperties } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
NodeTypesType,
|
||||
NodeTypes,
|
||||
addEdge,
|
||||
useZoomPanHelper,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
Elements,
|
||||
Node,
|
||||
Connection,
|
||||
Edge,
|
||||
ElementId,
|
||||
useUpdateNodeInternals,
|
||||
Position,
|
||||
isEdge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
import CustomNode from './CustomNode';
|
||||
|
||||
const initialHandleCount = 1;
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
@@ -28,23 +28,25 @@ const initialElements: Elements = [
|
||||
|
||||
const buttonWrapperStyles: CSSProperties = { position: 'absolute', right: 10, top: 10, zIndex: 10 };
|
||||
|
||||
const nodeTypes: NodeTypesType = {
|
||||
const nodeTypes: NodeTypes = {
|
||||
custom: CustomNode,
|
||||
};
|
||||
|
||||
let id = 5;
|
||||
const getId = (): ElementId => `${id++}`;
|
||||
const getId = (): string => `${id++}`;
|
||||
|
||||
const UpdateNodeInternalsFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const onConnect = (params: Edge | Connection) => setEdges((els) => addEdge(params, els));
|
||||
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const { project } = useZoomPanHelper();
|
||||
const { project } = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt) =>
|
||||
setElements((els) =>
|
||||
els.concat({
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: project({ x: evt.clientX, y: evt.clientY - 40 }),
|
||||
data: { label: 'new node' },
|
||||
@@ -56,25 +58,17 @@ const UpdateNodeInternalsFlow = () => {
|
||||
);
|
||||
|
||||
const toggleHandleCount = useCallback(() => {
|
||||
setElements((els) =>
|
||||
els.map((el) => {
|
||||
if (isEdge(el)) {
|
||||
return el;
|
||||
}
|
||||
|
||||
return { ...el, data: { ...el.data, handleCount: el.data?.handleCount === 1 ? 2 : 1 } };
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
return { ...node, data: { ...node.data, handleCount: node.data?.handleCount === 1 ? 2 : 1 } };
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleHandlePosition = useCallback(() => {
|
||||
setElements((els) =>
|
||||
els.map((el) => {
|
||||
if (isEdge(el)) {
|
||||
return el;
|
||||
}
|
||||
|
||||
return { ...el, data: { ...el.data, handlePosition: el.data?.handlePosition === 0 ? 1 : 0 } };
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
return { ...node, data: { ...node.data, handlePosition: node.data?.handlePosition === 0 ? 1 : 0 } };
|
||||
})
|
||||
);
|
||||
}, []);
|
||||
@@ -82,7 +76,15 @@ const UpdateNodeInternalsFlow = () => {
|
||||
const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]);
|
||||
|
||||
return (
|
||||
<ReactFlow elements={elements} nodeTypes={nodeTypes} onConnect={onConnect} onPaneClick={onPaneClick}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodeTypes={nodeTypes}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
>
|
||||
<div style={buttonWrapperStyles}>
|
||||
<button onClick={toggleHandleCount}>toggle handle count</button>
|
||||
<button onClick={toggleHandlePosition}>toggle handle position</button>
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import ReactFlow, {
|
||||
removeElements,
|
||||
Node,
|
||||
addEdge,
|
||||
Background,
|
||||
MiniMap,
|
||||
useZoomPanHelper,
|
||||
useReactFlow,
|
||||
ReactFlowProvider,
|
||||
Elements,
|
||||
ElementId,
|
||||
Connection,
|
||||
Edge,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
const initialElements: Elements = [
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 }, className: 'light' },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 }, className: 'light' },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 }, className: 'light' },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 }, className: 'light' },
|
||||
];
|
||||
|
||||
const initialEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
let id = 5;
|
||||
const getId = (): ElementId => `${id++}`;
|
||||
|
||||
const getId = () => `${id++}`;
|
||||
|
||||
const UseZoomPanHelperFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(params, els));
|
||||
const { project, setCenter, zoomIn, zoomOut } = useZoomPanHelper();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
const onConnect = (params: Connection | Edge) => setEdges((eds) => addEdge(params, eds));
|
||||
const { project, setCenter, zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
|
||||
const onPaneClick = useCallback(
|
||||
(evt) => {
|
||||
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
|
||||
|
||||
setElements((els) =>
|
||||
els.concat({
|
||||
setNodes((nds) =>
|
||||
nds.concat({
|
||||
id: getId(),
|
||||
position: projectedPosition,
|
||||
data: {
|
||||
@@ -48,25 +52,31 @@ const UseZoomPanHelperFlow = () => {
|
||||
[project]
|
||||
);
|
||||
|
||||
const onElementClick = useCallback(
|
||||
const onNodeClick = useCallback(
|
||||
(_, element) => {
|
||||
const { x, y } = element.position;
|
||||
setCenter(x, y, 1);
|
||||
setCenter(x, y, { zoom: 1, duration: 1200 });
|
||||
},
|
||||
[setCenter]
|
||||
);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementClick={onElementClick}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={onConnect}
|
||||
onPaneClick={onPaneClick}
|
||||
fitView
|
||||
fitViewOptions={{ duration: 1200, padding: 0.2 }}
|
||||
maxZoom={Infinity}
|
||||
>
|
||||
<div style={{ position: 'absolute', left: 0, top: 0, zIndex: 100 }}>
|
||||
<button onClick={() => zoomIn(1200)}>zoomIn</button>
|
||||
<button onClick={() => zoomOut(0)}>zoomOut</button>
|
||||
<button onClick={() => zoomIn({ duration: 1200 })}>zoomIn</button>
|
||||
<button onClick={() => zoomOut({ duration: 0 })}>zoomOut</button>
|
||||
<button onClick={() => fitView({ duration: 1200, padding: 0.3 })}>fitView</button>
|
||||
</div>
|
||||
<Background />
|
||||
<MiniMap />
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React, { useState, MouseEvent as ReactMouseEvent, FC } from 'react';
|
||||
import { MouseEvent as ReactMouseEvent, FC } from 'react';
|
||||
import ReactFlow, {
|
||||
addEdge,
|
||||
Handle,
|
||||
OnLoadParams,
|
||||
Connection,
|
||||
Position,
|
||||
Elements,
|
||||
Node,
|
||||
Edge,
|
||||
OnConnectStartParams,
|
||||
NodeProps,
|
||||
NodeTypesType,
|
||||
NodeTypes,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
} from 'react-flow-renderer';
|
||||
|
||||
import './validation.css';
|
||||
|
||||
const initialElements: Elements = [
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 } },
|
||||
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 } },
|
||||
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 } },
|
||||
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 } },
|
||||
const initialNodes: Node[] = [
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 }, data: null },
|
||||
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 }, data: null },
|
||||
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, data: null },
|
||||
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, data: null },
|
||||
];
|
||||
|
||||
const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView();
|
||||
const isValidConnection = (connection: Connection) => connection.target === 'B';
|
||||
const onConnectStart = (_: ReactMouseEvent, { nodeId, handleType }: OnConnectStartParams) =>
|
||||
console.log('on connect start', { nodeId, handleType });
|
||||
@@ -43,31 +43,36 @@ const CustomNode: FC<NodeProps> = ({ id }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
const nodeTypes: NodeTypesType = {
|
||||
const nodeTypes: NodeTypes = {
|
||||
custominput: CustomInput,
|
||||
customnode: CustomNode,
|
||||
};
|
||||
|
||||
const HorizontalFlow = () => {
|
||||
const [elements, setElements] = useState<Elements>(initialElements);
|
||||
const ValidationFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
const onConnect = (params: Connection | Edge) => {
|
||||
console.log('on connect', params);
|
||||
setElements((els) => addEdge(params, els));
|
||||
setEdges((eds) => addEdge(params, eds));
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
selectNodesOnDrag={false}
|
||||
onLoad={onLoad}
|
||||
className="validationflow"
|
||||
nodeTypes={nodeTypes}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectStop={onConnectStop}
|
||||
onConnectEnd={onConnectEnd}
|
||||
fitView
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default HorizontalFlow;
|
||||
export default ValidationFlow;
|
||||
|
||||
@@ -1,89 +1,106 @@
|
||||
import React, { ChangeEvent } from 'react';
|
||||
import { ChangeEvent } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter as Router, Route, Switch, withRouter } from 'react-router-dom';
|
||||
|
||||
import Overview from './Overview';
|
||||
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import Basic from './Basic';
|
||||
import CustomNode from './CustomNode';
|
||||
import Stress from './Stress';
|
||||
import Interaction from './Interaction';
|
||||
import Empty from './Empty';
|
||||
import Edges from './Edges';
|
||||
import Validation from './Validation';
|
||||
import Provider from './Provider';
|
||||
import Hidden from './Hidden';
|
||||
import EdgeTypes from './EdgeTypes';
|
||||
import ControlledUncontrolled from './ControlledUncontrolled';
|
||||
import CustomConnectionLine from './CustomConnectionLine';
|
||||
import CustomNode from './CustomNode';
|
||||
import DefaultNodes from './DefaultNodes';
|
||||
import DragHandle from './DragHandle';
|
||||
import DragNDrop from './DragNDrop';
|
||||
import Edges from './Edges';
|
||||
import EdgeTypes from './EdgeTypes';
|
||||
import Empty from './Empty';
|
||||
import FloatingEdges from './FloatingEdges';
|
||||
import Hidden from './Hidden';
|
||||
import './index.css';
|
||||
import Interaction from './Interaction';
|
||||
import Layouting from './Layouting';
|
||||
import MultiFlows from './MultiFlows';
|
||||
import NestedNodes from './NestedNodes';
|
||||
import NodeTypeChange from './NodeTypeChange';
|
||||
import NodeTypesObjectChange from './NodeTypesObjectChange';
|
||||
import Overview from './Overview';
|
||||
import Provider from './Provider';
|
||||
import SaveRestore from './SaveRestore';
|
||||
import Stress from './Stress';
|
||||
import Subflow from './Subflow';
|
||||
import SwitchFlow from './Switch';
|
||||
import TouchDevice from './TouchDevice';
|
||||
import Undirectional from './Undirectional';
|
||||
import UpdatableEdge from './UpdatableEdge';
|
||||
import UpdateNode from './UpdateNode';
|
||||
import SaveRestore from './SaveRestore';
|
||||
import DragNDrop from './DragNDrop';
|
||||
import Layout from './Layouting';
|
||||
import SwitchFlows from './Switch';
|
||||
import UseZoomPanHelper from './UseZoomPanHelper';
|
||||
import UseUpdateNodeInternals from './UseUpdateNodeInternals';
|
||||
import Undirectional from './Undirectional';
|
||||
import MultiFlows from './MultiFlows';
|
||||
import DragHandle from './DragHandle';
|
||||
import UseZoomPanHelper from './UseZoomPanHelper';
|
||||
import Validation from './Validation';
|
||||
|
||||
|
||||
import './index.css';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: Overview,
|
||||
},
|
||||
{
|
||||
path: '/edges',
|
||||
component: Edges,
|
||||
},
|
||||
{
|
||||
path: '/custom-node',
|
||||
component: CustomNode,
|
||||
},
|
||||
{
|
||||
path: '/validation',
|
||||
component: Validation,
|
||||
},
|
||||
{
|
||||
path: '/provider',
|
||||
component: Provider,
|
||||
},
|
||||
{
|
||||
path: '/stress',
|
||||
component: Stress,
|
||||
},
|
||||
{
|
||||
path: '/interaction',
|
||||
component: Interaction,
|
||||
},
|
||||
{
|
||||
path: '/basic',
|
||||
component: Basic,
|
||||
},
|
||||
{
|
||||
path: '/empty',
|
||||
component: Empty,
|
||||
},
|
||||
{
|
||||
path: '/hidden',
|
||||
component: Hidden,
|
||||
},
|
||||
{
|
||||
path: '/edge-types',
|
||||
component: EdgeTypes,
|
||||
path: '/default-nodes',
|
||||
component: DefaultNodes,
|
||||
},
|
||||
{
|
||||
path: '/custom-connectionline',
|
||||
component: CustomConnectionLine,
|
||||
},
|
||||
{
|
||||
path: '/custom-node',
|
||||
component: CustomNode,
|
||||
},
|
||||
{
|
||||
path: '/draghandle',
|
||||
component: DragHandle,
|
||||
},
|
||||
{
|
||||
path: '/dragndrop',
|
||||
component: DragNDrop,
|
||||
},
|
||||
{
|
||||
path: '/edges',
|
||||
component: Edges,
|
||||
},
|
||||
{
|
||||
path: '/edge-types',
|
||||
component: EdgeTypes,
|
||||
},
|
||||
{
|
||||
path: '/empty',
|
||||
component: Empty,
|
||||
},
|
||||
{
|
||||
path: '/floating-edges',
|
||||
component: FloatingEdges,
|
||||
},
|
||||
{
|
||||
path: '/hidden',
|
||||
component: Hidden,
|
||||
},
|
||||
{
|
||||
path: '/interaction',
|
||||
component: Interaction,
|
||||
},
|
||||
{
|
||||
path: '/layouting',
|
||||
component: Layouting,
|
||||
},
|
||||
{
|
||||
path: '/multiflows',
|
||||
component: MultiFlows,
|
||||
},
|
||||
{
|
||||
path: '/nested-nodes',
|
||||
component: NestedNodes,
|
||||
},
|
||||
{
|
||||
path: '/nodetype-change',
|
||||
component: NodeTypeChange,
|
||||
@@ -93,28 +110,40 @@ const routes = [
|
||||
component: NodeTypesObjectChange,
|
||||
},
|
||||
{
|
||||
path: '/updatable-edge',
|
||||
component: UpdatableEdge,
|
||||
},
|
||||
{
|
||||
path: '/update-node',
|
||||
component: UpdateNode,
|
||||
path: '/provider',
|
||||
component: Provider,
|
||||
},
|
||||
{
|
||||
path: '/save-restore',
|
||||
component: SaveRestore,
|
||||
},
|
||||
{
|
||||
path: '/drag-and-drop',
|
||||
component: DragNDrop,
|
||||
path: '/stress',
|
||||
component: Stress,
|
||||
},
|
||||
{
|
||||
path: '/layouting',
|
||||
component: Layout,
|
||||
path: '/subflow',
|
||||
component: Subflow,
|
||||
},
|
||||
{
|
||||
path: '/switch',
|
||||
component: SwitchFlows,
|
||||
component: SwitchFlow,
|
||||
},
|
||||
{
|
||||
path: '/touch-device',
|
||||
component: TouchDevice,
|
||||
},
|
||||
{
|
||||
path: '/undirectional',
|
||||
component: Undirectional,
|
||||
},
|
||||
{
|
||||
path: '/updatable-edge',
|
||||
component: UpdatableEdge,
|
||||
},
|
||||
{
|
||||
path: '/update-node',
|
||||
component: UpdateNode,
|
||||
},
|
||||
{
|
||||
path: '/usezoompanhelper',
|
||||
@@ -125,46 +154,44 @@ const routes = [
|
||||
component: UseUpdateNodeInternals,
|
||||
},
|
||||
{
|
||||
path: '/undirectional',
|
||||
component: Undirectional,
|
||||
path: '/validation',
|
||||
component: Validation,
|
||||
},
|
||||
{
|
||||
path: '/multiflows',
|
||||
component: MultiFlows,
|
||||
},
|
||||
{
|
||||
path: '/draghandle',
|
||||
component: DragHandle,
|
||||
path: '/controlled-uncontrolled',
|
||||
component: ControlledUncontrolled,
|
||||
},
|
||||
];
|
||||
|
||||
const Header = withRouter(({ history, location }) => {
|
||||
const onChange = (event: ChangeEvent<HTMLSelectElement>) => history.push(event.target.value);
|
||||
const Header = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const onChange = (event: ChangeEvent<HTMLSelectElement>) => navigate(event.target.value);
|
||||
|
||||
return (
|
||||
<header>
|
||||
<a className="logo" href="https://github.com/wbkd/react-flow">
|
||||
React Flow Dev
|
||||
</a>
|
||||
<select defaultValue={location.pathname} onChange={onChange}>
|
||||
{routes.map((route) => (
|
||||
<option value={route.path} key={route.path}>
|
||||
{route.path === '/' ? 'overview' : route.path.substr(1, route.path.length)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</header>
|
||||
<header>
|
||||
<a className="logo" href="https://github.com/wbkd/react-flow">
|
||||
React Flow Dev
|
||||
</a>
|
||||
<select defaultValue={location.pathname} onChange={onChange}>
|
||||
{routes.map((route) => (
|
||||
<option value={route.path} key={route.path}>
|
||||
{route.path === '/' ? 'overview' : route.path.substring(1, route.path.length)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</header>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<Router forceRefresh={true}>
|
||||
<BrowserRouter>
|
||||
<Header />
|
||||
<Switch>
|
||||
<Routes>
|
||||
{routes.map((route) => (
|
||||
<Route exact path={route.path} render={() => <route.component />} key={route.path} />
|
||||
<Route path={route.path} key={route.path} element={<route.component />}/>
|
||||
))}
|
||||
</Switch>
|
||||
</Router>,
|
||||
</Routes>
|
||||
</BrowserRouter>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -20,7 +16,6 @@
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"],
|
||||
"exclude": ["src_oldapi"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"internal": true,
|
||||
"main": "../dist/nocss/ReactFlow-nocss.js",
|
||||
"module": "../dist/nocss/ReactFlow-nocss.esm.js",
|
||||
"type": "module",
|
||||
"main": "../dist/nocss/index.js",
|
||||
"module": "../dist/nocss/index.js",
|
||||
"types": "../dist/nocss/index.d.ts"
|
||||
}
|
||||
|
||||
1670
package-lock.json
generated
1670
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "react-flow-renderer",
|
||||
"version": "9.7.4",
|
||||
"version": "10.0.0-next.51",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"main": "dist/ReactFlow.js",
|
||||
"module": "dist/ReactFlow.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"main": "dist/esm/index.js",
|
||||
"module": "dist/esm/index.js",
|
||||
"types": "dist/esm/index.d.ts",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,51 +29,48 @@
|
||||
"cy:open": "cypress open",
|
||||
"release": "npm run test && release-it",
|
||||
"release:notest": "release-it",
|
||||
"release:next": "release-it --preRelease=next"
|
||||
"release:next": "release-it --preRelease=next --no-git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.7",
|
||||
"@babel/runtime": "^7.17.2",
|
||||
"classcat": "^5.0.3",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"react-draggable": "^4.4.4",
|
||||
"react-redux": "^7.2.6",
|
||||
"redux": "^4.1.2"
|
||||
"zustand": "^3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.12",
|
||||
"@babel/core": "^7.17.5",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-commonjs": "^21.0.1",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-commonjs": "^21.0.2",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"@rollup/plugin-replace": "^3.0.1",
|
||||
"@rollup/plugin-replace": "^4.0.0",
|
||||
"@svgr/rollup": "^6.2.1",
|
||||
"@types/d3": "^7.1.0",
|
||||
"@types/react-redux": "^7.1.22",
|
||||
"@types/redux": "^3.6.31",
|
||||
"@types/resize-observer-browser": "^0.1.6",
|
||||
"@types/react": "^17.0.40",
|
||||
"@types/resize-observer-browser": "^0.1.7",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-preset-react-app": "^10.0.1",
|
||||
"cypress": "^9.4.1",
|
||||
"postcss": "^8.4.5",
|
||||
"cypress": "^9.5.1",
|
||||
"postcss": "^8.4.8",
|
||||
"postcss-cli": "^9.1.0",
|
||||
"postcss-nested": "^5.0.6",
|
||||
"prettier": "2.5.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"release-it": "^14.12.4",
|
||||
"rollup": "^2.66.1",
|
||||
"rollup-plugin-bundle-size": "^1.0.3",
|
||||
"release-it": "^14.12.5",
|
||||
"rollup": "^2.70.1",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-postcss": "^4.0.2",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-typescript2": "^0.31.1",
|
||||
"rollup-plugin-typescript2": "^0.31.2",
|
||||
"rollup-plugin-visualizer": "^5.6.0",
|
||||
"start-server-and-test": "^1.14.0",
|
||||
"typescript": "^4.5.4"
|
||||
"typescript": "^4.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "16 || 17",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [require('autoprefixer'), require('postcss-nested')],
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
trailingComma: 'es5',
|
||||
singleQuote: true,
|
||||
printWidth: 120
|
||||
};
|
||||
@@ -2,11 +2,11 @@ import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import bundleSize from 'rollup-plugin-bundle-size';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import svgr from '@svgr/rollup';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import { DEFAULT_EXTENSIONS as DEFAULT_BABEL_EXTENSIONS } from '@babel/core';
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
import pkg from './package.json';
|
||||
|
||||
@@ -14,38 +14,51 @@ const isProd = process.env.NODE_ENV === 'production';
|
||||
const isTesting = process.env.NODE_ENV === 'testing';
|
||||
const processEnv = isProd || isTesting ? 'production' : 'development';
|
||||
|
||||
export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injectCSS = true } = {}) => ({
|
||||
input: 'src/index.ts',
|
||||
external: ['react', 'react-dom', (id) => id.includes('@babel/runtime')],
|
||||
export const baseConfig = ({ outputDir = 'dist/esm', injectCSS = true } = {}) => ({
|
||||
input: [
|
||||
'src/index.ts',
|
||||
'src/additional-components/Controls/index.tsx',
|
||||
'src/additional-components/Background/index.tsx',
|
||||
'src/additional-components/MiniMap/index.tsx',
|
||||
'src/hooks/useReactFlow.ts',
|
||||
'src/hooks/useNodes.ts',
|
||||
'src/hooks/useEdges.ts',
|
||||
'src/hooks/useViewport.ts',
|
||||
'src/hooks/useUpdateNodeInternals.ts',
|
||||
],
|
||||
external: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'classcat',
|
||||
'd3-selection',
|
||||
'd3-zoom',
|
||||
'react-draggable',
|
||||
'zustand',
|
||||
'zustand/shallow',
|
||||
'zustand/context',
|
||||
(id) => id.includes('@babel/runtime'),
|
||||
],
|
||||
onwarn(warning, rollupWarn) {
|
||||
if (warning.code !== 'CIRCULAR_DEPENDENCY') {
|
||||
rollupWarn(warning);
|
||||
}
|
||||
},
|
||||
output: [
|
||||
{
|
||||
file: mainFile,
|
||||
format: 'cjs',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
},
|
||||
{
|
||||
file: moduleFile,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
exports: 'named',
|
||||
},
|
||||
],
|
||||
output: {
|
||||
dir: outputDir,
|
||||
format: 'esm',
|
||||
sourcemap: true,
|
||||
},
|
||||
|
||||
plugins: [
|
||||
replace({
|
||||
__ENV__: JSON.stringify(processEnv),
|
||||
__REACT_FLOW_VERSION__: JSON.stringify(pkg.version),
|
||||
__INJECT_STYLES__: injectCSS,
|
||||
preventAssignment: true,
|
||||
}),
|
||||
bundleSize(),
|
||||
postcss({
|
||||
minimize: isProd,
|
||||
inject: injectCSS,
|
||||
inject: false,
|
||||
}),
|
||||
svgr(),
|
||||
resolve(),
|
||||
@@ -60,6 +73,7 @@ export const baseConfig = ({ mainFile = pkg.main, moduleFile = pkg.module, injec
|
||||
exclude: 'node_modules/**',
|
||||
babelHelpers: 'runtime',
|
||||
}),
|
||||
visualizer(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -67,8 +81,7 @@ export default isProd && !isTesting
|
||||
? [
|
||||
baseConfig(),
|
||||
baseConfig({
|
||||
mainFile: 'dist/nocss/ReactFlow-nocss.js',
|
||||
moduleFile: 'dist/nocss/ReactFlow-nocss.esm.js',
|
||||
outputDir: 'dist/nocss',
|
||||
injectCSS: false,
|
||||
}),
|
||||
]
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
import React, { memo, useMemo, FC, HTMLAttributes } from 'react';
|
||||
import React, { memo, useMemo, FC } from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import { BackgroundVariant } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
import { createGridLinesPath, createGridDotsPath } from './utils';
|
||||
|
||||
export interface BackgroundProps extends HTMLAttributes<SVGElement> {
|
||||
variant?: BackgroundVariant;
|
||||
gap?: number;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}
|
||||
import { BackgroundVariant, ReactFlowState, BackgroundProps } from '../../types';
|
||||
|
||||
const defaultColors = {
|
||||
[BackgroundVariant.Dots]: '#81818a',
|
||||
[BackgroundVariant.Lines]: '#eee',
|
||||
};
|
||||
|
||||
const transformSelector = (s: ReactFlowState) => s.transform;
|
||||
|
||||
const Background: FC<BackgroundProps> = ({
|
||||
variant = BackgroundVariant.Dots,
|
||||
gap = 15,
|
||||
@@ -25,11 +20,11 @@ const Background: FC<BackgroundProps> = ({
|
||||
style,
|
||||
className,
|
||||
}) => {
|
||||
const [x, y, scale] = useStoreState((s) => s.transform);
|
||||
const [x, y, scale] = useStore(transformSelector);
|
||||
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
|
||||
const patternId = useMemo(() => `pattern-${Math.floor(Math.random() * 100000)}`, []);
|
||||
|
||||
const bgClasses = cc(['react-flow__background', className]);
|
||||
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
|
||||
const scaledGap = gap * scale;
|
||||
const xOffset = x % scaledGap;
|
||||
const yOffset = y % scaledGap;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { memo, useCallback, HTMLAttributes, FC, useEffect, useState } from 'react';
|
||||
import React, { memo, useCallback, FC, useEffect, useState } from 'react';
|
||||
import cc from 'classcat';
|
||||
|
||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import useReactFlow from '../../hooks/useReactFlow';
|
||||
|
||||
import PlusIcon from '../../../assets/icons/plus.svg';
|
||||
import MinusIcon from '../../../assets/icons/minus.svg';
|
||||
@@ -9,21 +10,7 @@ import FitviewIcon from '../../../assets/icons/fitview.svg';
|
||||
import LockIcon from '../../../assets/icons/lock.svg';
|
||||
import UnlockIcon from '../../../assets/icons/unlock.svg';
|
||||
|
||||
import useZoomPanHelper from '../../hooks/useZoomPanHelper';
|
||||
import { FitViewParams } from '../../types';
|
||||
|
||||
export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
|
||||
showZoom?: boolean;
|
||||
showFitView?: boolean;
|
||||
showInteractive?: boolean;
|
||||
fitViewParams?: FitViewParams;
|
||||
onZoomIn?: () => void;
|
||||
onZoomOut?: () => void;
|
||||
onFitView?: () => void;
|
||||
onInteractiveChange?: (interactiveStatus: boolean) => void;
|
||||
}
|
||||
|
||||
export interface ControlButtonProps extends HTMLAttributes<HTMLButtonElement> {}
|
||||
import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
|
||||
|
||||
export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => (
|
||||
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
|
||||
@@ -31,12 +18,14 @@ export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...
|
||||
</button>
|
||||
);
|
||||
|
||||
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
|
||||
|
||||
const Controls: FC<ControlProps> = ({
|
||||
style,
|
||||
showZoom = true,
|
||||
showFitView = true,
|
||||
showInteractive = true,
|
||||
fitViewParams,
|
||||
fitViewOptions,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
onFitView,
|
||||
@@ -44,11 +33,11 @@ const Controls: FC<ControlProps> = ({
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
const store = useStoreApi();
|
||||
const [isVisible, setIsVisible] = useState<boolean>(false);
|
||||
const setInteractive = useStoreActions((actions) => actions.setInteractive);
|
||||
const { zoomIn, zoomOut, fitView } = useZoomPanHelper();
|
||||
const isInteractive = useStore(isInteractiveSelector);
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
|
||||
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
|
||||
const mapClasses = cc(['react-flow__controls', className]);
|
||||
|
||||
const onZoomInHandler = useCallback(() => {
|
||||
@@ -62,14 +51,19 @@ const Controls: FC<ControlProps> = ({
|
||||
}, [zoomOut, onZoomOut]);
|
||||
|
||||
const onFitViewHandler = useCallback(() => {
|
||||
fitView?.(fitViewParams);
|
||||
fitView?.(fitViewOptions);
|
||||
onFitView?.();
|
||||
}, [fitView, fitViewParams, onFitView]);
|
||||
}, [fitView, fitViewOptions, onFitView]);
|
||||
|
||||
const onInteractiveChangeHandler = useCallback(() => {
|
||||
setInteractive?.(!isInteractive);
|
||||
store.setState({
|
||||
nodesDraggable: !isInteractive,
|
||||
nodesConnectable: !isInteractive,
|
||||
elementsSelectable: !isInteractive,
|
||||
});
|
||||
|
||||
onInteractiveChange?.(!isInteractive);
|
||||
}, [isInteractive, setInteractive, onInteractiveChange]);
|
||||
}, [isInteractive, onInteractiveChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsVisible(true);
|
||||
@@ -83,21 +77,41 @@ const Controls: FC<ControlProps> = ({
|
||||
<div className={mapClasses} style={style}>
|
||||
{showZoom && (
|
||||
<>
|
||||
<ControlButton onClick={onZoomInHandler} className="react-flow__controls-zoomin">
|
||||
<ControlButton
|
||||
onClick={onZoomInHandler}
|
||||
className="react-flow__controls-zoomin"
|
||||
title="zoom in"
|
||||
aria-label="zoom in"
|
||||
>
|
||||
<PlusIcon />
|
||||
</ControlButton>
|
||||
<ControlButton onClick={onZoomOutHandler} className="react-flow__controls-zoomout">
|
||||
<ControlButton
|
||||
onClick={onZoomOutHandler}
|
||||
className="react-flow__controls-zoomout"
|
||||
title="zoom out"
|
||||
aria-label="zoom out"
|
||||
>
|
||||
<MinusIcon />
|
||||
</ControlButton>
|
||||
</>
|
||||
)}
|
||||
{showFitView && (
|
||||
<ControlButton className="react-flow__controls-fitview" onClick={onFitViewHandler}>
|
||||
<ControlButton
|
||||
className="react-flow__controls-fitview"
|
||||
onClick={onFitViewHandler}
|
||||
title="fit view"
|
||||
aria-label="fit view"
|
||||
>
|
||||
<FitviewIcon />
|
||||
</ControlButton>
|
||||
)}
|
||||
{showInteractive && (
|
||||
<ControlButton className="react-flow__controls-interactive" onClick={onInteractiveChangeHandler}>
|
||||
<ControlButton
|
||||
className="react-flow__controls-interactive"
|
||||
onClick={onInteractiveChangeHandler}
|
||||
title="toggle interactivity"
|
||||
aria-label="toggle interactivity"
|
||||
>
|
||||
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
||||
</ControlButton>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ const MiniMapNode = ({
|
||||
strokeWidth,
|
||||
className,
|
||||
borderRadius,
|
||||
shapeRendering
|
||||
shapeRendering,
|
||||
}: MiniMapNodeProps) => {
|
||||
const { background, backgroundColor } = style || {};
|
||||
const fill = (color || background || backgroundColor) as string;
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import React, { memo, HTMLAttributes } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import { getRectOfNodes, getBoundsofRects } from '../../utils/graph';
|
||||
import { Node, Rect } from '../../types';
|
||||
import MiniMapNode from './MiniMapNode';
|
||||
|
||||
type StringFunc = (node: Node) => string;
|
||||
import { useStore } from '../../store';
|
||||
import { getRectOfNodes } from '../../utils/graph';
|
||||
import { getBoundsofRects } from '../../utils';
|
||||
|
||||
export interface MiniMapProps extends HTMLAttributes<SVGSVGElement> {
|
||||
nodeColor?: string | StringFunc;
|
||||
nodeStrokeColor?: string | StringFunc;
|
||||
nodeClassName?: string | StringFunc;
|
||||
nodeBorderRadius?: number;
|
||||
nodeStrokeWidth?: number;
|
||||
maskColor?: string;
|
||||
}
|
||||
import { MiniMapProps, GetMiniMapNodeAttribute, ReactFlowState, Rect } from '../../types';
|
||||
|
||||
declare const window: any;
|
||||
|
||||
const defaultWidth = 200;
|
||||
const defaultHeight = 150;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
transform: s.transform,
|
||||
nodeInternals: s.nodeInternals,
|
||||
});
|
||||
|
||||
const MiniMap = ({
|
||||
style,
|
||||
className,
|
||||
@@ -32,20 +32,22 @@ const MiniMap = ({
|
||||
nodeStrokeWidth = 2,
|
||||
maskColor = 'rgb(240, 242, 243, 0.7)',
|
||||
}: MiniMapProps) => {
|
||||
const containerWidth = useStoreState((s) => s.width);
|
||||
const containerHeight = useStoreState((s) => s.height);
|
||||
const [tX, tY, tScale] = useStoreState((s) => s.transform);
|
||||
const nodes = useStoreState((s) => s.nodes);
|
||||
const { width: containerWidth, height: containerHeight, transform, nodeInternals } = useStore(selector, shallow);
|
||||
const [tX, tY, tScale] = transform;
|
||||
|
||||
const mapClasses = cc(['react-flow__minimap', className]);
|
||||
const elementWidth = (style?.width || defaultWidth)! as number;
|
||||
const elementHeight = (style?.height || defaultHeight)! as number;
|
||||
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
|
||||
const nodeStrokeColorFunc = (nodeStrokeColor instanceof Function
|
||||
? nodeStrokeColor
|
||||
: () => nodeStrokeColor) as StringFunc;
|
||||
const nodeClassNameFunc = (nodeClassName instanceof Function ? nodeClassName : () => nodeClassName) as StringFunc;
|
||||
const hasNodes = nodes && nodes.length;
|
||||
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as GetMiniMapNodeAttribute;
|
||||
const nodeStrokeColorFunc = (
|
||||
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor
|
||||
) as GetMiniMapNodeAttribute;
|
||||
const nodeClassNameFunc = (
|
||||
nodeClassName instanceof Function ? nodeClassName : () => nodeClassName
|
||||
) as GetMiniMapNodeAttribute;
|
||||
const hasNodes = nodeInternals && nodeInternals.size > 0;
|
||||
// @TODO: work with nodeInternals instead of converting it to an array
|
||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
||||
const bb = getRectOfNodes(nodes);
|
||||
const viewBB: Rect = {
|
||||
x: -tX / tScale,
|
||||
@@ -64,7 +66,7 @@ const MiniMap = ({
|
||||
const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset;
|
||||
const width = viewWidth + offset * 2;
|
||||
const height = viewHeight + offset * 2;
|
||||
const shapeRendering = (typeof window === "undefined" || !!window.chrome) ? "crispEdges" : "geometricPrecision";
|
||||
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
|
||||
|
||||
return (
|
||||
<svg
|
||||
@@ -74,24 +76,28 @@ const MiniMap = ({
|
||||
style={style}
|
||||
className={mapClasses}
|
||||
>
|
||||
{nodes
|
||||
.filter((node) => !node.isHidden)
|
||||
.map((node) => (
|
||||
<MiniMapNode
|
||||
key={node.id}
|
||||
x={node.__rf.position.x}
|
||||
y={node.__rf.position.y}
|
||||
width={node.__rf.width}
|
||||
height={node.__rf.height}
|
||||
style={node.style}
|
||||
className={nodeClassNameFunc(node)}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
/>
|
||||
))}
|
||||
{Array.from(nodeInternals)
|
||||
.filter(([_, node]) => !node.hidden && node.width && node.height)
|
||||
.map(([_, node]) => {
|
||||
const positionAbsolute = nodeInternals.get(node.id)?.positionAbsolute;
|
||||
|
||||
return (
|
||||
<MiniMapNode
|
||||
key={node.id}
|
||||
x={positionAbsolute?.x || 0}
|
||||
y={positionAbsolute?.y || 0}
|
||||
width={node.width!}
|
||||
height={node.height!}
|
||||
style={node.style}
|
||||
className={nodeClassNameFunc(node)}
|
||||
color={nodeColorFunc(node)}
|
||||
borderRadius={nodeBorderRadius}
|
||||
strokeColor={nodeStrokeColorFunc(node)}
|
||||
strokeWidth={nodeStrokeWidth}
|
||||
shapeRendering={shapeRendering}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<path
|
||||
className="react-flow__minimap-mask"
|
||||
d={`M${x - offset},${y - offset}h${width + offset * 2}v${height + offset * 2}h${-width - offset * 2}z
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { initialState } from '../../store';
|
||||
import configureStore from '../../store/configure-store';
|
||||
import { Provider, createStore } from '../../store';
|
||||
|
||||
const ReactFlowProvider: FC = ({ children }) => {
|
||||
const store = useMemo(() => {
|
||||
return configureStore(initialState);
|
||||
}, []);
|
||||
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
};
|
||||
const ReactFlowProvider: FC = ({ children }) => <Provider createStore={createStore}>{children}</Provider>;
|
||||
|
||||
ReactFlowProvider.displayName = 'ReactFlowProvider';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// These components are not used by React Flow directly
|
||||
// but the user can add them as children of a React Flow component
|
||||
// They can be added as children of a React Flow component
|
||||
|
||||
export { default as MiniMap } from './MiniMap';
|
||||
export { default as Controls, ControlButton } from './Controls';
|
||||
|
||||
33
src/components/Attribution/index.tsx
Normal file
33
src/components/Attribution/index.tsx
Normal 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;
|
||||
@@ -1,38 +1,39 @@
|
||||
import React, { useEffect, useState, CSSProperties } from 'react';
|
||||
import React, { useRef, CSSProperties } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../store';
|
||||
import { getBezierPath } from '../Edges/BezierEdge';
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
|
||||
import {
|
||||
ElementId,
|
||||
Node,
|
||||
Transform,
|
||||
HandleElement,
|
||||
Position,
|
||||
ConnectionLineType,
|
||||
ConnectionLineComponent,
|
||||
HandleType,
|
||||
Node,
|
||||
ReactFlowState,
|
||||
Position,
|
||||
} from '../../types';
|
||||
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
|
||||
|
||||
interface ConnectionLineProps {
|
||||
connectionNodeId: ElementId;
|
||||
connectionHandleId: ElementId | null;
|
||||
connectionNodeId: string;
|
||||
connectionHandleId: string | null;
|
||||
connectionHandleType: HandleType;
|
||||
connectionPositionX: number;
|
||||
connectionPositionY: number;
|
||||
connectionLineType: ConnectionLineType;
|
||||
nodes: Node[];
|
||||
transform: Transform;
|
||||
isConnectable: boolean;
|
||||
connectionLineStyle?: CSSProperties;
|
||||
CustomConnectionLineComponent?: ConnectionLineComponent;
|
||||
}
|
||||
|
||||
const getSourceHandle = (handleId: ElementId | null, sourceNode: Node, connectionHandleType: HandleType) => {
|
||||
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
|
||||
const handleBound =
|
||||
sourceNode.__rf.handleBounds[connectionHandleType] || sourceNode.__rf.handleBounds[handleTypeInverted];
|
||||
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
|
||||
|
||||
return handleId ? handleBound.find((d: HandleElement) => d.id === handleId) : handleBound[0];
|
||||
const getSourceHandle = (handleId: string | null, sourceNode: Node, connectionHandleType: HandleType) => {
|
||||
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
|
||||
const handleBound = sourceNode.handleBounds?.[connectionHandleType] || sourceNode.handleBounds?.[handleTypeInverted];
|
||||
|
||||
return handleId ? handleBound?.find((d: HandleElement) => d.id === handleId) : handleBound?.[0];
|
||||
};
|
||||
|
||||
export default ({
|
||||
@@ -43,29 +44,29 @@ export default ({
|
||||
connectionPositionX,
|
||||
connectionPositionY,
|
||||
connectionLineType = ConnectionLineType.Bezier,
|
||||
nodes = [],
|
||||
transform,
|
||||
isConnectable,
|
||||
CustomConnectionLineComponent,
|
||||
}: ConnectionLineProps) => {
|
||||
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||
const nodeId = connectionNodeId;
|
||||
const handleId = connectionHandleId;
|
||||
|
||||
useEffect(() => {
|
||||
const nextSourceNode = nodes.find((n) => n.id === nodeId) || null;
|
||||
setSourceNode(nextSourceNode);
|
||||
}, []);
|
||||
const { nodeInternals, transform } = useStore(selector, shallow);
|
||||
const sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
|
||||
|
||||
if (!sourceNode || !isConnectable) {
|
||||
if (
|
||||
!sourceNode.current ||
|
||||
!sourceNode.current ||
|
||||
!isConnectable ||
|
||||
!sourceNode.current.handleBounds?.[connectionHandleType]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceHandle = getSourceHandle(handleId, sourceNode, connectionHandleType);
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
|
||||
const sourceX = sourceNode.__rf.position.x + sourceHandleX;
|
||||
const sourceY = sourceNode.__rf.position.y + sourceHandleY;
|
||||
const sourceHandle = getSourceHandle(handleId, sourceNode.current, connectionHandleType);
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (sourceNode.current?.width ?? 0) / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.current?.height ?? 0;
|
||||
const sourceX = (sourceNode.current.positionAbsolute?.x || 0) + sourceHandleX;
|
||||
const sourceY = (sourceNode.current.positionAbsolute?.y || 0) + sourceHandleY;
|
||||
|
||||
const targetX = (connectionPositionX - transform[0]) / transform[2];
|
||||
const targetY = (connectionPositionY - transform[1]) / transform[2];
|
||||
@@ -85,7 +86,7 @@ export default ({
|
||||
targetPosition={targetPosition}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
sourceNode={sourceNode}
|
||||
sourceNode={sourceNode.current as Node}
|
||||
sourceHandle={sourceHandle}
|
||||
/>
|
||||
</g>
|
||||
@@ -94,34 +95,26 @@ export default ({
|
||||
|
||||
let dAttr: string = '';
|
||||
|
||||
const pathParams = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
};
|
||||
|
||||
if (connectionLineType === ConnectionLineType.Bezier) {
|
||||
dAttr = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
dAttr = getBezierPath(pathParams);
|
||||
} else if (connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
...pathParams,
|
||||
borderRadius: 0,
|
||||
});
|
||||
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
|
||||
dAttr = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
dAttr = getSmoothStepPath(pathParams);
|
||||
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
|
||||
dAttr = getSimpleBezierPath(pathParams);
|
||||
} else {
|
||||
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
39
src/components/Edges/BaseEdge.tsx
Normal file
39
src/components/Edges/BaseEdge.tsx
Normal 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}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,98 +1,111 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
|
||||
import { getMarkerEnd, getCenter } from './utils';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
|
||||
interface GetBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
centerX,
|
||||
centerY,
|
||||
}: GetBezierPathParams): string {
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
|
||||
const leftAndRight = [Position.Left, Position.Right];
|
||||
|
||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
||||
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
|
||||
|
||||
let path = `M${sourceX},${sourceY} C${sourceX},${cY} ${targetX},${cY} ${targetX},${targetY}`;
|
||||
|
||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} C${cX},${sourceY} ${cX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
path = `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
path = `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
}: EdgeProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
const path = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path style={style} d={path} className="react-flow__edge-path" markerEnd={markerEnd} />
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
import React, { memo } from 'react';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
import { getCenter } from './utils';
|
||||
|
||||
export interface GetBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
curvature?: number;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
centerX,
|
||||
centerY,
|
||||
}: GetBezierPathParams): string {
|
||||
const leftAndRight = [Position.Left, Position.Right];
|
||||
const hasCurvature = curvature > 0;
|
||||
let cX,
|
||||
cY = 0;
|
||||
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
|
||||
|
||||
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) {
|
||||
cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
||||
const distanceX = targetX - sourceX;
|
||||
const absDistanceX = Math.abs(Math.min(0, distanceX));
|
||||
const amtX = (Math.sqrt(absDistanceX) / 2) * (50 * curvature);
|
||||
|
||||
const hx1 = hasCurvature && distanceX < 0 ? sourceX + amtX : cX;
|
||||
const hx2 = hasCurvature && distanceX < 0 ? targetX - amtX : cX;
|
||||
|
||||
return `M${sourceX},${sourceY} C${hx1},${sourceY} ${hx2},${targetY}, ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
return `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
return `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
cY = typeof centerY !== 'undefined' ? centerY : _centerY;
|
||||
const distanceY = targetY - sourceY;
|
||||
|
||||
const absDistanceY = Math.abs(Math.min(0, distanceY));
|
||||
const amtY = (Math.sqrt(absDistanceY) / 2) * (50 * curvature);
|
||||
|
||||
const hy1 = hasCurvature && distanceY < 0 ? sourceY + amtY : cY;
|
||||
const hy2 = hasCurvature && distanceY < 0 ? targetY - amtY : cY;
|
||||
|
||||
return `M${sourceX},${sourceY} C${sourceX},${hy1} ${targetX},${hy2} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
export default memo(
|
||||
({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
label,
|
||||
labelStyle,
|
||||
labelShowBg,
|
||||
labelBgStyle,
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
curvature,
|
||||
}: EdgeProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
const path = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
curvature,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
12
src/components/Edges/SimpleBezierEdge.tsx
Normal file
12
src/components/Edges/SimpleBezierEdge.tsx
Normal 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} />;
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd, getCenter } from './utils';
|
||||
import { getCenter } from './utils';
|
||||
import { EdgeSmoothStepProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
|
||||
// These are some helper methods for drawing the round corners
|
||||
// The name indicates the direction of the path. "bottomLeftCorner" goes
|
||||
@@ -21,7 +21,7 @@ const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${
|
||||
const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}`;
|
||||
const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}`;
|
||||
|
||||
interface GetSmoothStepPathParams {
|
||||
export interface GetSmoothStepPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
@@ -130,8 +130,8 @@ export default memo(
|
||||
style,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetPosition = Position.Top,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
borderRadius = 5,
|
||||
}: EdgeSmoothStepProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
@@ -146,26 +146,21 @@ export default memo(
|
||||
borderRadius,
|
||||
});
|
||||
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path style={style} className="react-flow__edge-path" d={path} markerEnd={markerEnd} />
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
import { getMarkerEnd } from './utils';
|
||||
import BaseEdge from './BaseEdge';
|
||||
import { EdgeProps } from '../../types';
|
||||
|
||||
export default memo(
|
||||
@@ -17,39 +16,31 @@ export default memo(
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
markerEndId,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
}: EdgeProps) => {
|
||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||
|
||||
const xOffset = Math.abs(targetX - sourceX) / 2;
|
||||
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
|
||||
const markerEnd = getMarkerEnd(arrowHeadType, markerEndId);
|
||||
const path = `M ${sourceX},${sourceY}L ${targetX},${targetY}`;
|
||||
|
||||
const text = label ? (
|
||||
<EdgeText
|
||||
x={centerX}
|
||||
y={centerY}
|
||||
return (
|
||||
<BaseEdge
|
||||
path={path}
|
||||
centerX={centerX}
|
||||
centerY={centerY}
|
||||
label={label}
|
||||
labelStyle={labelStyle}
|
||||
labelShowBg={labelShowBg}
|
||||
labelBgStyle={labelBgStyle}
|
||||
labelBgPadding={labelBgPadding}
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
style={style}
|
||||
markerEnd={markerEnd}
|
||||
markerStart={markerStart}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
style={style}
|
||||
className="react-flow__edge-path"
|
||||
d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
{text}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as BezierEdge } from './BezierEdge';
|
||||
export { default as StepEdge } from './StepEdge';
|
||||
export { default as SimpleBezierEdge } from './SimpleBezierEdge';
|
||||
export { default as SmoothStepEdge } from './SmoothStepEdge';
|
||||
export { default as StepEdge } from './StepEdge';
|
||||
export { default as StraightEdge } from './StraightEdge';
|
||||
export { default as BezierEdge } from './BezierEdge';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ArrowHeadType, Position } from '../../types';
|
||||
import { MarkerType, Position } from '../../types';
|
||||
|
||||
export const getMarkerEnd = (arrowHeadType?: ArrowHeadType, markerEndId?: string): string => {
|
||||
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
|
||||
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
||||
return `url(#${markerEndId})`;
|
||||
}
|
||||
|
||||
return typeof arrowHeadType !== 'undefined' ? `url(#react-flow__${arrowHeadType})` : 'none';
|
||||
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
|
||||
};
|
||||
|
||||
export interface GetCenterParams {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import React, { memo, ComponentType, useCallback, useState, useMemo } from 'react';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { Edge, EdgeProps, WrapEdgeProps } from '../../types';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { Edge, EdgeProps, WrapEdgeProps, ReactFlowState, Connection } from '../../types';
|
||||
import { onMouseDown } from '../../components/Handle/handler';
|
||||
import { EdgeAnchor } from './EdgeAnchor';
|
||||
import { getMarkerId } from '../../utils/graph';
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
addSelectedEdges: s.addSelectedEdges,
|
||||
connectionMode: s.connectionMode,
|
||||
});
|
||||
|
||||
export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const EdgeWrapper = ({
|
||||
@@ -23,7 +30,6 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
labelBgPadding,
|
||||
labelBgBorderRadius,
|
||||
style,
|
||||
arrowHeadType,
|
||||
source,
|
||||
target,
|
||||
sourceX,
|
||||
@@ -33,29 +39,27 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
elementsSelectable,
|
||||
markerEndId,
|
||||
isHidden,
|
||||
hidden,
|
||||
sourceHandleId,
|
||||
targetHandleId,
|
||||
handleEdgeUpdate,
|
||||
onConnectEdge,
|
||||
onContextMenu,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
edgeUpdaterRadius,
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
markerEnd,
|
||||
markerStart,
|
||||
}: WrapEdgeProps): JSX.Element | null => {
|
||||
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
|
||||
const connectionMode = useStoreState((state) => state.connectionMode);
|
||||
const store = useStoreApi();
|
||||
const { addSelectedEdges, connectionMode } = useStore(selector, shallow);
|
||||
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
|
||||
const inactive = !elementsSelectable && !onClick;
|
||||
const handleEdgeUpdate = typeof onEdgeUpdate !== 'undefined';
|
||||
const edgeClasses = cc([
|
||||
'react-flow__edge',
|
||||
`react-flow__edge-${type}`,
|
||||
@@ -89,8 +93,8 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const onEdgeClick = useCallback(
|
||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||
if (elementsSelectable) {
|
||||
unsetNodesSelection();
|
||||
addSelectedElements(edgeElement);
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
addSelectedEdges([edgeElement.id]);
|
||||
}
|
||||
|
||||
onClick?.(event, edgeElement);
|
||||
@@ -146,21 +150,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
? (evt: MouseEvent): void => onEdgeUpdateEnd(evt, edgeElement)
|
||||
: undefined;
|
||||
|
||||
const onConnectEdge = (connection: Connection) => {
|
||||
const { edges } = store.getState();
|
||||
const edge = edges.find((e) => e.id === id);
|
||||
|
||||
if (edge && onEdgeUpdate) {
|
||||
onEdgeUpdate(edge, connection);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
store.setState,
|
||||
onConnectEdge,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
isSourceHandle ? 'target' : 'source',
|
||||
_onEdgeUpdate
|
||||
_onEdgeUpdate,
|
||||
store.getState
|
||||
);
|
||||
},
|
||||
[id, source, target, type, sourceHandleId, targetHandleId, setConnectionNodeId, setPosition, edgeElement, onConnectEdge]
|
||||
[id, source, target, type, sourceHandleId, targetHandleId, edgeElement, onEdgeUpdate]
|
||||
);
|
||||
|
||||
const onEdgeUpdaterSourceMouseDown = useCallback(
|
||||
@@ -179,8 +192,10 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
|
||||
const onEdgeUpdaterMouseEnter = useCallback(() => setUpdating(true), [setUpdating]);
|
||||
const onEdgeUpdaterMouseOut = useCallback(() => setUpdating(false), [setUpdating]);
|
||||
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart)})`, [markerStart]);
|
||||
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd)})`, [markerEnd]);
|
||||
|
||||
if (isHidden) {
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -208,16 +223,16 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
labelBgBorderRadius={labelBgBorderRadius}
|
||||
data={data}
|
||||
style={style}
|
||||
arrowHeadType={arrowHeadType}
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
markerEndId={markerEndId}
|
||||
sourceHandleId={sourceHandleId}
|
||||
targetHandleId={targetHandleId}
|
||||
markerStart={markerStartUrl}
|
||||
markerEnd={markerEndUrl}
|
||||
/>
|
||||
{handleEdgeUpdate && (
|
||||
<g
|
||||
|
||||
@@ -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;
|
||||
@@ -1,24 +1,19 @@
|
||||
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { SetState } from 'zustand';
|
||||
|
||||
import { getHostForElement } from '../../utils';
|
||||
|
||||
import {
|
||||
ElementId,
|
||||
XYPosition,
|
||||
OnConnectFunc,
|
||||
OnConnectStartFunc,
|
||||
OnConnectStopFunc,
|
||||
OnConnectEndFunc,
|
||||
OnConnect,
|
||||
OnConnectStart,
|
||||
OnConnectStop,
|
||||
OnConnectEnd,
|
||||
ConnectionMode,
|
||||
SetConnectionId,
|
||||
Connection,
|
||||
HandleType,
|
||||
ReactFlowState,
|
||||
} from '../../types';
|
||||
|
||||
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||
export type SetSourceIdFunc = (params: SetConnectionId) => void;
|
||||
|
||||
export type SetPosition = (pos: XYPosition) => void;
|
||||
|
||||
type Result = {
|
||||
elementBelow: Element | null;
|
||||
@@ -28,17 +23,15 @@ type Result = {
|
||||
};
|
||||
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
function checkElementBelowIsValid(
|
||||
export function checkElementBelowIsValid(
|
||||
event: MouseEvent,
|
||||
connectionMode: ConnectionMode,
|
||||
isTarget: boolean,
|
||||
nodeId: ElementId,
|
||||
handleId: ElementId | null,
|
||||
nodeId: string,
|
||||
handleId: string | null,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
doc: Document | ShadowRoot
|
||||
) {
|
||||
// TODO: why does this throw an error? elementFromPoint should be available for ShadowRoot too
|
||||
// @ts-ignore
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target') || false;
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source') || false;
|
||||
@@ -91,19 +84,18 @@ function resetRecentHandle(hoveredHandle: Element): void {
|
||||
|
||||
export function onMouseDown(
|
||||
event: ReactMouseEvent,
|
||||
handleId: ElementId | null,
|
||||
nodeId: ElementId,
|
||||
setConnectionNodeId: SetSourceIdFunc,
|
||||
setPosition: SetPosition,
|
||||
onConnect: OnConnectFunc,
|
||||
handleId: string | null,
|
||||
nodeId: string,
|
||||
setState: SetState<ReactFlowState>,
|
||||
onConnect: OnConnect,
|
||||
isTarget: boolean,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
connectionMode: ConnectionMode,
|
||||
elementEdgeUpdaterType?: HandleType,
|
||||
onEdgeUpdateEnd?: (evt: MouseEvent) => void,
|
||||
onConnectStart?: OnConnectStartFunc,
|
||||
onConnectStop?: OnConnectStopFunc,
|
||||
onConnectEnd?: OnConnectEndFunc
|
||||
onConnectStart?: OnConnectStart,
|
||||
onConnectStop?: OnConnectStop,
|
||||
onConnectEnd?: OnConnectEnd
|
||||
): void {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow');
|
||||
// when react-flow is used inside a shadow root we can't use document
|
||||
@@ -113,7 +105,6 @@ export function onMouseDown(
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY);
|
||||
const elementBelowIsTarget = elementBelow?.classList.contains('target');
|
||||
const elementBelowIsSource = elementBelow?.classList.contains('source');
|
||||
@@ -126,18 +117,24 @@ export function onMouseDown(
|
||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||
let recentHoveredHandle: Element;
|
||||
|
||||
setPosition({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
setState({
|
||||
connectionPosition: {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
connectionNodeId: nodeId,
|
||||
connectionHandleId: handleId,
|
||||
connectionHandleType: handleType,
|
||||
});
|
||||
|
||||
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType });
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
setPosition({
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
setState({
|
||||
connectionPosition: {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
},
|
||||
});
|
||||
|
||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(
|
||||
@@ -187,7 +184,11 @@ export function onMouseDown(
|
||||
}
|
||||
|
||||
resetRecentHandle(recentHoveredHandle);
|
||||
setConnectionNodeId({ connectionNodeId: null, connectionHandleId: null, connectionHandleType: null });
|
||||
setState({
|
||||
connectionNodeId: null,
|
||||
connectionHandleId: null,
|
||||
connectionHandleType: null,
|
||||
});
|
||||
|
||||
doc.removeEventListener('mousemove', onMouseMove as EventListenerOrEventListenerObject);
|
||||
doc.removeEventListener('mouseup', onMouseUp as EventListenerOrEventListenerObject);
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import React, { memo, useContext, useCallback, HTMLAttributes, forwardRef } from 'react';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import NodeIdContext from '../../contexts/NodeIdContext';
|
||||
import { HandleProps, Connection, ElementId, Position } from '../../types';
|
||||
|
||||
import { onMouseDown, SetSourceIdFunc, SetPosition } from './handler';
|
||||
import { HandleProps, Connection, ReactFlowState, Position } from '../../types';
|
||||
import { checkElementBelowIsValid, onMouseDown } from './handler';
|
||||
import { getHostForElement } from '../../utils';
|
||||
import { addEdge } from '../../utils/graph';
|
||||
|
||||
const alwaysValid = () => true;
|
||||
|
||||
export type HandleComponentProps = HandleProps & Omit<HTMLAttributes<HTMLDivElement>, 'id'>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
onConnectAction: s.onConnect,
|
||||
onConnectStart: s.onConnectStart,
|
||||
onConnectStop: s.onConnectStop,
|
||||
onConnectEnd: s.onConnectEnd,
|
||||
connectionMode: s.connectionMode,
|
||||
connectionStartHandle: s.connectionStartHandle,
|
||||
connectOnClick: s.connectOnClick,
|
||||
hasDefaultEdges: s.hasDefaultEdges,
|
||||
});
|
||||
|
||||
const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
(
|
||||
{
|
||||
@@ -26,49 +39,64 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||
const setPosition = useStoreActions((actions) => actions.setConnectionPosition);
|
||||
const setConnectionNodeId = useStoreActions((actions) => actions.setConnectionNodeId);
|
||||
const onConnectAction = useStoreState((state) => state.onConnect);
|
||||
const onConnectStart = useStoreState((state) => state.onConnectStart);
|
||||
const onConnectStop = useStoreState((state) => state.onConnectStop);
|
||||
const onConnectEnd = useStoreState((state) => state.onConnectEnd);
|
||||
const connectionMode = useStoreState((state) => state.connectionMode);
|
||||
const store = useStoreApi();
|
||||
const nodeId = useContext(NodeIdContext) as string;
|
||||
const {
|
||||
onConnectAction,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd,
|
||||
connectionMode,
|
||||
connectionStartHandle,
|
||||
connectOnClick,
|
||||
hasDefaultEdges,
|
||||
} = useStore(selector, shallow);
|
||||
|
||||
const handleId = id || null;
|
||||
const isTarget = type === 'target';
|
||||
|
||||
const onConnectExtended = useCallback(
|
||||
(params: Connection) => {
|
||||
onConnectAction?.(params);
|
||||
onConnect?.(params);
|
||||
const { defaultEdgeOptions } = store.getState();
|
||||
|
||||
const edgeParams = {
|
||||
...defaultEdgeOptions,
|
||||
...params,
|
||||
};
|
||||
if (hasDefaultEdges) {
|
||||
const { edges } = store.getState();
|
||||
store.setState({ edges: addEdge(edgeParams, edges) });
|
||||
}
|
||||
|
||||
onConnectAction?.(edgeParams);
|
||||
onConnect?.(edgeParams);
|
||||
},
|
||||
[onConnectAction, onConnect]
|
||||
[hasDefaultEdges, onConnectAction, onConnect]
|
||||
);
|
||||
|
||||
const onMouseDownHandler = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId as unknown as SetSourceIdFunc,
|
||||
setPosition as unknown as SetPosition,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
undefined,
|
||||
undefined,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd
|
||||
);
|
||||
if (event.button === 0) {
|
||||
onMouseDown(
|
||||
event,
|
||||
handleId,
|
||||
nodeId,
|
||||
store.setState,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
connectionMode,
|
||||
undefined,
|
||||
undefined,
|
||||
onConnectStart,
|
||||
onConnectStop,
|
||||
onConnectEnd
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
handleId,
|
||||
nodeId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
onConnectExtended,
|
||||
isTarget,
|
||||
isValidConnection,
|
||||
@@ -79,6 +107,47 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
]
|
||||
);
|
||||
|
||||
const onClick = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (!connectionStartHandle) {
|
||||
onConnectStart?.(event, { nodeId, handleId, handleType: type });
|
||||
store.setState({ connectionStartHandle: { nodeId, type, handleId } });
|
||||
} else {
|
||||
const doc = getHostForElement(event.target as HTMLElement);
|
||||
const { connection, isValid } = checkElementBelowIsValid(
|
||||
event as unknown as MouseEvent,
|
||||
connectionMode,
|
||||
connectionStartHandle.type === 'target',
|
||||
connectionStartHandle.nodeId,
|
||||
connectionStartHandle.handleId || null,
|
||||
isValidConnection,
|
||||
doc
|
||||
);
|
||||
|
||||
onConnectStop?.(event as unknown as MouseEvent);
|
||||
|
||||
if (isValid) {
|
||||
onConnectExtended(connection);
|
||||
}
|
||||
|
||||
onConnectEnd?.(event as unknown as MouseEvent);
|
||||
|
||||
store.setState({ connectionStartHandle: null });
|
||||
}
|
||||
},
|
||||
[
|
||||
connectionStartHandle,
|
||||
onConnectStart,
|
||||
onConnectExtended,
|
||||
onConnectStop,
|
||||
onConnectEnd,
|
||||
isTarget,
|
||||
nodeId,
|
||||
handleId,
|
||||
type,
|
||||
]
|
||||
);
|
||||
|
||||
const handleClasses = cc([
|
||||
'react-flow__handle',
|
||||
`react-flow__handle-${position}`,
|
||||
@@ -88,6 +157,10 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
source: !isTarget,
|
||||
target: isTarget,
|
||||
connectable: isConnectable,
|
||||
connecting:
|
||||
connectionStartHandle?.nodeId === nodeId &&
|
||||
connectionStartHandle?.handleId === handleId &&
|
||||
connectionStartHandle?.type === type,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -98,6 +171,7 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={onMouseDownHandler}
|
||||
onClick={connectOnClick ? onClick : undefined}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
|
||||
@@ -8,13 +8,15 @@ const DefaultNode = ({
|
||||
isConnectable,
|
||||
targetPosition = Position.Top,
|
||||
sourcePosition = Position.Bottom,
|
||||
}: NodeProps) => (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
}: NodeProps) => {
|
||||
return (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
DefaultNode.displayName = 'DefaultNode';
|
||||
|
||||
|
||||
5
src/components/Nodes/GroupNode.tsx
Normal file
5
src/components/Nodes/GroupNode.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
const GroupNode = () => null;
|
||||
|
||||
GroupNode.displayName = 'GroupNode';
|
||||
|
||||
export default GroupNode;
|
||||
@@ -5,7 +5,7 @@ import { NodeProps, Position } from '../../types';
|
||||
|
||||
const InputNode = ({ data, isConnectable, sourcePosition = Position.Bottom }: NodeProps) => (
|
||||
<>
|
||||
{data.label}
|
||||
{data?.label}
|
||||
<Handle type="source" position={sourcePosition} isConnectable={isConnectable} />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NodeProps, Position } from '../../types';
|
||||
const OutputNode = ({ data, isConnectable, targetPosition = Position.Top }: NodeProps) => (
|
||||
<>
|
||||
<Handle type="target" position={targetPosition} isConnectable={isConnectable} />
|
||||
{data.label}
|
||||
{data?.label}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
25
src/components/Nodes/useMemoizedMouseHandler.ts
Normal file
25
src/components/Nodes/useMemoizedMouseHandler.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { GetState } from 'zustand';
|
||||
|
||||
import { ReactFlowState, Node } from '../../types';
|
||||
|
||||
function useMemoizedMouseHandler(
|
||||
id: string,
|
||||
dragging: boolean,
|
||||
getState: GetState<ReactFlowState>,
|
||||
handler?: (event: MouseEvent, node: Node) => void
|
||||
) {
|
||||
const memoizedHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (typeof handler !== 'undefined' && !dragging) {
|
||||
const node = getState().nodeInternals.get(id)!;
|
||||
handler(event, { ...node });
|
||||
}
|
||||
},
|
||||
[handler, dragging, id]
|
||||
);
|
||||
|
||||
return memoizedHandler;
|
||||
}
|
||||
|
||||
export default useMemoizedMouseHandler;
|
||||
@@ -13,7 +13,7 @@ export const getHandleBounds = (nodeElement: HTMLDivElement, scale: number) => {
|
||||
export const getHandleBoundsByHandleType = (
|
||||
selector: string,
|
||||
nodeElement: HTMLDivElement,
|
||||
parentBounds: ClientRect | DOMRect,
|
||||
parentBounds: DOMRect,
|
||||
k: number
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
@@ -24,20 +24,18 @@ export const getHandleBoundsByHandleType = (
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||
|
||||
return handlesArray.map(
|
||||
(handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const handleId = handle.getAttribute('data-handleid');
|
||||
const handlePosition = (handle.getAttribute('data-handlepos') as unknown) as Position;
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
const bounds = handle.getBoundingClientRect();
|
||||
const dimensions = getDimensions(handle);
|
||||
const handleId = handle.getAttribute('data-handleid');
|
||||
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
|
||||
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) / k,
|
||||
y: (bounds.top - parentBounds.top) / k,
|
||||
...dimensions,
|
||||
};
|
||||
}
|
||||
);
|
||||
return {
|
||||
id: handleId,
|
||||
position: handlePosition,
|
||||
x: (bounds.left - parentBounds.left) / k,
|
||||
y: (bounds.top - parentBounds.top) / k,
|
||||
...dimensions,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import React, {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
memo,
|
||||
ComponentType,
|
||||
CSSProperties,
|
||||
useMemo,
|
||||
MouseEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
|
||||
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions } from '../../store/hooks';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { Provider } from '../../contexts/NodeIdContext';
|
||||
import { NodeComponentProps, WrapNodeProps } from '../../types';
|
||||
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
|
||||
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
|
||||
|
||||
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
addSelectedNodes: s.addSelectedNodes,
|
||||
updateNodePosition: s.updateNodePosition,
|
||||
unselectNodesAndEdges: s.unselectNodesAndEdges,
|
||||
updateNodeDimensions: s.updateNodeDimensions,
|
||||
});
|
||||
|
||||
export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
const NodeWrapper = ({
|
||||
id,
|
||||
type,
|
||||
@@ -42,192 +41,188 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
selectNodesOnDrag,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
isHidden,
|
||||
isInitialized,
|
||||
hidden,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
isDragging,
|
||||
dragging,
|
||||
resizeObserver,
|
||||
dragHandle,
|
||||
zIndex,
|
||||
isParent,
|
||||
noPanClassName,
|
||||
noDragClassName,
|
||||
}: WrapNodeProps) => {
|
||||
const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions);
|
||||
const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements);
|
||||
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
|
||||
const store = useStoreApi();
|
||||
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
|
||||
selector,
|
||||
shallow
|
||||
);
|
||||
const nodeElement = useRef<HTMLDivElement>(null);
|
||||
|
||||
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
|
||||
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
|
||||
|
||||
const prevSourcePosition = useRef(sourcePosition);
|
||||
const prevTargetPosition = useRef(targetPosition);
|
||||
const prevType = useRef(type);
|
||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||
const nodeStyle: CSSProperties = useMemo(
|
||||
() => ({
|
||||
zIndex: selected ? 10 : 3,
|
||||
zIndex,
|
||||
transform: `translate(${xPos}px,${yPos}px)`,
|
||||
pointerEvents:
|
||||
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
|
||||
// prevents jumping of nodes on start
|
||||
opacity: isInitialized ? 1 : 0,
|
||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||
...style,
|
||||
}),
|
||||
[
|
||||
selected,
|
||||
xPos,
|
||||
yPos,
|
||||
isSelectable,
|
||||
isDraggable,
|
||||
onClick,
|
||||
isInitialized,
|
||||
style,
|
||||
onMouseEnter,
|
||||
onMouseMove,
|
||||
onMouseLeave,
|
||||
]
|
||||
[zIndex, xPos, yPos, hasPointerEvents, style]
|
||||
);
|
||||
const onMouseEnterHandler = useMemo(() => {
|
||||
if (!onMouseEnter || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseEnter(event, node);
|
||||
}, [onMouseEnter, isDragging, node]);
|
||||
const grid = useMemo(
|
||||
() => (snapToGrid ? snapGrid : [1, 1])! as [number, number],
|
||||
[snapToGrid, snapGrid?.[0], snapGrid?.[1]]
|
||||
);
|
||||
|
||||
const onMouseMoveHandler = useMemo(() => {
|
||||
if (!onMouseMove || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseMove(event, node);
|
||||
}, [onMouseMove, isDragging, node]);
|
||||
|
||||
const onMouseLeaveHandler = useMemo(() => {
|
||||
if (!onMouseLeave || isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onMouseLeave(event, node);
|
||||
}, [onMouseLeave, isDragging, node]);
|
||||
|
||||
const onContextMenuHandler = useMemo(() => {
|
||||
if (!onContextMenu) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (event: MouseEvent) => onContextMenu(event, node);
|
||||
}, [onContextMenu, node]);
|
||||
const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter);
|
||||
const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove);
|
||||
const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave);
|
||||
const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu);
|
||||
const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick);
|
||||
|
||||
const onSelectNodeHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
if (!isDraggable) {
|
||||
if (isSelectable) {
|
||||
unsetNodesSelection();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (!selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
}
|
||||
|
||||
onClick?.(event, node);
|
||||
if (onClick) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onClick(event, { ...node });
|
||||
}
|
||||
}
|
||||
},
|
||||
[isSelectable, selected, isDraggable, onClick, node]
|
||||
[isSelectable, selected, isDraggable, onClick, id]
|
||||
);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
(event: DraggableEvent) => {
|
||||
onNodeDragStart?.(event as MouseEvent, node);
|
||||
|
||||
if (selectNodesOnDrag && isSelectable) {
|
||||
unsetNodesSelection();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
if (!selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
} else if (!selectNodesOnDrag && !selected && isSelectable) {
|
||||
unsetNodesSelection();
|
||||
addSelectedElements([]);
|
||||
unselectNodesAndEdges();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
}
|
||||
|
||||
if (onNodeDragStart) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onNodeDragStart(event as MouseEvent, { ...node });
|
||||
}
|
||||
},
|
||||
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||
);
|
||||
|
||||
const onDrag = useCallback(
|
||||
(event: DraggableEvent, draggableData: DraggableData) => {
|
||||
if (onNodeDrag) {
|
||||
node.position.x += draggableData.deltaX;
|
||||
node.position.y += draggableData.deltaY;
|
||||
onNodeDrag(event as MouseEvent, node);
|
||||
}
|
||||
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
|
||||
|
||||
updateNodePosDiff({
|
||||
id,
|
||||
diff: {
|
||||
x: draggableData.deltaX,
|
||||
y: draggableData.deltaY,
|
||||
},
|
||||
isDragging: true,
|
||||
});
|
||||
if (onNodeDrag) {
|
||||
const node = store.getState().nodeInternals.get(id)!;
|
||||
onNodeDrag(event as MouseEvent, {
|
||||
...node,
|
||||
dragging: true,
|
||||
position: {
|
||||
x: node.position.x + draggableData.deltaX,
|
||||
y: node.position.y + draggableData.deltaY,
|
||||
},
|
||||
positionAbsolute: {
|
||||
x: (node.positionAbsolute?.x || 0) + draggableData.deltaX,
|
||||
y: (node.positionAbsolute?.y || 0) + draggableData.deltaY,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, node, onNodeDrag]
|
||||
[id, onNodeDrag]
|
||||
);
|
||||
|
||||
const onDragStop = useCallback(
|
||||
(event: DraggableEvent) => {
|
||||
// onDragStop also gets called when user just clicks on a node.
|
||||
// Because of that we set dragging to true inside the onDrag handler and handle the click here
|
||||
if (!isDragging) {
|
||||
let node;
|
||||
|
||||
if (onClick || onNodeDragStop) {
|
||||
node = store.getState().nodeInternals.get(id)!;
|
||||
}
|
||||
|
||||
if (!dragging) {
|
||||
if (isSelectable && !selectNodesOnDrag && !selected) {
|
||||
addSelectedElements(node);
|
||||
addSelectedNodes([id]);
|
||||
}
|
||||
|
||||
onClick?.(event as MouseEvent, node);
|
||||
if (onClick && node) {
|
||||
onClick(event as MouseEvent, { ...node });
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateNodePosDiff({
|
||||
id: node.id,
|
||||
isDragging: false,
|
||||
updateNodePosition({
|
||||
id,
|
||||
dragging: false,
|
||||
});
|
||||
|
||||
onNodeDragStop?.(event as MouseEvent, node);
|
||||
if (onNodeDragStop && node) {
|
||||
onNodeDragStop(event as MouseEvent, { ...node, dragging: false });
|
||||
}
|
||||
},
|
||||
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected]
|
||||
[id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected]
|
||||
);
|
||||
|
||||
const onNodeDoubleClickHandler = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
onNodeDoubleClick?.(event, node);
|
||||
},
|
||||
[node, onNodeDoubleClick]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (nodeElement.current && !isHidden) {
|
||||
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
|
||||
}
|
||||
}, [id, isHidden, sourcePosition, targetPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeElement.current) {
|
||||
if (nodeElement.current && !hidden) {
|
||||
const currNode = nodeElement.current;
|
||||
resizeObserver?.observe(currNode);
|
||||
|
||||
return () => resizeObserver?.unobserve(currNode);
|
||||
}
|
||||
}, []);
|
||||
}, [hidden]);
|
||||
|
||||
if (isHidden) {
|
||||
useEffect(() => {
|
||||
// when the user programmatically changes the source or handle position, we re-initialize the node
|
||||
const typeChanged = prevType.current !== type;
|
||||
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
|
||||
const targetPosChanged = prevTargetPosition.current !== targetPosition;
|
||||
|
||||
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
|
||||
if (typeChanged) {
|
||||
prevType.current = type;
|
||||
}
|
||||
if (sourcePosChanged) {
|
||||
prevSourcePosition.current = sourcePosition;
|
||||
}
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = targetPosition;
|
||||
}
|
||||
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
|
||||
}
|
||||
}, [id, type, sourcePosition, targetPosition]);
|
||||
|
||||
if (hidden) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeClasses = cc([
|
||||
'react-flow__node',
|
||||
`react-flow__node-${type}`,
|
||||
noPanClassName,
|
||||
className,
|
||||
{
|
||||
selected,
|
||||
selectable: isSelectable,
|
||||
parent: isParent,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -238,7 +233,7 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
onStop={onDragStop}
|
||||
scale={scale}
|
||||
disabled={!isDraggable}
|
||||
cancel=".nodrag"
|
||||
cancel={`.${noDragClassName}`}
|
||||
nodeRef={nodeElement}
|
||||
grid={grid}
|
||||
enableUserSelectHack={false}
|
||||
@@ -267,8 +262,9 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||
isConnectable={isConnectable}
|
||||
sourcePosition={sourcePosition}
|
||||
targetPosition={targetPosition}
|
||||
isDragging={isDragging}
|
||||
dragging={dragging}
|
||||
dragHandle={dragHandle}
|
||||
zIndex={zIndex}
|
||||
/>
|
||||
</Provider>
|
||||
</div>
|
||||
|
||||
@@ -1,57 +1,53 @@
|
||||
/**
|
||||
* The nodes selection rectangle gets displayed when a user
|
||||
* made a selectio with on or several nodes
|
||||
* made a selection with on or several nodes
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
|
||||
import ReactDraggable, { DraggableData } from 'react-draggable';
|
||||
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
|
||||
import { DraggableCore, DraggableData } from 'react-draggable';
|
||||
import cc from 'classcat';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
import { isNode } from '../../utils/graph';
|
||||
import { Node } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
import { Node, ReactFlowState } from '../../types';
|
||||
import { getRectOfNodes } from '../../utils/graph';
|
||||
|
||||
export interface NodesSelectionProps {
|
||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionDrag?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionDragStop?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
|
||||
noPanClassName?: string;
|
||||
}
|
||||
// @TODO: work with nodeInternals instead of converting it to an array
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
transform: s.transform,
|
||||
selectedNodesBbox: s.selectedNodesBbox,
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
selectedNodes: Array.from(s.nodeInternals)
|
||||
.filter(([_, n]) => n.selected)
|
||||
.map(([_, n]) => n),
|
||||
snapToGrid: s.snapToGrid,
|
||||
snapGrid: s.snapGrid,
|
||||
updateNodePosition: s.updateNodePosition,
|
||||
});
|
||||
|
||||
export default ({
|
||||
function NodesSelection({
|
||||
onSelectionDragStart,
|
||||
onSelectionDrag,
|
||||
onSelectionDragStop,
|
||||
onSelectionContextMenu,
|
||||
}: NodesSelectionProps) => {
|
||||
const [tX, tY, tScale] = useStoreState((state) => state.transform);
|
||||
const selectedNodesBbox = useStoreState((state) => state.selectedNodesBbox);
|
||||
const selectionActive = useStoreState((state) => state.selectionActive);
|
||||
const selectedElements = useStoreState((state) => state.selectedElements);
|
||||
const snapToGrid = useStoreState((state) => state.snapToGrid);
|
||||
const snapGrid = useStoreState((state) => state.snapGrid);
|
||||
const nodes = useStoreState((state) => state.nodes);
|
||||
|
||||
const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff);
|
||||
|
||||
noPanClassName,
|
||||
}: NodesSelectionProps) {
|
||||
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } = useStore(
|
||||
selector,
|
||||
shallow
|
||||
);
|
||||
const [tX, tY, tScale] = transform;
|
||||
const nodeRef = useRef(null);
|
||||
|
||||
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
|
||||
|
||||
const selectedNodes = useMemo(
|
||||
() =>
|
||||
selectedElements
|
||||
? selectedElements.filter(isNode).map((selectedNode) => {
|
||||
const matchingNode = nodes.find((node) => node.id === selectedNode.id);
|
||||
|
||||
return {
|
||||
...matchingNode,
|
||||
position: matchingNode?.__rf.position,
|
||||
} as Node;
|
||||
})
|
||||
: [],
|
||||
[selectedElements, nodes]
|
||||
);
|
||||
|
||||
const style = useMemo(
|
||||
() => ({
|
||||
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
|
||||
@@ -59,6 +55,8 @@ export default ({
|
||||
[tX, tY, tScale]
|
||||
);
|
||||
|
||||
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
|
||||
|
||||
const innerStyle = useMemo(
|
||||
() => ({
|
||||
width: selectedNodesBbox.width,
|
||||
@@ -78,25 +76,23 @@ export default ({
|
||||
|
||||
const onDrag = useCallback(
|
||||
(event: MouseEvent, data: DraggableData) => {
|
||||
if (onSelectionDrag) {
|
||||
onSelectionDrag(event, selectedNodes);
|
||||
}
|
||||
|
||||
updateNodePosDiff({
|
||||
updateNodePosition({
|
||||
diff: {
|
||||
x: data.deltaX,
|
||||
y: data.deltaY,
|
||||
},
|
||||
isDragging: true,
|
||||
dragging: true,
|
||||
});
|
||||
|
||||
onSelectionDrag?.(event, selectedNodes);
|
||||
},
|
||||
[onSelectionDrag, selectedNodes, updateNodePosDiff]
|
||||
[onSelectionDrag, selectedNodes, updateNodePosition]
|
||||
);
|
||||
|
||||
const onStop = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
updateNodePosDiff({
|
||||
isDragging: false,
|
||||
updateNodePosition({
|
||||
dragging: false,
|
||||
});
|
||||
|
||||
onSelectionDragStop?.(event, selectedNodes);
|
||||
@@ -106,22 +102,18 @@ export default ({
|
||||
|
||||
const onContextMenu = useCallback(
|
||||
(event: MouseEvent) => {
|
||||
const selectedNodes = selectedElements
|
||||
? selectedElements.filter(isNode).map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)!)
|
||||
: [];
|
||||
|
||||
onSelectionContextMenu?.(event, selectedNodes);
|
||||
},
|
||||
[onSelectionContextMenu]
|
||||
[onSelectionContextMenu, selectedNodes]
|
||||
);
|
||||
|
||||
if (!selectedElements || selectionActive) {
|
||||
if (!selectedNodes?.length || userSelectionActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="react-flow__nodesselection" style={style}>
|
||||
<ReactDraggable
|
||||
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
|
||||
<DraggableCore
|
||||
scale={tScale}
|
||||
grid={grid}
|
||||
onStart={(event) => onStart(event as MouseEvent)}
|
||||
@@ -136,7 +128,9 @@ export default ({
|
||||
onContextMenu={onContextMenu}
|
||||
style={innerStyle}
|
||||
/>
|
||||
</ReactDraggable>
|
||||
</DraggableCore>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(NodesSelection);
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { Elements } from '../../types';
|
||||
import { useStoreState } from '../../store/hooks';
|
||||
import { ReactFlowState, OnSelectionChangeFunc, Node, Edge } from '../../types';
|
||||
import { useStore } from '../../store';
|
||||
|
||||
interface SelectionListenerProps {
|
||||
onSelectionChange: (elements: Elements | null) => void;
|
||||
onSelectionChange: OnSelectionChangeFunc;
|
||||
}
|
||||
|
||||
// This is a helper component for calling the onSelectionChange listener
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
|
||||
selectedEdges: s.edges.filter((e) => e.selected),
|
||||
});
|
||||
|
||||
export default ({ onSelectionChange }: SelectionListenerProps) => {
|
||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
||||
const areEqual = (objA: any, objB: any) => {
|
||||
const selectedNodeIdsA = objA.selectedNodes.map((n: Node) => n.id);
|
||||
const selectedNodeIdsB = objB.selectedNodes.map((n: Node) => n.id);
|
||||
|
||||
const selectedEdgeIdsA = objA.selectedEdges.map((e: Edge) => e.id);
|
||||
const selectedEdgeIdsB = objB.selectedEdges.map((e: Edge) => e.id);
|
||||
|
||||
return shallow(selectedNodeIdsA, selectedNodeIdsB) && shallow(selectedEdgeIdsA, selectedEdgeIdsB);
|
||||
};
|
||||
|
||||
// This is just a helper component for calling the onSelectionChange listener.
|
||||
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
function SelectionListener({ onSelectionChange }: SelectionListenerProps) {
|
||||
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectionChange(selectedElements);
|
||||
}, [selectedElements]);
|
||||
onSelectionChange({ nodes: selectedNodes, edges: selectedEdges });
|
||||
}, [selectedNodes, selectedEdges]);
|
||||
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default memo(SelectionListener);
|
||||
|
||||
161
src/components/StoreUpdater/index.tsx
Normal file
161
src/components/StoreUpdater/index.tsx
Normal 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;
|
||||
@@ -2,100 +2,160 @@
|
||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import React, { memo, useState, useRef, useCallback } from 'react';
|
||||
import shallow from 'zustand/shallow';
|
||||
|
||||
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||
import { XYPosition } from '../../types';
|
||||
import { useStore, useStoreApi } from '../../store';
|
||||
import { getSelectionChanges } from '../../utils/changes';
|
||||
import { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types';
|
||||
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
|
||||
|
||||
type SelectionRect = Rect & {
|
||||
startX: number;
|
||||
startY: number;
|
||||
draw: boolean;
|
||||
};
|
||||
|
||||
type UserSelectionProps = {
|
||||
selectionKeyPressed: boolean;
|
||||
};
|
||||
|
||||
function getMousePosition(event: React.MouseEvent): XYPosition | void {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow');
|
||||
if (!reactFlowNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||
|
||||
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
|
||||
return {
|
||||
x: event.clientX - containerBounds.left,
|
||||
y: event.clientY - containerBounds.top,
|
||||
};
|
||||
}
|
||||
|
||||
const SelectionRect = () => {
|
||||
const userSelectionRect = useStoreState((state) => state.userSelectionRect);
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
});
|
||||
|
||||
if (!userSelectionRect.draw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__selection"
|
||||
style={{
|
||||
width: userSelectionRect.width,
|
||||
height: userSelectionRect.height,
|
||||
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const initialRect: SelectionRect = {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
draw: false,
|
||||
};
|
||||
|
||||
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
|
||||
const selectionActive = useStoreState((state) => state.selectionActive);
|
||||
const elementsSelectable = useStoreState((state) => state.elementsSelectable);
|
||||
const store = useStoreApi();
|
||||
const prevSelectedNodesCount = useRef<number>(0);
|
||||
const prevSelectedEdgesCount = useRef<number>(0);
|
||||
const containerBounds = useRef<DOMRect>();
|
||||
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
|
||||
const { userSelectionActive, elementsSelectable } = useStore(selector, shallow);
|
||||
|
||||
const setUserSelection = useStoreActions((actions) => actions.setUserSelection);
|
||||
const updateUserSelection = useStoreActions((actions) => actions.updateUserSelection);
|
||||
const unsetUserSelection = useStoreActions((actions) => actions.unsetUserSelection);
|
||||
const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection);
|
||||
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
|
||||
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
|
||||
|
||||
const resetUserSelection = useCallback(() => {
|
||||
setUserSelectionRect(initialRect);
|
||||
|
||||
store.setState({ userSelectionActive: false });
|
||||
|
||||
prevSelectedNodesCount.current = 0;
|
||||
prevSelectedEdgesCount.current = 0;
|
||||
}, []);
|
||||
|
||||
const onMouseDown = useCallback((event: React.MouseEvent): void => {
|
||||
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
|
||||
containerBounds.current = reactFlowNode.getBoundingClientRect();
|
||||
|
||||
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||
|
||||
setUserSelectionRect({
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: mousePos.x,
|
||||
startY: mousePos.y,
|
||||
x: mousePos.x,
|
||||
y: mousePos.y,
|
||||
draw: true,
|
||||
});
|
||||
|
||||
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
|
||||
}, []);
|
||||
|
||||
const onMouseMove = (event: React.MouseEvent): void => {
|
||||
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||
const startX = userSelectionRect.startX ?? 0;
|
||||
const startY = userSelectionRect.startY ?? 0;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...userSelectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
};
|
||||
|
||||
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
|
||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
||||
const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, false, true);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||
if (changes.length) {
|
||||
onNodesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
if (changes.length) {
|
||||
onEdgesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
setUserSelectionRect(nextUserSelectRect);
|
||||
};
|
||||
|
||||
const onMouseUp = useCallback(() => {
|
||||
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
|
||||
|
||||
resetUserSelection();
|
||||
}, []);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
|
||||
resetUserSelection();
|
||||
}, []);
|
||||
|
||||
if (!elementsSelectable || !renderUserSelectionPane) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onMouseDown = (event: React.MouseEvent): void => {
|
||||
const mousePos = getMousePosition(event);
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
|
||||
setUserSelection(mousePos);
|
||||
};
|
||||
|
||||
const onMouseMove = (event: React.MouseEvent): void => {
|
||||
if (!selectionKeyPressed || !selectionActive) {
|
||||
return;
|
||||
}
|
||||
const mousePos = getMousePosition(event);
|
||||
|
||||
if (!mousePos) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateUserSelection(mousePos);
|
||||
};
|
||||
|
||||
const onMouseUp = () => unsetUserSelection();
|
||||
|
||||
const onMouseLeave = () => {
|
||||
unsetUserSelection();
|
||||
unsetNodesSelection();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__selectionpane"
|
||||
className="react-flow__selectionpane react-flow__container"
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<SelectionRect />
|
||||
{userSelectionRect.draw && (
|
||||
<div
|
||||
className="react-flow__selection react-flow__container"
|
||||
style={{
|
||||
width: userSelectionRect.width,
|
||||
height: userSelectionRect.height,
|
||||
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user