Merge pull request #249 from wbkd/develop

Develop
This commit is contained in:
Moritz
2020-05-25 17:20:06 +02:00
committed by GitHub
34 changed files with 437 additions and 440 deletions
+68 -44
View File
@@ -2,7 +2,7 @@
# :ocean: React Flow # :ocean: React Flow
React Flow is a library for building node-based graphs. You can easily implement custom node types and it comes with plugins like a mini-map and graph controls. Feel free to check out the [examples](https://react-flow.netlify.com/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started. 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://react-flow.netlify.com/) or read the [blog post](https://webkid.io/blog/react-flow-node-based-graph-library/) to get started.
- [Key Features](#key-features) - [Key Features](#key-features)
- [Installation](#installation) - [Installation](#installation)
@@ -16,10 +16,11 @@ React Flow is a library for building node-based graphs. You can easily implement
- [Edges](#nodes) - [Edges](#nodes)
- [Options](#options-2) - [Options](#options-2)
- [Edge Types / Custom Edges](#edge-types--custom-edges) - [Edge Types / Custom Edges](#edge-types--custom-edges)
- [Helper Functions](#helper-functions) - [Components](#components)
- [Plugins](#plugins) - [Background](#background)
- [Minimap](#minimap) - [Minimap](#minimap)
- [Controls](#controls) - [Controls](#controls)
- [Helper Functions](#helper-functions)
- [Examples](#examples) - [Examples](#examples)
- [Development](#development) - [Development](#development)
- [Testing](#testing) - [Testing](#testing)
@@ -30,7 +31,7 @@ React Flow is a library for building node-based graphs. You can easily implement
* **Customizable:** Different [node](#node-types--custom-nodes) and [edge types](#edge-types--custom-edges) and support for custom nodes with multiple handles and edges * **Customizable:** Different [node](#node-types--custom-nodes) and [edge types](#edge-types--custom-edges) and support for custom nodes with multiple handles and edges
* **Fast rendering:** Only nodes that have changed are re-rendered and only those that are in the viewport are displayed * **Fast rendering:** Only nodes that have changed are re-rendered and only those that are in the viewport are displayed
* **Utils:** Snap-to-grid, background styles and graph [helper functions](#helper-functions) * **Utils:** Snap-to-grid, background styles and graph [helper functions](#helper-functions)
* **Plugin system:** [Mini map and graph controls](#plugins) * **Components:** [Background, Minimap and graph controls](#components)
* **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/) * **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/)
In order to make this library as flexible as possible we dont do any state updates besides the positions. This means that you need to pass the functions to remove an element or connect nodes by yourself. You can implement your own ones or use the helper functions that come with the library. In order to make this library as flexible as possible we dont do any state updates besides the positions. This means that you need to pass the functions to remove an element or connect nodes by yourself. You can implement your own ones or use the helper functions that come with the library.
@@ -81,10 +82,6 @@ const BasicFlow = () => (
- `connectionLineStyle`: connection style as svg attributes - `connectionLineStyle`: connection style as svg attributes
- `deleteKeyCode`: default: `8` (delete) - `deleteKeyCode`: default: `8` (delete)
- `selectionKeyCode`: default: `16` (shift) - `selectionKeyCode`: default: `16` (shift)
- `showBackground`: default: `true`
- `backgroundGap`: gap size - default: `16`
- `backgroundColor`: color of dots or lines - default: `#eee`
- `backgroundType`: background type = `dots` or `lines` - default: `dots`
- `snapToGrid`: default: `false` - `snapToGrid`: default: `false`
- `snapGrid`: [x, y] array - default: `[16, 16]` - `snapGrid`: [x, y] array - default: `[16, 16]`
- `onlyRenderVisibleNodes`: default: `true` - `onlyRenderVisibleNodes`: default: `true`
@@ -242,47 +239,34 @@ Now you could use the new type `special` for an edge.
The `straight`, `default` and `step` types would still be available unless you overwrote one of them. The `straight`, `default` and `step` types would still be available unless you overwrote one of them.
There is an implementation of a custom edge in the [edges example](/example/src/Edges/index.js). There is an implementation of a custom edge in the [edges example](/example/src/Edges/index.js).
## Helper Functions ## Components
If you want to remove a node or connect two nodes with each other you need to pass a function to `onElementsRemove` and `onConnect`. In order to simplify this process we export some helper functions so that you don't need to implement them: ### Background
React Flow comes with two background variants: **dots** and **lines**. You can use it by passing it as a children to the React Flow component:
```javascript ```javascript
import ReactFlow, { isNode, isEdge, removeElements, addEdge } from 'react-flow-renderer'; import ReactFlow, { Background } from 'react-flow-renderer';
const FlowWithBackground = () => (
<ReactFlow elements={elements}>
<Background
variant="dots"
gap={12}
size={4}
/>
</ReactFlow>
);
``` ```
#### isEdge #### Prop Types
Returns true if element is an edge - `variant`: string - has to be 'dots' or 'lines' - default: `dots`
- `gap`: number - the gap between the dots or lines - default: `16`
`isEdge = (element: Node | Edge): element is Edge` - `size`: number - the radius of the dots or the stroke width of the lines - default: `0.5`
- `color`: string - the color of the dots or lines - default: `#999` for dots, `#eee` for lines
#### isNode - `style`: css properties
- `className`: class name
Returns true if element is a node
`isNode = (element: Node | Edge): element is Node`
#### removeElements
Returns elements without the elements from `elementsToRemove`
`removeElements = (elementsToRemove: Elements, elements: Elements): Elements`
#### addEdge
Returns elements array with added edge
`addEdge = (edgeParams: Edge, elements: Elements): Elements`
#### project
Transforms pixel coordinates to the internal React Flow coordinate system
`project = (position: XYPosition): XYPosition`
You can use these function as seen in [this example](/example/src/Overview/index.js#L40-L41) or use your own ones.
## Plugins
### MiniMap ### MiniMap
@@ -291,7 +275,7 @@ You can use the mini map plugin by passing it as a children to the React Flow co
```javascript ```javascript
import ReactFlow, { MiniMap } from 'react-flow-renderer'; import ReactFlow, { MiniMap } from 'react-flow-renderer';
const GraphWithMiniMap = () => ( const FlowWithMiniMap = () => (
<ReactFlow elements={elements}> <ReactFlow elements={elements}>
<MiniMap <MiniMap
nodeColor={(node) => { nodeColor={(node) => {
@@ -337,6 +321,46 @@ const FlowWithControls = () => (
- `showFitView`: boolean - default: true - `showFitView`: boolean - default: true
- `showInteractive`: boolean - default: true - `showInteractive`: boolean - default: true
## Helper Functions
If you want to remove a node or connect two nodes with each other you need to pass a function to `onElementsRemove` and `onConnect`. In order to simplify this process we export some helper functions so that you don't need to implement them:
```javascript
import ReactFlow, { isNode, isEdge, removeElements, addEdge } from 'react-flow-renderer';
```
#### isEdge
Returns true if element is an edge
`isEdge = (element: Node | Edge): element is Edge`
#### isNode
Returns true if element is a node
`isNode = (element: Node | Edge): element is Node`
#### removeElements
Returns elements without the elements from `elementsToRemove`
`removeElements = (elementsToRemove: Elements, elements: Elements): Elements`
#### addEdge
Returns elements array with added edge
`addEdge = (edgeParams: Edge, elements: Elements): Elements`
#### project
Transforms pixel coordinates to the internal React Flow coordinate system
`project = (position: XYPosition): XYPosition`
You can use these function as seen in [this example](/example/src/Overview/index.js#L40-L41) or use your own ones.
## Examples ## Examples
You can find all examples in the [example](example) folder or check out the live versions: You can find all examples in the [example](example) folder or check out the live versions:
+2 -2
View File
@@ -10,9 +10,9 @@ describe('Basic Graph Rendering', () => {
}); });
it('renders a grid', () => { it('renders a grid', () => {
cy.get('.react-flow__grid'); cy.get('.react-flow__background');
const gridStroke = Cypress.$('.react-flow__grid path').attr('stroke'); const gridStroke = Cypress.$('.react-flow__background path').attr('stroke');
expect(gridStroke).to.equal('#eee'); expect(gridStroke).to.equal('#eee');
}); });
@@ -1,4 +1,4 @@
describe('Rich Graph Rendering', () => { describe('Overview Graph Rendering', () => {
it('renders a graph', () => { it('renders a graph', () => {
cy.visit('/'); cy.visit('/');
@@ -9,9 +9,9 @@ describe('Rich Graph Rendering', () => {
}); });
it('renders a grid', () => { it('renders a grid', () => {
cy.get('.react-flow__grid'); cy.get('.react-flow__background');
const gridStroke = Cypress.$('.react-flow__grid path').attr('fill'); const gridStroke = Cypress.$('.react-flow__background path').attr('fill');
expect(gridStroke).to.equal('#888'); expect(gridStroke).to.equal('#888');
}); });
}); });
+3 -3
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, isNode } from 'react-flow-renderer'; import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer';
const onNodeDragStop = node => console.log('drag stop', node); const onNodeDragStop = node => console.log('drag stop', node);
const onLoad = graphInstance => console.log('graph loaded:', graphInstance); const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
@@ -47,10 +47,10 @@ const BasicFlow = () => {
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
onConnect={onConnect} onConnect={onConnect}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
className="react-flow-basic-example" className="react-flow-basic-example"
> >
<Background variant="lines" />
<button <button
onClick={updatePos} onClick={updatePos}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }} style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
-2
View File
@@ -64,8 +64,6 @@ const CustomNodeFlow = () => {
}} }}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }} connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
connectionLineType="bezier" connectionLineType="bezier"
backgroundColor="#888"
backgroundGap={16}
snapToGrid={true} snapToGrid={true}
snapGrid={[16, 16]} snapGrid={[16, 16]}
> >
+2 -1
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer'; import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
import CustomEdge from './CustomEdge'; import CustomEdge from './CustomEdge';
@@ -50,6 +50,7 @@ const EdgesFlow = () => {
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<Background />
</ReactFlow> </ReactFlow>
); );
} }
+12 -12
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer'; import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
const onNodeDragStop = node => console.log('drag stop', node); const onNodeDragStop = node => console.log('drag stop', node);
const onLoad = graphInstance => console.log('graph loaded:', graphInstance); const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
@@ -29,18 +29,18 @@ const EmptyFlow = () => {
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
onConnect={p => onConnect(p)} onConnect={p => onConnect(p)}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<button <Background variant="lines" />
type="button"
onClick={addRandomNode} <button
style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }} type="button"
> onClick={addRandomNode}
add node style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}
</button> >
add node
</button>
</ReactFlow> </ReactFlow>
); );
} }
+1 -2
View File
@@ -20,12 +20,11 @@ const InactiveFlow = () => {
return ( return (
<ReactFlow <ReactFlow
elements={initialElements} elements={initialElements}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
isInteractive={isInteractive} isInteractive={isInteractive}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
<div <div
style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }} style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}
> >
+7 -4
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer'; import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
const onNodeDragStart = node => console.log('drag start', node); const onNodeDragStart = node => console.log('drag start', node);
const onNodeDragStop = node => console.log('drag stop', node); const onNodeDragStop = node => console.log('drag stop', node);
@@ -59,8 +59,6 @@ const OverviewFlow = () => {
onLoad={onLoad} onLoad={onLoad}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }} connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
connectionLineType="bezier" connectionLineType="bezier"
backgroundColor="#888"
backgroundGap={16}
snapToGrid={true} snapToGrid={true}
snapGrid={[16, 16]} snapGrid={[16, 16]}
> >
@@ -74,11 +72,16 @@ const OverviewFlow = () => {
}} }}
/> />
<Controls /> <Controls />
<Background
color="#888"
gap={16}
/>
<button <button
type="button" type="button"
onClick={addRandomNode} onClick={addRandomNode}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }} style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
className="richexample__add" className="overview-example__add"
> >
add node add node
</button> </button>
+5 -5
View File
@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { removeElements, addEdge, MiniMap, isNode, Controls } from 'react-flow-renderer'; import ReactFlow, { removeElements, addEdge, MiniMap, isNode, Controls, Background } from 'react-flow-renderer';
import { getElements } from './utils'; import { getElements } from './utils';
const onLoad = graph => { const onLoad = graph => {
@@ -40,17 +40,17 @@ const StressGraph = () => {
onLoad={onLoad} onLoad={onLoad}
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
onConnect={onConnect} onConnect={onConnect}
style={{ width: '100%', height: '100%' }}
backgroundType="lines"
> >
<MiniMap />
<Controls />
<Background />
<button <button
onClick={updatePos} onClick={updatePos}
style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }} style={{ position: 'absolute', right: 10, top: 30, zIndex: 4 }}
> >
change pos change pos
</button> </button>
<MiniMap />
<Controls />
</ReactFlow> </ReactFlow>
); );
} }
+24 -12
View File
@@ -5,11 +5,14 @@ body {
color: #111; color: #111;
} }
html, body { html,
body {
margin: 0; margin: 0;
} }
html, body, #root { html,
body,
#root {
height: 100%; height: 100%;
} }
@@ -32,7 +35,10 @@ header {
line-height: 1; line-height: 1;
} }
header a, header a:focus, header a:active, header a:visited { header a,
header a:focus,
header a:active,
header a:visited {
color: #111; color: #111;
} }
@@ -58,7 +64,7 @@ nav {
z-index: 10; z-index: 10;
left: 0; left: 0;
padding: 3%; padding: 3%;
background: rgba(255,255,255,0.9); background: rgba(255, 255, 255, 0.9);
} }
nav.is-open { nav.is-open {
@@ -77,7 +83,10 @@ nav a {
margin-bottom: 5px; margin-bottom: 5px;
} }
nav a, nav a:focus, nav a:active, nav a:visited { nav a,
nav a:focus,
nav a:active,
nav a:visited {
color: #fff; color: #fff;
} }
@@ -93,8 +102,8 @@ nav a.active:before {
height: 6px; height: 6px;
background: white; background: white;
opacity: 0.5; opacity: 0.5;
left:0; left: 0;
top:50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
} }
@@ -107,7 +116,10 @@ nav a.active:before {
font-size: 12px; font-size: 12px;
} }
.sourcedisplay, .sourcedisplay:focus, .sourcedisplay:active, .sourcedisplay:visited { .sourcedisplay,
.sourcedisplay:focus,
.sourcedisplay:active,
.sourcedisplay:visited {
color: #777; color: #777;
} }
@@ -115,12 +127,12 @@ nav a.active:before {
color: #111; color: #111;
} }
.richexample__add { .overview-example__add {
display: none; display: none;
} }
.react-flow__node a { .react-flow__node a {
font-weight :700; font-weight: 700;
color: #111; color: #111;
} }
@@ -146,7 +158,7 @@ nav a.active:before {
display: none; display: none;
} }
.richexample__add { .overview-example__add {
display: block; display: block;
} }
} }
@@ -0,0 +1,54 @@
import React, { memo, HTMLAttributes, CSSProperties } from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { BackgroundVariant } from '../../types';
import { createGridLinesPath, createGridDotsPath } from './utils';
interface BackgroundProps extends HTMLAttributes<SVGElement> {
variant?: BackgroundVariant;
gap?: number;
color?: string;
size?: number;
}
const baseStyles: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
};
const defaultColors = {
[BackgroundVariant.Dots]: '#999',
[BackgroundVariant.Lines]: '#eee',
};
const Background = memo(
({ variant = BackgroundVariant.Dots, gap = 24, size = 0.5, color, style = {}, className = '' }: BackgroundProps) => {
const width = useStoreState((s) => s.width);
const height = useStoreState((s) => s.height);
const [x, y, scale] = useStoreState((s) => s.transform);
const bgClasses = classnames('react-flow__background', className);
const bgColor = color ? color : defaultColors[variant];
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const isLines = variant === BackgroundVariant.Lines;
const path = isLines
? createGridLinesPath(width, height, xOffset, yOffset, scaledGap)
: createGridDotsPath(width, height, xOffset, yOffset, scaledGap, size);
const fill = isLines ? 'none' : bgColor;
const stroke = isLines ? bgColor : 'none';
return (
<svg width={width} height={height} style={{ ...baseStyles, ...style }} className={bgClasses}>
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
</svg>
);
}
);
Background.displayName = 'Background';
export default Background;
@@ -0,0 +1,37 @@
export const createGridLinesPath = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const xValues = Array.from({ length: lineCountX }, (_, i) => `M${i * gap + xOffset} 0 V${height}`);
const yValues = Array.from({ length: lineCountY }, (_, i) => `M0 ${i * gap + yOffset} H${width}`);
return [...xValues, ...yValues].join(' ');
};
export const createGridDotsPath = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number,
size: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const values = Array.from({ length: lineCountX }, (_, col) => {
const x = col * gap + xOffset;
return Array.from({ length: lineCountY }, (_, row) => {
const y = row * gap + yOffset;
return `M${x} ${y - size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
}).join(' ');
});
return values.join(' ');
};
@@ -25,7 +25,7 @@ interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
const Controls = ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => { const Controls = ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => {
const setInteractive = useStoreActions((actions) => actions.setInteractive); const setInteractive = useStoreActions((actions) => actions.setInteractive);
const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive })); const isInteractive = useStoreState((s) => s.isInteractive);
const mapClasses: string = classnames('react-flow__controls', className); const mapClasses: string = classnames('react-flow__controls', className);
return ( return (
@@ -30,27 +30,23 @@ const MiniMap = ({
nodeBorderRadius = 5, nodeBorderRadius = 5,
maskColor = 'rgba(10, 10, 10, .25)', maskColor = 'rgba(10, 10, 10, .25)',
}: MiniMapProps) => { }: MiniMapProps) => {
const state = useStoreState(({ width, height, nodes, transform: [tX, tY, tScale] }) => ({ const containerWidth = useStoreState((s) => s.width);
width, const containerHeight = useStoreState((s) => s.height);
height, const [tX, tY, tScale] = useStoreState((s) => s.transform);
nodes, const nodes = useStoreState((s) => s.nodes);
tX,
tY,
tScale,
}));
const mapClasses = classnames('react-flow__minimap', className); const mapClasses = classnames('react-flow__minimap', className);
const elementWidth = (style.width || baseStyle.width)! as number; const elementWidth = (style.width || baseStyle.width)! as number;
const elementHeight = (style.height || baseStyle.height)! as number; const elementHeight = (style.height || baseStyle.height)! as number;
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc; const nodeColorFunc = (nodeColor instanceof Function ? nodeColor : () => nodeColor) as StringFunc;
const hasNodes = state.nodes && state.nodes.length; const hasNodes = nodes && nodes.length;
const bb = getRectOfNodes(state.nodes); const bb = getRectOfNodes(nodes);
const viewBB: Rect = { const viewBB: Rect = {
x: -state.tX / state.tScale, x: -tX / tScale,
y: -state.tY / state.tScale, y: -tY / tScale,
width: state.width / state.tScale, width: containerWidth / tScale,
height: state.height / state.tScale, height: containerHeight / tScale,
}; };
const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB; const boundingRect = hasNodes ? getBoundsofRects(bb, viewBB) : viewBB;
@@ -79,7 +75,7 @@ const MiniMap = ({
}} }}
className={mapClasses} className={mapClasses}
> >
{state.nodes.map((node) => ( {nodes.map((node) => (
<MiniMapNode key={node.id} node={node} color={nodeColorFunc(node)} borderRadius={nodeBorderRadius} /> <MiniMapNode key={node.id} node={node} color={nodeColorFunc(node)} borderRadius={nodeBorderRadius} />
))} ))}
<path <path
+6
View File
@@ -0,0 +1,6 @@
// These components are not used by React Flow directly
// but the user can add them as a child of a React Flow component
export { default as MiniMap } from './MiniMap';
export { default as Controls } from './Controls';
export { default as Background } from './Background';
-108
View File
@@ -1,108 +0,0 @@
import React, { memo, HTMLAttributes, CSSProperties } from 'react';
import classnames from 'classnames';
import { useStoreState } from '../../store/hooks';
import { GridType } from '../../types';
interface GridProps extends HTMLAttributes<SVGElement> {
backgroundType?: GridType;
gap?: number;
color?: string;
size?: number;
}
const baseStyles: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
};
const createGridLines = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const xValues = Array.from(
{ length: lineCountX },
(_, i) => `M${i * gap + xOffset} 0 V${height}`
);
const yValues = Array.from(
{ length: lineCountY },
(_, i) => `M0 ${i * gap + yOffset} H${width}`
);
return [...xValues, ...yValues].join(' ');
};
const createGridDots = (
width: number,
height: number,
xOffset: number,
yOffset: number,
gap: number,
size: number
): string => {
const lineCountX = Math.ceil(width / gap) + 1;
const lineCountY = Math.ceil(height / gap) + 1;
const values = Array.from({ length: lineCountX }, (_, col) => {
const x = col * gap + xOffset;
return Array.from({ length: lineCountY }, (_, row) => {
const y = row * gap + yOffset;
return `M${x} ${y -
size} l${size} ${size} l${-size} ${size} l${-size} ${-size}z`;
}).join(' ');
});
return values.join(' ');
};
const Grid = memo(
({
gap = 24,
color = '#aaa',
size = 0.5,
style = {},
className = '',
backgroundType = GridType.Dots,
}: GridProps) => {
const {
width,
height,
transform: [x, y, scale],
} = useStoreState(s => s);
const gridClasses = classnames('react-flow__grid', className);
const scaledGap = gap * scale;
const xOffset = x % scaledGap;
const yOffset = y % scaledGap;
const isLines = backgroundType === 'lines';
const path = isLines
? createGridLines(width, height, xOffset, yOffset, scaledGap)
: createGridDots(width, height, xOffset, yOffset, scaledGap, size);
const fill = isLines ? 'none' : color;
const stroke = isLines ? color : 'none';
return (
<svg
width={width}
height={height}
style={{ ...baseStyles, ...style }}
className={gridClasses}
>
<path fill={fill} stroke={stroke} strokeWidth={size} d={path} />
</svg>
);
}
);
Grid.displayName = 'Grid';
export default Grid;
+5 -2
View File
@@ -13,7 +13,7 @@ interface EdgeWrapperProps {
labelStyle?: CSSProperties; labelStyle?: CSSProperties;
labelShowBg?: boolean; labelShowBg?: boolean;
labelBgStyle: CSSProperties; labelBgStyle: CSSProperties;
onClick: (edge: Edge) => void; onClick?: (edge: Edge) => void;
animated: boolean; animated: boolean;
selected: boolean; selected: boolean;
isInteractive: boolean; isInteractive: boolean;
@@ -46,7 +46,10 @@ export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
} }
store.dispatch.setSelectedElements({ id, source, target }); store.dispatch.setSelectedElements({ id, source, target });
onClick({ id, source, target, type });
if (onClick) {
onClick({ id, source, target, type });
}
}; };
return ( return (
+2 -4
View File
@@ -15,10 +15,8 @@ const Handle = memo(
...rest ...rest
}: HandleProps) => { }: HandleProps) => {
const nodeId = useContext(NodeIdContext) as ElementId; const nodeId = useContext(NodeIdContext) as ElementId;
const { setPosition, setSourceId } = useStoreActions((a) => ({ const setPosition = useStoreActions((a) => a.setConnectionPosition);
setPosition: a.setConnectionPosition, const setSourceId = useStoreActions((a) => a.setConnectionSourceId);
setSourceId: a.setConnectionSourceId,
}));
const onConnectAction = useStoreState((s) => s.onConnect); const onConnectAction = useStoreState((s) => s.onConnect);
const onConnectExtended = (params: Connection) => { const onConnectExtended = (params: Connection) => {
onConnectAction(params); onConnectAction(params);
+65 -33
View File
@@ -10,16 +10,27 @@ import { Node, XYPosition, Transform, ElementId, NodeComponentProps, WrapNodePro
const getMouseEvent = (evt: MouseEvent | TouchEvent) => const getMouseEvent = (evt: MouseEvent | TouchEvent) =>
typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent); typeof TouchEvent !== 'undefined' && evt instanceof TouchEvent ? evt.touches[0] : (evt as MouseEvent);
const onStart = ( interface OnDragStartParams {
evt: MouseEvent | TouchEvent, evt: MouseEvent | TouchEvent;
onDragStart: (node: Node) => void, id: ElementId;
id: ElementId, type: string;
type: string, data: any;
data: any, setOffset: (pos: XYPosition) => void;
setOffset: (pos: XYPosition) => void, transform: Transform;
transform: Transform, position: XYPosition;
position: XYPosition onNodeDragStart?: (node: Node) => void;
): false | void => { }
const onStart = ({
evt,
onNodeDragStart,
id,
type,
data,
setOffset,
transform,
position,
}: OnDragStartParams): false | void => {
const startEvt = getMouseEvent(evt); const startEvt = getMouseEvent(evt);
const scaledClient: XYPosition = { const scaledClient: XYPosition = {
@@ -33,16 +44,21 @@ const onStart = (
store.dispatch.setSelectedElements({ id, type } as Node); store.dispatch.setSelectedElements({ id, type } as Node);
setOffset({ x: offsetX, y: offsetY }); setOffset({ x: offsetX, y: offsetY });
onDragStart(node);
if (onNodeDragStart) {
onNodeDragStart(node);
}
}; };
const onDrag = ( interface OnDragParams {
evt: MouseEvent | TouchEvent, evt: MouseEvent | TouchEvent;
setDragging: (isDragging: boolean) => void, setDragging: (isDragging: boolean) => void;
id: ElementId, id: ElementId;
offset: XYPosition, offset: XYPosition;
transform: Transform transform: Transform;
): void => { }
const onDrag = ({ evt, setDragging, id, offset, transform }: OnDragParams): void => {
const dragEvt = getMouseEvent(evt); const dragEvt = getMouseEvent(evt);
const scaledClient = { const scaledClient = {
@@ -60,16 +76,27 @@ const onDrag = (
}); });
}; };
const onStop = ( interface OnDragStopParams {
onDragStop: (node: Node) => void, isDragging: boolean;
onClick: (node: Node) => void, setDragging: (isDragging: boolean) => void;
isDragging: boolean, id: ElementId;
setDragging: (isDragging: boolean) => void, type: string;
id: ElementId, position: XYPosition;
type: string, data: any;
position: XYPosition, onNodeDragStop?: (node: Node) => void;
data: any onClick?: (node: Node) => void;
): void => { }
const onStop = ({
onNodeDragStop,
onClick,
isDragging,
setDragging,
id,
type,
position,
data,
}: OnDragStopParams): void => {
const node = { const node = {
id, id,
type, type,
@@ -77,12 +104,15 @@ const onStop = (
data, data,
} as Node; } as Node;
if (!isDragging) { if (!isDragging && onClick) {
return onClick(node); return onClick(node);
} }
setDragging(false); setDragging(false);
onDragStop(node);
if (onNodeDragStop) {
onNodeDragStop(node);
}
}; };
export default (NodeComponent: ComponentType<NodeComponentProps>) => { export default (NodeComponent: ComponentType<NodeComponentProps>) => {
@@ -146,9 +176,11 @@ export default (NodeComponent: ComponentType<NodeComponentProps>) => {
return ( return (
<DraggableCore <DraggableCore
onStart={(evt) => onStart(evt as MouseEvent, onNodeDragStart, id, type, data, setOffset, transform, position)} onStart={(evt) =>
onDrag={(evt) => onDrag(evt as MouseEvent, setDragging, id, offset, transform)} onStart({ evt: evt as MouseEvent, onNodeDragStart, id, type, data, setOffset, transform, position })
onStop={() => onStop(onNodeDragStop, onClick, isDragging, setDragging, id, type, position, data)} }
onDrag={(evt) => onDrag({ evt: evt as MouseEvent, setDragging, id, offset, transform })}
onStop={() => onStop({ onNodeDragStop, onClick, isDragging, setDragging, id, type, position, data })}
scale={transform[2]} scale={transform[2]}
disabled={!isInteractive} disabled={!isInteractive}
cancel=".nodrag" cancel=".nodrag"
+24 -26
View File
@@ -30,30 +30,28 @@ function getStartPositions(nodes: Node[]): StartPositions {
export default memo(() => { export default memo(() => {
const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 }); const [offset, setOffset] = useState<XYPosition>({ x: 0, y: 0 });
const [startPositions, setStartPositions] = useState<StartPositions>({}); const [startPositions, setStartPositions] = useState<StartPositions>({});
const state = useStoreState((s) => ({ const [tX, tY, tScale] = useStoreState((s) => s.transform);
transform: s.transform, const selectedNodesBbox = useStoreState((s) => s.selectedNodesBbox);
selectedNodesBbox: s.selectedNodesBbox, const selectedElements = useStoreState((s) => s.selectedElements);
selectedElements: s.selectedElements, const snapToGrid = useStoreState((s) => s.snapToGrid);
snapToGrid: s.snapToGrid, const snapGrid = useStoreState((s) => s.snapGrid);
snapGrid: s.snapGrid, const nodes = useStoreState((s) => s.nodes);
nodes: s.nodes,
}));
const updateNodePos = useStoreActions((a) => a.updateNodePos); const updateNodePos = useStoreActions((a) => a.updateNodePos);
const [tx, ty, tScale] = state.transform; const position = selectedNodesBbox;
const position = state.selectedNodesBbox; const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number];
const grid = (state.snapToGrid ? state.snapGrid : [1, 1])! as [number, number];
const onStart = (evt: MouseEvent) => { const onStart = (evt: MouseEvent) => {
const scaledClient: XYPosition = { const scaledClient: XYPosition = {
x: evt.clientX / tScale, x: evt.clientX / tScale,
y: evt.clientY / tScale, y: evt.clientY / tScale,
}; };
const offsetX: number = scaledClient.x - position.x - tx; const offsetX: number = scaledClient.x - position.x - tX;
const offsetY: number = scaledClient.y - position.y - ty; const offsetY: number = scaledClient.y - position.y - tY;
const selectedNodes = state.selectedElements const selectedNodes = selectedElements
? (state.selectedElements.filter(isNode) as Node[]).map( ? selectedElements
(selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node .filter(isNode)
) .map((selectedNode) => nodes.find((node) => node.id === selectedNode.id)! as Node)
: []; : [];
const nextStartPositions = getStartPositions(selectedNodes); const nextStartPositions = getStartPositions(selectedNodes);
@@ -70,11 +68,11 @@ export default memo(() => {
y: evt.clientY / tScale, y: evt.clientY / tScale,
}; };
if (state.selectedElements) { if (selectedElements) {
(state.selectedElements.filter(isNode) as Node[]).forEach((node) => { selectedElements.filter(isNode).forEach((node) => {
const pos: XYPosition = { const pos: XYPosition = {
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tX,
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - tY,
}; };
updateNodePos({ id: node.id, pos }); updateNodePos({ id: node.id, pos });
@@ -86,7 +84,7 @@ export default memo(() => {
<div <div
className="react-flow__nodesselection" className="react-flow__nodesselection"
style={{ style={{
transform: `translate(${tx}px,${ty}px) scale(${tScale})`, transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
}} }}
> >
<ReactDraggable <ReactDraggable
@@ -98,10 +96,10 @@ export default memo(() => {
<div <div
className="react-flow__nodesselection-rect" className="react-flow__nodesselection-rect"
style={{ style={{
width: state.selectedNodesBbox.width, width: selectedNodesBbox.width,
height: state.selectedNodesBbox.height, height: selectedNodesBbox.height,
top: state.selectedNodesBbox.y, top: selectedNodesBbox.y,
left: state.selectedNodesBbox.x, left: selectedNodesBbox.x,
}} }}
/> />
</ReactDraggable> </ReactDraggable>
+3 -5
View File
@@ -45,11 +45,9 @@ const SelectionRect = () => {
}; };
export default memo(({ isInteractive }: UserSelectionProps) => { export default memo(({ isInteractive }: UserSelectionProps) => {
const { setUserSelection, updateUserSelection, unsetUserSelection } = useStoreActions((a) => ({ const setUserSelection = useStoreActions((a) => a.setUserSelection);
setUserSelection: a.setUserSelection, const updateUserSelection = useStoreActions((a) => a.updateUserSelection);
updateUserSelection: a.updateUserSelection, const unsetUserSelection = useStoreActions((a) => a.unsetUserSelection);
unsetUserSelection: a.unsetUserSelection,
}));
if (!isInteractive) { if (!isInteractive) {
return null; return null;
+11 -22
View File
@@ -187,23 +187,13 @@ function renderEdge(
} }
const EdgeRenderer = memo((props: EdgeRendererProps) => { const EdgeRenderer = memo((props: EdgeRendererProps) => {
const { const [tX, tY, tScale] = useStoreState((s) => s.transform);
transform, const edges = useStoreState((s) => s.edges);
edges, const nodes = useStoreState((s) => s.nodes);
nodes, const connectionSourceId = useStoreState((s) => s.connectionSourceId);
connectionSourceId, const connectionPosition = useStoreState((s) => s.connectionPosition);
connectionPosition: { x, y }, const selectedElements = useStoreState((s) => s.selectedElements);
selectedElements, const isInteractive = useStoreState((s) => s.isInteractive);
isInteractive,
} = useStoreState((s) => ({
transform: s.transform,
edges: s.edges,
nodes: s.nodes,
connectionSourceId: s.connectionSourceId,
connectionPosition: s.connectionPosition,
selectedElements: s.selectedElements,
isInteractive: s.isInteractive,
}));
const { width, height, connectionLineStyle, connectionLineType } = props; const { width, height, connectionLineStyle, connectionLineType } = props;
@@ -211,8 +201,7 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
return null; return null;
} }
const [tx, ty, tScale] = transform; const transformStyle = `translate(${tX},${tY}) scale(${tScale})`;
const transformStyle = `translate(${tx},${ty}) scale(${tScale})`;
return ( return (
<svg width={width} height={height} className="react-flow__edges"> <svg width={width} height={height} className="react-flow__edges">
@@ -222,9 +211,9 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
<ConnectionLine <ConnectionLine
nodes={nodes} nodes={nodes}
connectionSourceId={connectionSourceId} connectionSourceId={connectionSourceId}
connectionPositionX={x} connectionPositionX={connectionPosition.x}
connectionPositionY={y} connectionPositionY={connectionPosition.y}
transform={transform} transform={[tX, tY, tScale]}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
isInteractive={isInteractive} isInteractive={isInteractive}
+23 -35
View File
@@ -6,34 +6,29 @@ import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer'; import EdgeRenderer from '../EdgeRenderer';
import UserSelection from '../../components/UserSelection'; import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection'; import NodesSelection from '../../components/NodesSelection';
import BackgroundGrid from '../../components/BackgroundGrid';
import useKeyPress from '../../hooks/useKeyPress'; import useKeyPress from '../../hooks/useKeyPress';
import useD3Zoom from '../../hooks/useD3Zoom'; import useD3Zoom from '../../hooks/useD3Zoom';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useElementUpdater from '../../hooks/useElementUpdater'; import useElementUpdater from '../../hooks/useElementUpdater';
import { getDimensions } from '../../utils'; import { getDimensions } from '../../utils';
import { fitView, zoomIn, zoomOut, project } from '../../utils/graph'; import { fitView, zoomIn, zoomOut, project } from '../../utils/graph';
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc, Node, Edge, Connection } from '../../types'; import { Elements, NodeTypesType, EdgeTypesType, OnLoadFunc, Node, Edge, Connection } from '../../types';
export interface GraphViewProps { export interface GraphViewProps {
elements: Elements; elements: Elements;
onElementClick: (element: Node | Edge) => void; onElementClick?: (element: Node | Edge) => void;
onElementsRemove: (elements: Elements) => void; onElementsRemove?: (elements: Elements) => void;
onNodeDragStart: (node: Node) => void; onNodeDragStart?: (node: Node) => void;
onNodeDragStop: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
onConnect: (connection: Connection | Edge) => void; onConnect?: (connection: Connection | Edge) => void;
onLoad: OnLoadFunc; onLoad?: OnLoadFunc;
onMove: () => void; onMove?: () => void;
selectionKeyCode: number; selectionKeyCode: number;
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType; edgeTypes: EdgeTypesType;
connectionLineType: string; connectionLineType: string;
connectionLineStyle: CSSProperties; connectionLineStyle: CSSProperties;
deleteKeyCode: number; deleteKeyCode: number;
showBackground: boolean;
backgroundGap: number;
backgroundColor: string;
backgroundType: GridType;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: [number, number]; snapGrid: [number, number];
onlyRenderVisibleNodes: boolean; onlyRenderVisibleNodes: boolean;
@@ -55,10 +50,6 @@ const GraphView = memo(
onElementsRemove, onElementsRemove,
deleteKeyCode, deleteKeyCode,
elements, elements,
showBackground,
backgroundGap,
backgroundColor,
backgroundType,
onConnect, onConnect,
snapToGrid, snapToGrid,
snapGrid, snapGrid,
@@ -67,14 +58,11 @@ const GraphView = memo(
}: GraphViewProps) => { }: GraphViewProps) => {
const zoomPane = useRef<HTMLDivElement>(null); const zoomPane = useRef<HTMLDivElement>(null);
const rendererNode = useRef<HTMLDivElement>(null); const rendererNode = useRef<HTMLDivElement>(null);
const state = useStoreState((s) => ({ const width = useStoreState((s) => s.width);
width: s.width, const height = useStoreState((s) => s.height);
height: s.height, const d3Initialised = useStoreState((s) => s.d3Initialised);
nodes: s.nodes, const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive);
edges: s.edges,
d3Initialised: s.d3Initialised,
nodesSelectionActive: s.nodesSelectionActive,
}));
const updateSize = useStoreActions((actions) => actions.updateSize); const updateSize = useStoreActions((actions) => actions.updateSize);
const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection); const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection);
const setOnConnect = useStoreActions((a) => a.setOnConnect); const setOnConnect = useStoreActions((a) => a.setOnConnect);
@@ -101,18 +89,21 @@ const GraphView = memo(
useEffect(() => { useEffect(() => {
updateDimensions(); updateDimensions();
setOnConnect(onConnect);
window.onresize = updateDimensions; window.onresize = updateDimensions;
if (onConnect) {
setOnConnect(onConnect);
}
return () => { return () => {
window.onresize = null; window.onresize = null;
}; };
}, []); }, []);
useD3Zoom(zoomPane, onMove, selectionKeyPressed); useD3Zoom({ zoomPane, onMove, selectionKeyPressed });
useEffect(() => { useEffect(() => {
if (state.d3Initialised) { if (d3Initialised && onLoad) {
onLoad({ onLoad({
fitView, fitView,
zoomIn, zoomIn,
@@ -120,7 +111,7 @@ const GraphView = memo(
project, project,
}); });
} }
}, [state.d3Initialised]); }, [d3Initialised, onLoad]);
useEffect(() => { useEffect(() => {
setSnapGrid({ snapToGrid, snapGrid }); setSnapGrid({ snapToGrid, snapGrid });
@@ -135,9 +126,6 @@ const GraphView = memo(
return ( return (
<div className={rendererClasses} ref={rendererNode}> <div className={rendererClasses} ref={rendererNode}>
{showBackground && (
<BackgroundGrid gap={backgroundGap} color={backgroundColor} backgroundType={backgroundType} />
)}
<NodeRenderer <NodeRenderer
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onElementClick={onElementClick} onElementClick={onElementClick}
@@ -146,15 +134,15 @@ const GraphView = memo(
onlyRenderVisibleNodes={onlyRenderVisibleNodes} onlyRenderVisibleNodes={onlyRenderVisibleNodes}
/> />
<EdgeRenderer <EdgeRenderer
width={state.width} width={width}
height={state.height} height={height}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
onElementClick={onElementClick} onElementClick={onElementClick}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
/> />
{selectionKeyPressed && <UserSelection isInteractive={isInteractive} />} {selectionKeyPressed && <UserSelection isInteractive={isInteractive} />}
{state.nodesSelectionActive && <NodesSelection />} {nodesSelectionActive && <NodesSelection />}
<div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} /> <div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} />
</div> </div>
); );
+12 -15
View File
@@ -6,9 +6,9 @@ import { Node, Transform, NodeTypesType, WrapNodeProps, Elements, Edge } from '.
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
onElementClick: (element: Node | Edge) => void; onElementClick?: (element: Node | Edge) => void;
onNodeDragStart: (node: Node) => void; onNodeDragStart?: (node: Node) => void;
onNodeDragStop: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
onlyRenderVisibleNodes?: boolean; onlyRenderVisibleNodes?: boolean;
} }
@@ -22,7 +22,7 @@ function renderNode(
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>; const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
if (!props.nodeTypes[nodeType]) { if (!props.nodeTypes[nodeType]) {
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`); console.warn(`Node type "${nodeType}" not found. Using fallback type "default".`);
} }
const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false; const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false;
@@ -49,18 +49,15 @@ function renderNode(
} }
const NodeRenderer = memo(({ onlyRenderVisibleNodes = true, ...props }: NodeRendererProps) => { const NodeRenderer = memo(({ onlyRenderVisibleNodes = true, ...props }: NodeRendererProps) => {
const { nodes, transform, selectedElements, width, height, isInteractive } = useStoreState((s) => ({ const nodes = useStoreState((s) => s.nodes);
nodes: s.nodes, const transform = useStoreState((s) => s.transform);
transform: s.transform, const selectedElements = useStoreState((s) => s.selectedElements);
selectedElements: s.selectedElements, const width = useStoreState((s) => s.width);
width: s.width, const height = useStoreState((s) => s.height);
height: s.height, const isInteractive = useStoreState((s) => s.isInteractive);
isInteractive: s.isInteractive, const [tX, tY, tScale] = transform;
}));
const [tx, ty, tScale] = transform;
const transformStyle = { const transformStyle = {
transform: `translate(${tx}px,${ty}px) scale(${tScale})`, transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
}; };
const renderNodes = onlyRenderVisibleNodes const renderNodes = onlyRenderVisibleNodes
+9 -32
View File
@@ -20,30 +20,26 @@ import StraightEdge from '../../components/Edges/StraightEdge';
import StepEdge from '../../components/Edges/StepEdge'; import StepEdge from '../../components/Edges/StepEdge';
import { createEdgeTypes } from '../EdgeRenderer/utils'; import { createEdgeTypes } from '../EdgeRenderer/utils';
import store from '../../store'; import store from '../../store';
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc, Node, Edge, Connection } from '../../types'; import { Elements, NodeTypesType, EdgeTypesType, OnLoadFunc, Node, Edge, Connection } from '../../types';
import '../../style.css'; import '../../style.css';
export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> { export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onLoad'> {
elements: Elements; elements: Elements;
onElementClick: (element: Node | Edge) => void; onElementClick?: (element: Node | Edge) => void;
onElementsRemove: (elements: Elements) => void; onElementsRemove?: (elements: Elements) => void;
onNodeDragStart: (node: Node) => void; onNodeDragStart?: (node: Node) => void;
onNodeDragStop: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
onConnect: (connection: Edge | Connection) => void; onConnect?: (connection: Edge | Connection) => void;
onLoad: OnLoadFunc; onLoad?: OnLoadFunc;
onMove: () => void; onMove?: () => void;
onSelectionChange: (elements: Elements | null) => void; onSelectionChange?: (elements: Elements | null) => void;
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType; edgeTypes: EdgeTypesType;
connectionLineType: string; connectionLineType: string;
connectionLineStyle: CSSProperties; connectionLineStyle: CSSProperties;
deleteKeyCode: number; deleteKeyCode: number;
selectionKeyCode: number; selectionKeyCode: number;
showBackground: boolean;
backgroundGap: number;
backgroundColor: string;
backgroundType: GridType;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: [number, number]; snapGrid: [number, number];
onlyRenderVisibleNodes: boolean; onlyRenderVisibleNodes: boolean;
@@ -69,10 +65,6 @@ const ReactFlow = ({
connectionLineStyle, connectionLineStyle,
deleteKeyCode, deleteKeyCode,
selectionKeyCode, selectionKeyCode,
showBackground,
backgroundGap,
backgroundType,
backgroundColor,
snapToGrid, snapToGrid,
snapGrid, snapGrid,
onlyRenderVisibleNodes, onlyRenderVisibleNodes,
@@ -99,10 +91,6 @@ const ReactFlow = ({
deleteKeyCode={deleteKeyCode} deleteKeyCode={deleteKeyCode}
elements={elements} elements={elements}
onConnect={onConnect} onConnect={onConnect}
backgroundColor={backgroundColor}
backgroundGap={backgroundGap}
showBackground={showBackground}
backgroundType={backgroundType}
snapToGrid={snapToGrid} snapToGrid={snapToGrid}
snapGrid={snapGrid} snapGrid={snapGrid}
onlyRenderVisibleNodes={onlyRenderVisibleNodes} onlyRenderVisibleNodes={onlyRenderVisibleNodes}
@@ -118,13 +106,6 @@ const ReactFlow = ({
ReactFlow.displayName = 'ReactFlow'; ReactFlow.displayName = 'ReactFlow';
ReactFlow.defaultProps = { ReactFlow.defaultProps = {
onElementClick: () => {},
onElementsRemove: () => {},
onNodeDragStart: () => {},
onNodeDragStop: () => {},
onConnect: () => {},
onLoad: () => {},
onMove: () => {},
nodeTypes: { nodeTypes: {
input: InputNode, input: InputNode,
default: DefaultNode, default: DefaultNode,
@@ -139,10 +120,6 @@ ReactFlow.defaultProps = {
connectionLineStyle: {}, connectionLineStyle: {},
deleteKeyCode: 8, deleteKeyCode: 8,
selectionKeyCode: 16, selectionKeyCode: 16,
backgroundColor: '#eee',
backgroundGap: 24,
showBackground: true,
backgroundType: GridType.Dots,
snapToGrid: false, snapToGrid: false,
snapGrid: [16, 16], snapGrid: [16, 16],
onlyRenderVisibleNodes: true, onlyRenderVisibleNodes: true,
+22 -20
View File
@@ -1,23 +1,26 @@
import { useEffect, MutableRefObject } from 'react'; import { useEffect, MutableRefObject } from 'react';
import * as d3Zoom from 'd3-zoom'; import { zoom, zoomIdentity } from 'd3-zoom';
import { select, event } from 'd3-selection'; import { select, event } from 'd3-selection';
import { useStoreState, useStoreActions } from '../store/hooks'; import { useStoreState, useStoreActions } from '../store/hooks';
const d3ZoomInstance = d3Zoom interface UseD3ZoomParams {
.zoom() zoomPane: MutableRefObject<Element | null>;
selectionKeyPressed: boolean;
onMove?: () => void;
}
const d3ZoomInstance = zoom()
.scaleExtent([0.5, 2]) .scaleExtent([0.5, 2])
.filter(() => !event.button); .filter(() => !event.button);
export default (zoomPane: MutableRefObject<Element | null>, onMove: () => void, shiftPressed: boolean): void => { export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => {
const state = useStoreState(s => ({ const transform = useStoreState((s) => s.transform);
transform: s.transform, const d3Selection = useStoreState((s) => s.d3Selection);
d3Selection: s.d3Selection, const d3Zoom = useStoreState((s) => s.d3Zoom);
d3Zoom: s.d3Zoom,
}));
const initD3 = useStoreActions(actions => actions.initD3); const initD3 = useStoreActions((actions) => actions.initD3);
const updateTransform = useStoreActions(actions => actions.updateTransform); const updateTransform = useStoreActions((actions) => actions.updateTransform);
useEffect(() => { useEffect(() => {
if (zoomPane.current) { if (zoomPane.current) {
@@ -27,7 +30,7 @@ export default (zoomPane: MutableRefObject<Element | null>, onMove: () => void,
}, []); }, []);
useEffect(() => { useEffect(() => {
if (shiftPressed) { if (selectionKeyPressed) {
d3ZoomInstance.on('zoom', null); d3ZoomInstance.on('zoom', null);
} else { } else {
d3ZoomInstance.on('zoom', () => { d3ZoomInstance.on('zoom', () => {
@@ -37,21 +40,20 @@ export default (zoomPane: MutableRefObject<Element | null>, onMove: () => void,
updateTransform(event.transform); updateTransform(event.transform);
onMove(); if (onMove) {
onMove();
}
}); });
if (state.d3Selection && state.d3Zoom) { if (d3Selection && d3Zoom) {
// we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced // we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced
const graphTransform = d3Zoom.zoomIdentity const graphTransform = zoomIdentity.translate(transform[0], transform[1]).scale(transform[2]);
.translate(state.transform[0], state.transform[1]) d3Selection.call(d3Zoom.transform, graphTransform);
.scale(state.transform[2]);
state.d3Selection.call(state.d3Zoom.transform, graphTransform);
} }
} }
return () => { return () => {
d3ZoomInstance.on('zoom', null); d3ZoomInstance.on('zoom', null);
}; };
}, [shiftPressed]); }, [selectionKeyPressed]);
}; };
+6 -11
View File
@@ -6,13 +6,8 @@ import { parseElement, isNode, isEdge } from '../utils/graph';
import { Elements, Node, Edge } from '../types'; import { Elements, Node, Edge } from '../types';
const useElementUpdater = (elements: Elements): void => { const useElementUpdater = (elements: Elements): void => {
const state = useStoreState((s) => ({ const stateNodes = useStoreState((s) => s.nodes);
nodes: s.nodes, const stateEdges = useStoreState((s) => s.edges);
edges: s.edges,
transform: s.transform,
snapToGrid: s.snapToGrid,
snapGrid: s.snapGrid,
}));
const setNodes = useStoreActions((a) => a.setNodes); const setNodes = useStoreActions((a) => a.setNodes);
const setEdges = useStoreActions((a) => a.setEdges); const setEdges = useStoreActions((a) => a.setEdges);
@@ -22,7 +17,7 @@ const useElementUpdater = (elements: Elements): void => {
const edges: Edge[] = elements.filter(isEdge).map((e) => parseElement(e) as Edge); const edges: Edge[] = elements.filter(isEdge).map((e) => parseElement(e) as Edge);
const nextNodes: Node[] = nodes.map((propNode) => { const nextNodes: Node[] = nodes.map((propNode) => {
const existingNode = state.nodes.find((n) => n.id === propNode.id); const existingNode = stateNodes.find((n) => n.id === propNode.id);
if (existingNode) { if (existingNode) {
const data = !isEqual(existingNode.data, propNode.data) const data = !isEqual(existingNode.data, propNode.data)
@@ -59,8 +54,8 @@ const useElementUpdater = (elements: Elements): void => {
return parseElement(propNode) as Node; return parseElement(propNode) as Node;
}); });
const nodesChanged: boolean = !isEqual(state.nodes, nextNodes); const nodesChanged: boolean = !isEqual(stateNodes, nextNodes);
const edgesChanged: boolean = !isEqual(state.edges, edges); const edgesChanged: boolean = !isEqual(stateEdges, edges);
if (nodesChanged) { if (nodesChanged) {
setNodes(nextNodes); setNodes(nextNodes);
@@ -69,7 +64,7 @@ const useElementUpdater = (elements: Elements): void => {
if (edgesChanged) { if (edgesChanged) {
setEdges(edges); setEdges(edges);
} }
}, [elements, state.nodes, state.edges]); }, [elements, stateNodes, stateEdges]);
}; };
export default useElementUpdater; export default useElementUpdater;
+12 -13
View File
@@ -2,31 +2,30 @@ import { useEffect } from 'react';
import { useStoreState, useStoreActions } from '../store/hooks'; import { useStoreState, useStoreActions } from '../store/hooks';
import useKeyPress from './useKeyPress'; import useKeyPress from './useKeyPress';
import { isEdge, getConnectedEdges } from '../utils/graph'; import { isNode, getConnectedEdges } from '../utils/graph';
import { Elements, Node } from '../types'; import { Elements } from '../types';
interface HookParams { interface HookParams {
deleteKeyCode: number; deleteKeyCode: number;
onElementsRemove: (elements: Elements) => void; onElementsRemove?: (elements: Elements) => void;
} }
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
const state = useStoreState((s) => ({ const selectedElements = useStoreState((s) => s.selectedElements);
selectedElements: s.selectedElements, const edges = useStoreState((s) => s.edges);
edges: s.edges,
}));
const setNodesSelection = useStoreActions((a) => a.setNodesSelection); const setNodesSelection = useStoreActions((a) => a.setNodesSelection);
const deleteKeyPressed = useKeyPress(deleteKeyCode); const deleteKeyPressed = useKeyPress(deleteKeyCode);
useEffect(() => { useEffect(() => {
if (deleteKeyPressed && state.selectedElements) { if (onElementsRemove && deleteKeyPressed && selectedElements) {
let elementsToRemove = state.selectedElements; let elementsToRemove = selectedElements;
// we also want to remove the edges if only one node is selected // we also want to remove the edges if only one node is selected
if (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) { if (selectedElements.length === 1 && isNode(selectedElements[0])) {
const node = (state.selectedElements[0] as unknown) as Node; const node = selectedElements[0];
const connectedEdges = getConnectedEdges([node], state.edges); const connectedEdges = getConnectedEdges([node], edges);
elementsToRemove = [...state.selectedElements, ...connectedEdges]; elementsToRemove = [...selectedElements, ...connectedEdges];
} }
onElementsRemove(elementsToRemove); onElementsRemove(elementsToRemove);
+1 -1
View File
@@ -4,8 +4,8 @@ export default ReactFlow;
export { default as Handle } from './components/Handle'; export { default as Handle } from './components/Handle';
export { default as EdgeText } from './components/Edges/EdgeText'; export { default as EdgeText } from './components/Edges/EdgeText';
export { MiniMap, Controls } from './plugins';
export { isNode, isEdge, removeElements, addEdge, getOutgoers } from './utils/graph'; export { isNode, isEdge, removeElements, addEdge, getOutgoers } from './utils/graph';
export * from './additional-components';
export * from './types'; export * from './types';
-2
View File
@@ -1,2 +0,0 @@
export { default as MiniMap } from './MiniMap';
export { default as Controls } from './Controls';
+6 -5
View File
@@ -54,7 +54,7 @@ export interface Edge {
animated?: boolean; animated?: boolean;
} }
export enum GridType { export enum BackgroundVariant {
Lines = 'lines', Lines = 'lines',
Dots = 'dots', Dots = 'dots',
} }
@@ -108,7 +108,8 @@ export interface NodeComponentProps {
yPos?: number; yPos?: number;
targetPosition?: Position; targetPosition?: Position;
sourcePosition?: Position; sourcePosition?: Position;
onClick?: (node: Node) => void | undefined; onClick?: (node: Node) => void;
onNodeDragStart?: (node: Node) => void;
onNodeDragStop?: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
style?: CSSProperties; style?: CSSProperties;
} }
@@ -122,9 +123,9 @@ export interface WrapNodeProps {
xPos: number; xPos: number;
yPos: number; yPos: number;
isInteractive: boolean; isInteractive: boolean;
onClick: (node: Node) => void | undefined; onClick?: (node: Node) => void;
onNodeDragStart: (node: Node) => void; onNodeDragStart?: (node: Node) => void;
onNodeDragStop: (node: Node) => void; onNodeDragStop?: (node: Node) => void;
style?: CSSProperties; style?: CSSProperties;
sourcePosition?: Position; sourcePosition?: Position;
targetPosition?: Position; targetPosition?: Position;