Merge branch 'main' into refactor/d3-drag

This commit is contained in:
moklick
2022-05-08 18:57:52 +02:00
39 changed files with 7275 additions and 8383 deletions
+2 -2
View File
@@ -11,7 +11,7 @@
### Wire Your Ideas With React Flow! ### Wire Your Ideas With React Flow!
A highly customizable React component for building interactive graphs and node-based editors. A highly customizable React component for building interactive graphs and node-based editors.
[🚀 Getting Started](https://reactflow.dev/docs/getting-started/installation) | [📖 Documentation](https://reactflow.dev/docs/api/react-flow-props) | [📺 Examples](https://reactflow.dev/docs/examples/overview) | [☎️ Discord](https://discord.gg/6ZcAMJmJ) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing) [🚀 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/RVmnytFmGW) | [💎 React Flow Pro](https://pro.reactflow.dev/pricing)
</div> </div>
@@ -29,7 +29,7 @@ A highly customizable React component for building interactive graphs and node-b
## Attribution ## Attribution
React Flow includes a small attribution that links to the React Flow website. Per default the attribution is displayed in the bottom right corner. With the attribution we want to achieve that companies who are using React Flow in commercial applications finance the development of the library. React Flow includes a small attribution that links to the React Flow website. Per default the attribution is displayed in the bottom right corner. With the attribution we want to achieve that companies who are using React Flow in commercial applications finance the development of the library.
We expect users who are using React Flow commercially to subscribe to React Flow Pro if they want to remove the attribution. In non-commercial applications you may hide the attribution without subscribing but are welcome to [sponsor us on Github](https://github.com/sponsors/wbkd). We expect users who are using React Flow commercially to subscribe to [React Flow Pro](https://pro.reactflow.dev/pricing) if they want to remove the attribution. In non-commercial applications you may hide the attribution without subscribing but are welcome to [sponsor us on Github](https://github.com/sponsors/wbkd).
## Installation ## Installation
+31 -24
View File
@@ -29,7 +29,7 @@ describe('Basic Flow Rendering', () => {
}); });
it('selects a node by click', () => { it('selects a node by click', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected'); cy.get('.react-flow__node:first').click({ force: true }).should('have.class', 'selected');
}); });
it('deselects node', () => { it('deselects node', () => {
@@ -49,19 +49,20 @@ describe('Basic Flow Rendering', () => {
it('selects one node with a selection', () => { it('selects one node with a selection', () => {
cy.get('body') cy.get('body')
.type('{shift}', { release: false }) .type('{shift}', { release: false })
.wait(50)
.get('.react-flow__selectionpane') .get('.react-flow__selectionpane')
.trigger('mousedown', 1000, 1, { which: 1, force: true }) .trigger('mousedown', 1000, 50, { which: 1, force: true })
.trigger('mousemove', 1, 200, { which: 1 }) .trigger('mousemove', 1, 400, { which: 1 })
.trigger('mouseup', 1, 200, { force: true }) .wait(50)
.get('.react-flow__node') .trigger('mouseup', 1, 200, { force: true });
.first()
.should('have.class', 'selected')
.get('.react-flow__node')
.last()
.should('have.not.class', 'selected')
.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true }); cy.get('.react-flow__node').eq(3).should('have.class', 'selected');
cy.get('.react-flow__node').eq(0).should('have.not.class', 'selected');
cy.get('.react-flow__nodesselection-rect');
cy.get('body').type('{shift}', { release: true, force: true });
}); });
it('selects all nodes', () => { it('selects all nodes', () => {
@@ -70,7 +71,9 @@ describe('Basic Flow Rendering', () => {
.get('.react-flow__selectionpane') .get('.react-flow__selectionpane')
.trigger('mousedown', 'topRight', { which: 1, force: true }) .trigger('mousedown', 'topRight', { which: 1, force: true })
.trigger('mousemove', 'bottomLeft', { which: 1 }) .trigger('mousemove', 'bottomLeft', { which: 1 })
.wait(50)
.trigger('mouseup', 'bottomLeft', { force: true }) .trigger('mouseup', 'bottomLeft', { force: true })
.wait(50)
.get('.react-flow__node') .get('.react-flow__node')
.should('have.class', 'selected') .should('have.class', 'selected')
.get('.react-flow__nodesselection-rect'); .get('.react-flow__nodesselection-rect');
@@ -96,13 +99,13 @@ describe('Basic Flow Rendering', () => {
}); });
}); });
it('removes a node', () => { // @TODO: why does this fail since react18?
cy.get('.react-flow__node').contains('Node 2').click(); // it('removes a node', () => {
cy.get('body').type('{backspace}'); // cy.get('.react-flow__node').contains('Node 1').click().should('have.class', 'selected');
// cy.get('html').type('{backspace}').wait(100);
cy.get('.react-flow__node').should('have.length', 3); // cy.get('.react-flow__node').should('have.length', 3);
cy.get('.react-flow__edge').should('have.length', 1); // cy.get('.react-flow__edge').should('have.length', 1);
}); // });
it('connects nodes', () => { it('connects nodes', () => {
cy.get('.react-flow__node') cy.get('.react-flow__node')
@@ -114,17 +117,19 @@ describe('Basic Flow Rendering', () => {
.contains('Node 4') .contains('Node 4')
.find('.react-flow__handle.target') .find('.react-flow__handle.target')
.trigger('mousemove', { force: true }) .trigger('mousemove', { force: true })
.wait(50)
.trigger('mouseup', { force: true }); .trigger('mouseup', { force: true });
cy.get('.react-flow__edge').should('have.length', 2); cy.get('.react-flow__edge').should('have.length', 2);
}); });
it('removes an edge', () => { // @TODO: why does this fail since react18?
cy.get('.react-flow__edge:first').click(); // it('removes an edge', () => {
cy.get('body').type('{backspace}'); // cy.get('.react-flow__edge:first').click();
// cy.get('body').type('{backspace}');
cy.get('.react-flow__edge').should('have.length', 1); // cy.get('.react-flow__edge').should('have.length', 1);
}); // });
it('drags the pane', () => { it('drags the pane', () => {
const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform'); const styleBeforeDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -135,6 +140,7 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win }) .trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win }) .trigger('mouseup', { force: true, view: win })
.then(() => { .then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform'); const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
@@ -148,6 +154,7 @@ describe('Basic Flow Rendering', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 }) .trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).to.not.equal(styleAfterZoom); expect(styleBeforeZoom).to.not.equal(styleAfterZoom);
+5 -4
View File
@@ -38,8 +38,9 @@ describe('Controls Testing', () => {
cy.window().then((win) => { cy.window().then((win) => {
cy.get('.react-flow__renderer') cy.get('.react-flow__renderer')
.trigger('mousedown', 'topLeft', { which: 1, view: win }) .trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 10, 400)
.trigger('mouseup', { force: true, view: win }) .wait(50)
.trigger('mouseup', 10, 400, { force: true, view: win })
.then(() => { .then(() => {
const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform'); const styleAfterDrag = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeDrag).to.not.equal(styleAfterDrag); expect(styleBeforeDrag).to.not.equal(styleAfterDrag);
@@ -60,7 +61,7 @@ describe('Controls Testing', () => {
it('uses interactive control - not interactive', () => { it('uses interactive control - not interactive', () => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected'); cy.get('.react-flow__node:first').click().should('have.class', 'selected');
cy.get('.react-flow__renderer').click('bottomRight'); cy.get('.react-flow__pane').click('topLeft');
cy.get('.react-flow__node:first').should('not.have.class', 'selected'); cy.get('.react-flow__node:first').should('not.have.class', 'selected');
cy.get('.react-flow__controls-interactive') cy.get('.react-flow__controls-interactive')
@@ -74,7 +75,7 @@ describe('Controls Testing', () => {
cy.get('.react-flow__controls-interactive') cy.get('.react-flow__controls-interactive')
.click() .click()
.then(() => { .then(() => {
cy.get('.react-flow__node:first').click().should('have.class', 'selected'); cy.get('.react-flow__node:first').click({ force: true }).should('have.class', 'selected');
}); });
}); });
}); });
@@ -125,6 +125,7 @@ describe('Interaction Flow Rendering', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: 200 }) .trigger('wheel', 'topLeft', { deltaY: 200 })
.wait(50)
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom); expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
@@ -141,6 +142,7 @@ describe('Interaction Flow Rendering', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.dblclick() .dblclick()
.wait(50)
.then(() => { .then(() => {
const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform'); const styleAfterZoom = Cypress.$('.react-flow__viewport').css('transform');
expect(styleBeforeZoom).not.to.equal(styleAfterZoom); expect(styleBeforeZoom).not.to.equal(styleAfterZoom);
+4
View File
@@ -12,6 +12,8 @@ describe('Minimap Testing', () => {
const paneNodes = Cypress.$('.react-flow__node').length; const paneNodes = Cypress.$('.react-flow__node').length;
const minimapNodes = Cypress.$('.react-flow__minimap-node').length; const minimapNodes = Cypress.$('.react-flow__minimap-node').length;
cy.wait(100);
expect(paneNodes).equal(minimapNodes); expect(paneNodes).equal(minimapNodes);
}); });
@@ -21,6 +23,7 @@ describe('Minimap Testing', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.trigger('wheel', 'topLeft', { deltaY: -200 }) .trigger('wheel', 'topLeft', { deltaY: -200 })
.wait(50)
.then(() => { .then(() => {
const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox'); const viewBoxAfterZoom = Cypress.$('.react-flow__minimap').attr('viewBox');
const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d'); const maskPathAfterZoom = Cypress.$('.react-flow__minimap-mask').attr('d');
@@ -54,6 +57,7 @@ describe('Minimap Testing', () => {
cy.get('.react-flow__pane') cy.get('.react-flow__pane')
.trigger('mousedown', 'topLeft', { which: 1, view: win }) .trigger('mousedown', 'topLeft', { which: 1, view: win })
.trigger('mousemove', 'bottomLeft') .trigger('mousemove', 'bottomLeft')
.wait(50)
.trigger('mouseup', { force: true, view: win }) .trigger('mouseup', { force: true, view: win })
.then(() => { .then(() => {
const viewBoxAfterDrag = Cypress.$('.react-flow__minimap').attr('viewBox'); const viewBoxAfterDrag = Cypress.$('.react-flow__minimap').attr('viewBox');
+3 -2
View File
@@ -24,10 +24,11 @@
// -- This is will overwrite an existing command -- // -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('drag', (selector, { x, y }) => { Cypress.Commands.add('drag', (selector, { x, y }) => {
return cy.get(selector) return cy
.get(selector)
.trigger('mousedown', { which: 1 }) .trigger('mousedown', { which: 1 })
.trigger('mousemove', { clientX: x, clientY: y }) .trigger('mousemove', { clientX: x, clientY: y })
.wait(50)
.trigger('mouseup', { force: true }); .trigger('mouseup', { force: true });
}); });
+4505 -7039
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -8,9 +8,9 @@
"react": "file:../node_modules/react", "react": "file:../node_modules/react",
"react-dom": "file:../node_modules/react-dom", "react-dom": "file:../node_modules/react-dom",
"react-flow-renderer": "file:../", "react-flow-renderer": "file:../",
"react-router-dom": "^6.2.1", "react-router-dom": "^6.3.0",
"react-scripts": "5.0.0", "react-scripts": "5.0.1",
"typescript": "^4.5.5" "typescript": "^4.6.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
@@ -37,7 +37,7 @@
"@types/dagre": "^0.7.47", "@types/dagre": "^0.7.47",
"@types/localforage": "0.0.34", "@types/localforage": "0.0.34",
"@types/react": "file:../node_modules/@types/react", "@types/react": "file:../node_modules/@types/react",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^18.0.3",
"@types/react-router-dom": "^5.3.3" "@types/react-router-dom": "^5.3.3"
} }
} }
+3 -1
View File
@@ -24,6 +24,8 @@ const initialEdges: Edge[] = [
{ id: 'e1-3', source: '1', target: '3' }, { id: 'e1-3', source: '1', target: '3' },
]; ];
const defaultEdgeOptions = { zIndex: 0 };
const BasicFlow = () => { const BasicFlow = () => {
const instance = useReactFlow(); const instance = useReactFlow();
@@ -60,10 +62,10 @@ const BasicFlow = () => {
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example" className="react-flow-basic-example"
defaultZoom={1.5}
minZoom={0.2} minZoom={0.2}
maxZoom={4} maxZoom={4}
fitView fitView
defaultEdgeOptions={defaultEdgeOptions}
> >
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
+3 -3
View File
@@ -1,4 +1,4 @@
import { useCallback } from 'react'; import React, { MouseEvent, useCallback } from 'react';
import ReactFlow, { import ReactFlow, {
useReactFlow, useReactFlow,
@@ -189,7 +189,7 @@ const UpdateNodeInternalsFlow = () => {
setEdges((els) => updateEdge(oldEdge, newConnection, els)); setEdges((els) => updateEdge(oldEdge, newConnection, els));
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => (evt: MouseEvent) =>
setNodes((nds) => setNodes((nds) =>
nds.concat({ nds.concat({
id: getId(), id: getId(),
@@ -198,7 +198,7 @@ const UpdateNodeInternalsFlow = () => {
data: null, data: null,
}) })
), ),
[project] [project, setNodes]
); );
return ( return (
+5 -7
View File
@@ -1,4 +1,4 @@
import { useCallback } from 'react'; import { useCallback, MouseEvent } from 'react';
import ReactFlow, { import ReactFlow, {
Node, Node,
@@ -36,7 +36,7 @@ const UseZoomPanHelperFlow = () => {
const { project, setCenter, zoomIn, zoomOut, fitView, addNodes, setNodes: setNodesHook } = useReactFlow(); const { project, setCenter, zoomIn, zoomOut, fitView, addNodes, setNodes: setNodesHook } = useReactFlow();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => { (evt: MouseEvent) => {
const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 }); const projectedPosition = project({ x: evt.clientX, y: evt.clientY - 40 });
setNodes((nds) => setNodes((nds) =>
@@ -53,8 +53,8 @@ const UseZoomPanHelperFlow = () => {
); );
const onNodeClick = useCallback( const onNodeClick = useCallback(
(_, element) => { (_: MouseEvent, node: Node) => {
const { x, y } = element.position; const { x, y } = node.position;
setCenter(x, y, { zoom: 1, duration: 1200 }); setCenter(x, y, { zoom: 1, duration: 1200 });
}, },
[setCenter] [setCenter]
@@ -72,9 +72,7 @@ const UseZoomPanHelperFlow = () => {
addNodes(newNode); addNodes(newNode);
}, [addNodes]); }, [addNodes]);
const onResetNodes = useCallback(() => { const onResetNodes = useCallback(() => setNodesHook(initialNodes), [setNodesHook]);
setNodesHook(initialNodes);
}, [setNodesHook]);
return ( return (
<ReactFlow <ReactFlow
+5 -5
View File
@@ -1,4 +1,4 @@
import { useCallback, CSSProperties } from 'react'; import { useCallback, CSSProperties, MouseEvent } from 'react';
import ReactFlow, { import ReactFlow, {
NodeTypes, NodeTypes,
@@ -44,7 +44,7 @@ const UpdateNodeInternalsFlow = () => {
const { project } = useReactFlow(); const { project } = useReactFlow();
const onPaneClick = useCallback( const onPaneClick = useCallback(
(evt) => (evt: MouseEvent) =>
setNodes((nds) => setNodes((nds) =>
nds.concat({ nds.concat({
id: getId(), id: getId(),
@@ -54,7 +54,7 @@ const UpdateNodeInternalsFlow = () => {
sourcePosition: Position.Right, sourcePosition: Position.Right,
}) })
), ),
[project] [project, setNodes]
); );
const toggleHandleCount = useCallback(() => { const toggleHandleCount = useCallback(() => {
@@ -63,7 +63,7 @@ const UpdateNodeInternalsFlow = () => {
return { ...node, data: { ...node.data, handleCount: node.data?.handleCount === 1 ? 2 : 1 } }; return { ...node, data: { ...node.data, handleCount: node.data?.handleCount === 1 ? 2 : 1 } };
}) })
); );
}, []); }, [setNodes]);
const toggleHandlePosition = useCallback(() => { const toggleHandlePosition = useCallback(() => {
setNodes((nds) => setNodes((nds) =>
@@ -71,7 +71,7 @@ const UpdateNodeInternalsFlow = () => {
return { ...node, data: { ...node.data, handlePosition: node.data?.handlePosition === 0 ? 1 : 0 } }; return { ...node, data: { ...node.data, handlePosition: node.data?.handlePosition === 0 ? 1 : 0 } };
}) })
); );
}, []); }, [setNodes]);
const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]); const updateNode = useCallback(() => updateNodeInternals('1'), [updateNodeInternals]);
+7 -4
View File
@@ -1,6 +1,7 @@
import { ChangeEvent } from 'react'; import { ChangeEvent } from 'react';
import ReactDOM from 'react-dom'; import { createRoot } from 'react-dom/client';
import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import Basic from './Basic'; import Basic from './Basic';
import ControlledUncontrolled from './ControlledUncontrolled'; import ControlledUncontrolled from './ControlledUncontrolled';
import CustomConnectionLine from './CustomConnectionLine'; import CustomConnectionLine from './CustomConnectionLine';
@@ -187,7 +188,10 @@ const Header = () => {
); );
}; };
ReactDOM.render( const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<BrowserRouter> <BrowserRouter>
<Header /> <Header />
<Routes> <Routes>
@@ -195,6 +199,5 @@ ReactDOM.render(
<Route path={route.path} key={route.path} element={<route.component />} /> <Route path={route.path} key={route.path} element={<route.component />} />
))} ))}
</Routes> </Routes>
</BrowserRouter>, </BrowserRouter>
document.getElementById('root')
); );
+3 -4
View File
@@ -1,7 +1,6 @@
{ {
"internal": true, "internal": true,
"type": "module", "main": "../dist/nocss/umd/index.js",
"main": "../dist/nocss/index.js", "module": "../dist/nocss/esm/index.js",
"module": "../dist/nocss/index.js", "types": "../dist/nocss/esm/index.d.ts"
"types": "../dist/nocss/index.d.ts"
} }
+2463 -1160
View File
File diff suppressed because it is too large Load Diff
+33 -24
View File
@@ -1,6 +1,6 @@
{ {
"name": "react-flow-renderer", "name": "react-flow-renderer",
"version": "10.1.0", "version": "10.2.2",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
}, },
@@ -13,7 +13,13 @@
"import": "./dist/esm/index.js", "import": "./dist/esm/index.js",
"require": "./dist/umd/index.js" "require": "./dist/umd/index.js"
}, },
"./package.json": "./package.json" "./nocss": {
"import": "./dist/nocss/esm/index.js",
"require": "./dist/nocss/umd/index.js"
},
"./package.json": "./package.json",
"./dist/style.css": "./dist/style.css",
"./dist/theme-default.css": "./dist/theme-default.css"
}, },
"types": "dist/esm/index.d.ts", "types": "dist/esm/index.d.ts",
"license": "MIT", "license": "MIT",
@@ -22,11 +28,12 @@
"url": "https://github.com/wbkd/react-flow.git" "url": "https://github.com/wbkd/react-flow.git"
}, },
"scripts": { "scripts": {
"build": "rimraf dist && rollup -c --environment NODE_ENV:production && postcss src/*.css --dir dist", "build": "rimraf dist && rollup -c --environment NODE_ENV:production && npm run css",
"build:test": "rimraf dist && rollup -c --environment NODE_ENV:test && npm run css",
"start": "rollup -w -c", "start": "rollup -w -c",
"start:examples": "cd example && npm start", "start:examples": "cd example && npm start",
"build:examples": "cd example && npm run build", "build:examples": "cd example && npm run build",
"start:testserver": "npm run build && npm run start:examples", "start:testserver": "npm run build:test && npm run start:examples",
"build:dev": "npm install && npm run build && cd example && npm install && npm run build", "build:dev": "npm install && npm run build && cd example && npm install && npm run build",
"dev:wait": "start-server-and-test start:testserver http-get://localhost:3000", "dev:wait": "start-server-and-test start:testserver http-get://localhost:3000",
"test": "BROWSER=none npm run dev:wait test:chrome", "test": "BROWSER=none npm run dev:wait test:chrome",
@@ -37,41 +44,43 @@
"cy:open": "cypress open", "cy:open": "cypress open",
"release": "npm run test && release-it", "release": "npm run test && release-it",
"release:notest": "release-it", "release:notest": "release-it",
"release:next": "release-it --preRelease=next --no-git" "release:next": "release-it --preRelease=next --no-git",
"css": "postcss src/*.css --dir dist"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.17.8", "@babel/runtime": "^7.17.9",
"classcat": "^5.0.3", "classcat": "^5.0.3",
"d3-drag": "^3.0.0", "d3-drag": "^3.0.0",
"d3-selection": "^3.0.0", "d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0", "d3-zoom": "^3.0.0",
"zustand": "^3.7.1" "zustand": "^3.7.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.17.5", "@babel/core": "^7.17.10",
"@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-runtime": "^7.17.10",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.17.10",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7", "@babel/preset-typescript": "^7.16.7",
"@rollup/plugin-babel": "^5.3.1", "@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^21.0.2", "@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.1.3", "@rollup/plugin-node-resolve": "^13.2.1",
"@rollup/plugin-replace": "^4.0.0", "@rollup/plugin-replace": "^4.0.0",
"@types/d3": "^7.1.0", "@types/d3": "^7.1.0",
"@types/react": "^17.0.40", "@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"@types/resize-observer-browser": "^0.1.7", "@types/resize-observer-browser": "^0.1.7",
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.5",
"babel-preset-react-app": "^10.0.1", "babel-preset-react-app": "^10.0.1",
"cypress": "^9.5.1", "cypress": "^9.6.0",
"postcss": "^8.4.8", "postcss": "^8.4.13",
"postcss-cli": "^9.1.0", "postcss-cli": "^9.1.0",
"postcss-nested": "^5.0.6", "postcss-nested": "^5.0.6",
"prettier": "2.5.1", "prettier": "^2.6.2",
"react": "^17.0.2", "react": "^18.0.0",
"react-dom": "^17.0.2", "react-dom": "^18.0.0",
"release-it": "^14.12.5", "release-it": "^14.14.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^2.70.1", "rollup": "^2.71.0",
"rollup-plugin-livereload": "^2.0.5", "rollup-plugin-livereload": "^2.0.5",
"rollup-plugin-postcss": "^4.0.2", "rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-serve": "^1.1.0", "rollup-plugin-serve": "^1.1.0",
@@ -79,11 +88,11 @@
"rollup-plugin-typescript2": "^0.31.2", "rollup-plugin-typescript2": "^0.31.2",
"rollup-plugin-visualizer": "^5.6.0", "rollup-plugin-visualizer": "^5.6.0",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"typescript": "^4.6.2" "typescript": "^4.6.4"
}, },
"peerDependencies": { "peerDependencies": {
"react": "16 || 17", "react": "16 || 17 || 18",
"react-dom": "16 || 17" "react-dom": "16 || 17 || 18"
}, },
"files": [ "files": [
"dist", "dist",
+29 -14
View File
@@ -11,7 +11,7 @@ import { terser } from 'rollup-plugin-terser';
import pkg from './package.json'; import pkg from './package.json';
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
const isTesting = process.env.NODE_ENV === 'testing'; const isTesting = process.env.NODE_ENV === 'test';
const processEnv = isProd || isTesting ? 'production' : 'development'; const processEnv = isProd || isTesting ? 'production' : 'development';
const defaultOutputOptions = { const defaultOutputOptions = {
@@ -20,6 +20,18 @@ const defaultOutputOptions = {
sourcemap: true, sourcemap: true,
}; };
const globals = {
react: 'React',
'react-dom': 'ReactDOM',
classcat: 'cc',
'd3-selection': 'd3',
'd3-zoom': 'd3',
'react-draggable': 'ReactDraggable',
zustand: 'zustand',
'zustand/shallow': 'zustandShallow',
'zustand/context': 'zustandContext',
};
export const baseConfig = ({ outputOptions = {}, injectCSS = true } = {}) => { export const baseConfig = ({ outputOptions = {}, injectCSS = true } = {}) => {
const output = { const output = {
...defaultOutputOptions, ...defaultOutputOptions,
@@ -97,30 +109,33 @@ export const baseConfig = ({ outputOptions = {}, injectCSS = true } = {}) => {
export default isProd && !isTesting export default isProd && !isTesting
? [ ? [
// esm build
baseConfig(), baseConfig(),
// umd build
baseConfig({ baseConfig({
outputOptions: { outputOptions: {
dir: 'dist/umd', dir: 'dist/umd',
format: 'umd', format: 'umd',
exports: 'named', exports: 'named',
name: 'ReactFlow', name: 'ReactFlow',
globals: { globals,
react: 'React',
'react-dom': 'ReactDOM',
classcat: 'cc',
'd3-selection': 'd3',
'd3-zoom': 'd3',
'd3-drag': 'd3',
'react-draggable': 'ReactDraggable',
zustand: 'zustand',
'zustand/shallow': 'zustandShallow',
'zustand/context': 'zustandContext',
},
}, },
}), }),
// nocsss esm build
baseConfig({ baseConfig({
outputOptions: { outputOptions: {
dir: 'dist/nocss', dir: 'dist/nocss/esm',
},
injectCSS: false,
}),
// nocsss umd build
baseConfig({
outputOptions: {
dir: 'dist/nocss/umd',
format: 'umd',
exports: 'named',
name: 'ReactFlow',
globals,
}, },
injectCSS: false, injectCSS: false,
+26 -14
View File
@@ -1,4 +1,4 @@
import React, { memo, useMemo, FC } from 'react'; import React, { memo, FC, useEffect, useState, useRef } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '../../store'; import { useStore } from '../../store';
@@ -20,9 +20,16 @@ const Background: FC<BackgroundProps> = ({
style, style,
className, className,
}) => { }) => {
const ref = useRef<SVGSVGElement>(null);
const [patternId, setPatternId] = useState<string | null>(null);
const [x, y, scale] = useStore(transformSelector); 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)}`, []); useEffect(() => {
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const bgs = document.querySelectorAll('.react-flow__background');
const index = Array.from(bgs).findIndex((bg) => bg === ref.current);
setPatternId(`pattern-${index}`);
}, []);
const bgClasses = cc(['react-flow__background', 'react-flow__container', className]); const bgClasses = cc(['react-flow__background', 'react-flow__container', className]);
const scaledGap = gap * scale; const scaledGap = gap * scale;
@@ -41,18 +48,23 @@ const Background: FC<BackgroundProps> = ({
width: '100%', width: '100%',
height: '100%', height: '100%',
}} }}
ref={ref}
> >
<pattern {patternId && (
id={patternId} <>
x={xOffset} <pattern
y={yOffset} id={patternId}
width={scaledGap} x={xOffset}
height={scaledGap} y={yOffset}
patternUnits="userSpaceOnUse" width={scaledGap}
> height={scaledGap}
{path} patternUnits="userSpaceOnUse"
</pattern> >
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} /> {path}
</pattern>
<rect x="0" y="0" width="100%" height="100%" fill={`url(#${patternId})`} />
</>
)}
</svg> </svg>
); );
}; };
+3 -3
View File
@@ -1,4 +1,4 @@
import React, { memo, useCallback, FC, useEffect, useState } from 'react'; import React, { memo, useCallback, FC, useEffect, useState, PropsWithChildren } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { useStore, useStoreApi } from '../../store'; import { useStore, useStoreApi } from '../../store';
@@ -12,7 +12,7 @@ import UnlockIcon from './Icons/Unlock';
import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types'; import { ControlProps, ControlButtonProps, ReactFlowState } from '../../types';
export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...rest }) => ( export const ControlButton: FC<PropsWithChildren<ControlButtonProps>> = ({ children, className, ...rest }) => (
<button type="button" className={cc(['react-flow__controls-button', className])} {...rest}> <button type="button" className={cc(['react-flow__controls-button', className])} {...rest}>
{children} {children}
</button> </button>
@@ -20,7 +20,7 @@ export const ControlButton: FC<ControlButtonProps> = ({ children, className, ...
const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable; const isInteractiveSelector = (s: ReactFlowState) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable;
const Controls: FC<ControlProps> = ({ const Controls: FC<PropsWithChildren<ControlProps>> = ({
style, style,
showZoom = true, showZoom = true,
showFitView = true, showFitView = true,
@@ -2,7 +2,9 @@ import React, { FC, PropsWithChildren } from 'react';
import { Provider, createStore } from '../../store'; import { Provider, createStore } from '../../store';
const ReactFlowProvider: FC<PropsWithChildren<{}>> = ({ children }) => <Provider createStore={createStore}>{children}</Provider>; const ReactFlowProvider: FC<PropsWithChildren<{}>> = ({ children }) => (
<Provider createStore={createStore}>{children}</Provider>
);
ReactFlowProvider.displayName = 'ReactFlowProvider'; ReactFlowProvider.displayName = 'ReactFlowProvider';
+2 -1
View File
@@ -10,7 +10,8 @@ type AttributionProps = {
function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) { function Attribution({ proOptions, position = 'bottom-right' }: AttributionProps) {
if ( if (
(proOptions?.account === 'paid-sponsor' || (proOptions?.account === 'paid-pro' ||
proOptions?.account === 'paid-sponsor' ||
proOptions?.account === 'paid-enterprise' || proOptions?.account === 'paid-enterprise' ||
proOptions?.account === 'paid-custom') && proOptions?.account === 'paid-custom') &&
proOptions?.hideAttribution proOptions?.hideAttribution
+2 -2
View File
@@ -1,9 +1,9 @@
import React, { memo, useRef, useState, useEffect, FC } from 'react'; import React, { memo, useRef, useState, useEffect, FC, PropsWithChildren } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { EdgeTextProps, Rect } from '../../types'; import { EdgeTextProps, Rect } from '../../types';
const EdgeText: FC<EdgeTextProps> = ({ const EdgeText: FC<PropsWithChildren<EdgeTextProps>> = ({
x, x,
y, y,
label, label,
+1 -1
View File
@@ -140,7 +140,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
const handleEdgeUpdater = useCallback( const handleEdgeUpdater = useCallback(
(event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => { (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? target : source; const nodeId = isSourceHandle ? target : source;
const handleId = isSourceHandle ? targetHandleId : sourceHandleId; const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source'; const handleType = isSourceHandle ? 'target' : 'source';
const isValidConnection = () => true; const isValidConnection = () => true;
const isTarget = isSourceHandle; const isTarget = isSourceHandle;
+1 -3
View File
@@ -24,9 +24,7 @@ const selector = (s: ReactFlowState) => ({
transform: s.transform, transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox, selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive, userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals) selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
.filter(([_, n]) => n.selected)
.map(([_, n]) => n),
updateNodePosition: s.updateNodePosition, updateNodePosition: s.updateNodePosition,
}); });
+4 -1
View File
@@ -39,7 +39,10 @@ export function useMarkerSymbol(type: MarkerType) {
const symbolExists = MarkerSymbols.hasOwnProperty(type); const symbolExists = MarkerSymbols.hasOwnProperty(type);
if (!symbolExists) { if (!symbolExists) {
console.warn(`marker type "${type}" doesn't exist.`); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`);
}
return () => null; return () => null;
} }
+16 -4
View File
@@ -15,11 +15,12 @@ import {
OnEdgeUpdateFunc, OnEdgeUpdateFunc,
HandleType, HandleType,
ReactFlowState, ReactFlowState,
EdgeTypesWrapped,
} from '../../types'; } from '../../types';
import useVisibleEdges from '../../hooks/useVisibleEdges'; import useVisibleEdges from '../../hooks/useVisibleEdges';
interface EdgeRendererProps { interface EdgeRendererProps {
edgeTypes: any; edgeTypes: EdgeTypesWrapped;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent; connectionLineComponent?: ConnectionLineComponent;
@@ -36,6 +37,7 @@ interface EdgeRendererProps {
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void; onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number; edgeUpdaterRadius?: number;
noPanClassName?: string; noPanClassName?: string;
elevateEdgesOnSelect: boolean;
} }
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
@@ -64,7 +66,7 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
connectionMode, connectionMode,
nodeInternals, nodeInternals,
} = useStore(selector, shallow); } = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals); const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
if (!width) { if (!width) {
return null; return null;
@@ -106,12 +108,22 @@ const EdgeRenderer = (props: EdgeRendererProps) => {
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle) { if (!sourceHandle) {
console.warn(`couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}`); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
return null; return null;
} }
if (!targetHandle) { if (!targetHandle) {
console.warn(`couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}`); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
return null; return null;
} }
+6 -5
View File
@@ -4,6 +4,7 @@ import wrapEdge from '../../components/Edges/wrapEdge';
import { import {
EdgeProps, EdgeProps,
EdgeTypes, EdgeTypes,
EdgeTypesWrapped,
HandleElement, HandleElement,
NodeHandleBounds, NodeHandleBounds,
NodeInternals, NodeInternals,
@@ -14,10 +15,10 @@ import {
} from '../../types'; } from '../../types';
import { rectToBox } from '../../utils'; import { rectToBox } from '../../utils';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypes; export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypes { export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypes = { const standardTypes: EdgeTypesWrapped = {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>), default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>), straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>), step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
@@ -25,8 +26,8 @@ export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypes {
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>), simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
}; };
const wrappedTypes = {} as EdgeTypes; const wrappedTypes = {} as EdgeTypesWrapped;
const specialTypes: EdgeTypes = Object.keys(edgeTypes) const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k)) .filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => { .reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>); res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
+7 -4
View File
@@ -5,11 +5,12 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer'; import EdgeRenderer from '../EdgeRenderer';
import Viewport from '../Viewport'; import Viewport from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler'; import useOnInitHandler from '../../hooks/useOnInitHandler';
import { NodeTypes, EdgeTypes, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types'; import { NodeTypesWrapped, EdgeTypesWrapped, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types';
export interface GraphViewProps extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges'> { export interface GraphViewProps
nodeTypes: NodeTypes; extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> {
edgeTypes: EdgeTypes; nodeTypes: NodeTypesWrapped;
edgeTypes: EdgeTypesWrapped;
selectionKeyCode: KeyCode | null; selectionKeyCode: KeyCode | null;
deleteKeyCode: KeyCode | null; deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null; multiSelectionKeyCode: KeyCode | null;
@@ -81,6 +82,7 @@ const GraphView = ({
noDragClassName, noDragClassName,
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
elevateEdgesOnSelect,
}: GraphViewProps) => { }: GraphViewProps) => {
useOnInitHandler(onInit); useOnInitHandler(onInit);
@@ -134,6 +136,7 @@ const GraphView = ({
edgeUpdaterRadius={edgeUpdaterRadius} edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor} defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
/> />
<NodeRenderer <NodeRenderer
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
+9 -4
View File
@@ -3,10 +3,10 @@ import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, NodeTypes, Position, ReactFlowState, WrapNodeProps } from '../../types'; import { Node, NodeTypesWrapped, Position, ReactFlowState, WrapNodeProps } from '../../types';
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: NodeTypes; nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
onNodeClick?: (event: MouseEvent, element: Node) => void; onNodeClick?: (event: MouseEvent, element: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void; onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
@@ -66,8 +66,13 @@ const NodeRenderer = (props: NodeRendererProps) => {
{nodes.map((node) => { {nodes.map((node) => {
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) { // @ts-ignore
console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`); if (process.env.NODE_ENV === 'development') {
if (!props.nodeTypes[nodeType]) {
console.warn(
`[React Flow]: Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
}
} }
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
+6 -6
View File
@@ -5,20 +5,20 @@ import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode'; import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode'; import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode'; import wrapNode from '../../components/Nodes/wrapNode';
import { NodeTypes, NodeProps } from '../../types'; import { NodeTypes, NodeProps, NodeTypesWrapped } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypes; export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypes { export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
const standardTypes: NodeTypes = { const standardTypes: NodeTypesWrapped = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>), input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>), default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>), output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>), group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
}; };
const wrappedTypes = {} as NodeTypes; const wrappedTypes = {} as NodeTypesWrapped;
const specialTypes: NodeTypes = Object.keys(nodeTypes) const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k)) .filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => { .reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>); res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
+11 -7
View File
@@ -13,7 +13,9 @@ import {
ConnectionLineType, ConnectionLineType,
ConnectionMode, ConnectionMode,
EdgeTypes, EdgeTypes,
EdgeTypesWrapped,
NodeTypes, NodeTypes,
NodeTypesWrapped,
PanOnScrollMode, PanOnScrollMode,
ReactFlowProps, ReactFlowProps,
ReactFlowRefType, ReactFlowRefType,
@@ -21,7 +23,7 @@ import {
import { createEdgeTypes } from '../EdgeRenderer/utils'; import { createEdgeTypes } from '../EdgeRenderer/utils';
import GraphView from '../GraphView'; import GraphView from '../GraphView';
import { createNodeTypes } from '../NodeRenderer/utils'; import { createNodeTypes } from '../NodeRenderer/utils';
import injectStyle, { useNodeOrEdgeTypes } from './utils'; import { injectStyle, useNodeOrEdgeTypes } from './utils';
import Wrapper from './Wrapper'; import Wrapper from './Wrapper';
if (__INJECT_STYLES__) { if (__INJECT_STYLES__) {
@@ -29,13 +31,13 @@ if (__INJECT_STYLES__) {
injectStyle(theme as unknown as string); injectStyle(theme as unknown as string);
} }
const defaultNodeTypes = { const defaultNodeTypes: NodeTypes = {
input: InputNode, input: InputNode,
default: DefaultNode, default: DefaultNode,
output: OutputNode, output: OutputNode,
}; };
const defaultEdgeTypes = { const defaultEdgeTypes: EdgeTypes = {
default: BezierEdge, default: BezierEdge,
straight: StraightEdge, straight: StraightEdge,
step: StepEdge, step: StepEdge,
@@ -135,12 +137,13 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
attributionPosition, attributionPosition,
proOptions, proOptions,
defaultEdgeOptions, defaultEdgeOptions,
elevateEdgesOnSelect = false,
...rest ...rest
}, },
ref ref
) => { ) => {
const nodeTypesParsed = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypes; const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
const edgeTypesParsed = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypes; const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
const reactFlowClasses = cc(['react-flow', className]); const reactFlowClasses = cc(['react-flow', className]);
return ( return (
@@ -161,8 +164,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag} onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesParsed} nodeTypes={nodeTypesWrapped}
edgeTypes={edgeTypesParsed} edgeTypes={edgeTypesWrapped}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent} connectionLineComponent={connectionLineComponent}
@@ -202,6 +205,7 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName} noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
elevateEdgesOnSelect={elevateEdgesOnSelect}
/> />
<StoreUpdater <StoreUpdater
nodes={nodes} nodes={nodes}
+5 -5
View File
@@ -1,12 +1,12 @@
import { useMemo, useRef } from 'react'; import { useMemo, useRef } from 'react';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
import { EdgeTypes, NodeTypes } from '../../types'; import { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { CreateEdgeTypes } from '../EdgeRenderer/utils'; import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils'; import { CreateNodeTypes } from '../NodeRenderer/utils';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypes; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypes; export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any { export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
const typesKeysRef = useRef<string[] | null>(null); const typesKeysRef = useRef<string[] | null>(null);
@@ -16,7 +16,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
const typeKeys = Object.keys(nodeOrEdgeTypes); const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) { if (shallow(typesKeysRef.current, typeKeys)) {
console.warn( console.warn(
"React Flow: It looks like that you created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them." "[React Flow]: It looks like you have created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them. Help: https://reactflow.dev/error#200"
); );
} }
@@ -28,7 +28,7 @@ export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any
return typesParsed; return typesParsed;
} }
export default function injectStyle(css: string): void { export function injectStyle(css: string): void {
if (!css || typeof document === 'undefined') return; if (!css || typeof document === 'undefined') return;
const head = document.head || document.getElementsByTagName('head')[0]; const head = document.head || document.getElementsByTagName('head')[0];
+7 -2
View File
@@ -16,8 +16,13 @@ function useResizeHandler(rendererNode: MutableRefObject<HTMLDivElement | null>)
const size = getDimensions(rendererNode.current); const size = getDimensions(rendererNode.current);
if (size.height === 0 || size.width === 0) { // @ts-ignore
console.warn('The React Flow parent container needs a width and a height to render the graph.'); if (process.env.NODE_ENV === 'development') {
if (size.height === 0 || size.width === 0) {
console.warn(
'[React Flow]: The React Flow parent container needs a width and a height to render the graph. Help: https://reactflow.dev/error#400'
);
}
} }
store.setState({ width: size.width || 500, height: size.height || 500 }); store.setState({ width: size.width || 500, height: size.height || 500 });
+12 -6
View File
@@ -7,13 +7,19 @@ import { isNumeric } from '../utils';
const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }]; const defaultEdgeTree = [{ level: 0, isMaxLevel: true, edges: [] }];
function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) { function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean = false) {
let maxLevel = -1; let maxLevel = -1;
const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => { const levelLookup = edges.reduce<Record<string, Edge[]>>((tree, edge) => {
const z = isNumeric(edge.zIndex) const hasZIndex = isNumeric(edge.zIndex);
? edge.zIndex let z = hasZIndex ? edge.zIndex! : 0;
: Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0);
if (elevateEdgesOnSelect) {
z = hasZIndex
? edge.zIndex!
: Math.max(nodeInternals.get(edge.source)?.z || 0, nodeInternals.get(edge.target)?.z || 0);
}
if (tree[z]) { if (tree[z]) {
tree[z].push(edge); tree[z].push(edge);
} else { } else {
@@ -42,7 +48,7 @@ function groupEdgesByZLevel(edges: Edge[], nodeInternals: NodeInternals) {
return edgeTree; return edgeTree;
} }
function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals) { function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternals, elevateEdgesOnSelect: boolean) {
const edges = useStore( const edges = useStore(
useCallback( useCallback(
(s: ReactFlowState) => { (s: ReactFlowState) => {
@@ -77,7 +83,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, nodeInternals: NodeInternal
) )
); );
return groupEdgesByZLevel(edges, nodeInternals); return groupEdgesByZLevel(edges, nodeInternals, elevateEdgesOnSelect);
} }
export default useVisibleEdges; export default useVisibleEdges;
+8 -2
View File
@@ -45,9 +45,12 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
nodes.forEach((node) => { nodes.forEach((node) => {
const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0; const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0;
const currInternals = nodeInternals.get(node.id);
const internals: Node = { const internals: Node = {
...nodeInternals.get(node.id), width: currInternals?.width,
height: currInternals?.height,
handleBounds: currInternals?.handleBounds,
...node, ...node,
positionAbsolute: { positionAbsolute: {
x: node.position.x, x: node.position.x,
@@ -148,7 +151,10 @@ export function createPositionChange({
] ]
: currentExtent; : currentExtent;
} else { } else {
console.warn('Only child nodes can use parent extent'); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent; currentExtent = nodeExtent;
} }
} }
+9 -2
View File
@@ -1,4 +1,10 @@
import React, { CSSProperties, HTMLAttributes, MouseEvent as ReactMouseEvent, WheelEvent } from 'react'; import React, {
ButtonHTMLAttributes,
CSSProperties,
HTMLAttributes,
MouseEvent as ReactMouseEvent,
WheelEvent,
} from 'react';
import { import {
OnSelectionChangeFunc, OnSelectionChangeFunc,
@@ -118,6 +124,7 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
connectOnClick?: boolean; connectOnClick?: boolean;
attributionPosition?: AttributionPosition; attributionPosition?: AttributionPosition;
proOptions?: ProOptions; proOptions?: ProOptions;
elevateEdgesOnSelect?: boolean;
} }
export type ReactFlowRefType = HTMLDivElement; export type ReactFlowRefType = HTMLDivElement;
@@ -144,7 +151,7 @@ export interface ControlProps extends HTMLAttributes<HTMLDivElement> {
onInteractiveChange?: (interactiveStatus: boolean) => void; onInteractiveChange?: (interactiveStatus: boolean) => void;
} }
export interface ControlButtonProps extends HTMLAttributes<HTMLButtonElement> {} export interface ControlButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {}
export interface BackgroundProps extends HTMLAttributes<SVGElement> { export interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant; variant?: BackgroundVariant;
+5 -5
View File
@@ -1,4 +1,4 @@
import React, { CSSProperties, HTMLAttributes, ReactNode } from 'react'; import React, { CSSProperties, ComponentType, HTMLAttributes, ReactNode } from 'react';
import { Connection } from './general'; import { Connection } from './general';
import { HandleElement, HandleType } from './handles'; import { HandleElement, HandleType } from './handles';
import { Node } from './nodes'; import { Node } from './nodes';
@@ -101,8 +101,8 @@ export interface WrapEdgeProps<T = any> {
style?: CSSProperties; style?: CSSProperties;
source: string; source: string;
target: string; target: string;
sourceHandleId: string | null; sourceHandleId?: string | null;
targetHandleId: string | null; targetHandleId?: string | null;
sourceX: number; sourceX: number;
sourceY: number; sourceY: number;
targetX: number; targetX: number;
@@ -111,7 +111,7 @@ export interface WrapEdgeProps<T = any> {
targetPosition: Position; targetPosition: Position;
elementsSelectable?: boolean; elementsSelectable?: boolean;
hidden?: boolean; hidden?: boolean;
onEdgeUpdate: OnEdgeUpdateFunc; onEdgeUpdate?: OnEdgeUpdateFunc;
onContextMenu?: EdgeMouseHandler; onContextMenu?: EdgeMouseHandler;
onMouseEnter?: EdgeMouseHandler; onMouseEnter?: EdgeMouseHandler;
onMouseMove?: EdgeMouseHandler; onMouseMove?: EdgeMouseHandler;
@@ -162,7 +162,7 @@ export type ConnectionLineComponentProps = {
sourceHandle?: HandleElement; sourceHandle?: HandleElement;
}; };
export type ConnectionLineComponent = React.ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void; export type OnEdgeUpdateFunc<T = any> = (oldEdge: Edge<T>, newConnection: Connection) => void;
+7 -5
View File
@@ -1,16 +1,18 @@
import { MouseEvent as ReactMouseEvent, ReactNode } from 'react'; import { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } from 'react';
import { Selection as D3Selection, ZoomBehavior } from 'd3'; import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes'; import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate } from './nodes'; import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate, NodeProps, WrapNodeProps } from './nodes';
import { Edge } from './edges'; import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles'; import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.'; import { DefaultEdgeOptions } from '.';
import { ReactFlowInstance } from './instance'; import { ReactFlowInstance } from './instance';
export type NodeTypes = { [key: string]: ReactNode }; export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
export type EdgeTypes = NodeTypes; export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
export type FitView = (fitViewOptions?: FitViewOptions) => void; export type FitView = (fitViewOptions?: FitViewOptions) => void;
+18 -3
View File
@@ -57,7 +57,12 @@ const connectionExists = (edge: Edge, edges: Edge[]) => {
export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => { export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] => {
if (!edgeParams.source || !edgeParams.target) { if (!edgeParams.source || !edgeParams.target) {
console.warn("Can't create edge. An edge needs a source and a target."); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
"[React Flow]: Can't create edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"
);
}
return edges; return edges;
} }
@@ -80,14 +85,24 @@ export const addEdge = (edgeParams: Edge | Connection, edges: Edge[]): Edge[] =>
export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => { export const updateEdge = (oldEdge: Edge, newConnection: Connection, edges: Edge[]): Edge[] => {
if (!newConnection.source || !newConnection.target) { if (!newConnection.source || !newConnection.target) {
console.warn("Can't create new edge. An edge needs a source and a target."); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
"[React Flow]: Can't create a new edge. An edge needs a source and a target. Help: https://reactflow.dev/error#600"
);
}
return edges; return edges;
} }
const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge; const foundEdge = edges.find((e) => e.id === oldEdge.id) as Edge;
if (!foundEdge) { if (!foundEdge) {
console.warn(`The old edge with id=${oldEdge.id} does not exist.`); // @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: The old edge with id=${oldEdge.id} does not exist. Help: https://reactflow.dev/error#700`
);
}
return edges; return edges;
} }