@@ -37,7 +37,7 @@ React Flow is a library for building node-based graphs. You can easily implement
|
||||
|
||||
In order to make this library as flexible as possible we don’t 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.
|
||||
|
||||
## Installation
|
||||
# Installation
|
||||
|
||||
```
|
||||
npm install react-flow-renderer
|
||||
@@ -61,17 +61,17 @@ const elements = [
|
||||
const BasicFlow = () => <ReactFlow elements={elements} />;
|
||||
```
|
||||
|
||||
# ReactFlow Component Prop Types
|
||||
# React Flow Component Prop Types
|
||||
|
||||
- `elements`: array of [nodes](#nodes) and [edges](#edges) *(required)*
|
||||
- `onElementClick`: element click handler
|
||||
- `onElementsRemove`: element remove handler
|
||||
- `onNodeDragStart`: node drag start handler
|
||||
- `onNodeDragStop`: node drag stop handler
|
||||
- `onConnect`: connect handler
|
||||
- `onLoad`: editor load handler
|
||||
- `onMove`: move handler
|
||||
- `onSelectionChange`: fired when element selection changes
|
||||
- `onElementClick(element: Node | Edge)`: element click callback
|
||||
- `onElementsRemove(elements: Elements)`: element remove callback
|
||||
- `onNodeDragStart(node: Node)`: node drag start callback
|
||||
- `onNodeDragStop(node: Node)`: node drag stop callback
|
||||
- `onConnect({ source, target })`: connect callback
|
||||
- `onLoad(reactFlowInstance)`: editor load callback
|
||||
- `onMove()`: move callback
|
||||
- `onSelectionChange(elements: Elements)`: fired when element selection changes
|
||||
- `nodeTypes`: object with [node types](#node-types--custom-nodes)
|
||||
- `edgeTypes`: object with [edge types](#edge-types--custom-edges)
|
||||
- `style`: css properties
|
||||
@@ -85,6 +85,50 @@ const BasicFlow = () => <ReactFlow elements={elements} />;
|
||||
- `onlyRenderVisibleNodes`: default: `true`
|
||||
- `isInteractive`: default: `true`. If the graph is not interactive you can't drag any nodes
|
||||
- `selectNodesOnDrag`: default: `true`
|
||||
- `minZoom`: default: `0.5`
|
||||
- `maxZoom`: default: `2`
|
||||
- `defaultZoom`: default: `1`
|
||||
|
||||
## React Flow Instance
|
||||
|
||||
You can receive a `reactFlowInstance` by using the `onLoad` callback:
|
||||
|
||||
```javascript
|
||||
import React from 'react';
|
||||
import ReactFlow from 'react-flow-renderer';
|
||||
|
||||
const onLoad = (reactFlowInstance) => {
|
||||
reactFlowInstance.fitView();
|
||||
}
|
||||
|
||||
const BasicFlow = () => <ReactFlow onLoad={onLoad} elements={[]} />;
|
||||
```
|
||||
|
||||
`reactFlowInstance` has the following functions:
|
||||
|
||||
### project
|
||||
|
||||
Transforms pixel coordinates to the internal React Flow coordinate system
|
||||
|
||||
`project = (position: XYPosition): XYPosition`
|
||||
|
||||
### fitView
|
||||
|
||||
Fits view port so that all nodes are visible
|
||||
|
||||
`fitView = ({ padding }): void`
|
||||
|
||||
### zoomIn
|
||||
|
||||
`zoomIn = (): void`
|
||||
|
||||
### zoomOut
|
||||
|
||||
`zoomOut = (): void`
|
||||
|
||||
### getElements
|
||||
|
||||
`getElements = (): Elements`
|
||||
|
||||
# Nodes
|
||||
|
||||
@@ -167,6 +211,10 @@ const targetHandleWithValidation = (
|
||||
- `style`: css properties
|
||||
- `className`: additional class name
|
||||
|
||||
### Validation
|
||||
|
||||
The handle receives the additional class names `connecting` when the connection line is above the handle and `valid` if the connection is valid. You can find an example which uses these classes [here](/example/src/Validation/index.js).
|
||||
|
||||
### Multiple Handles
|
||||
|
||||
If you need multiple source or target handles you can achieve this by creating a custom node. Normally you just use the id of a node for the `source` or `target` of an edge. If you have multiple source or target handles you need to pass an id to these handles. These ids get then added to the node id, so that you can connect a specific handle. If you have a node with an id = `1` and a handle with an id = `a` you can connect this handle by using the id = `1__a`.
|
||||
@@ -341,10 +389,12 @@ The React Flow wrapper has the className `react-flow`. If you want to change the
|
||||
* `.react-flow__nodesselection` - Nodes selection
|
||||
* `.react-flow__nodesselection-rect ` - Nodes selection rect
|
||||
* `.react-flow__handle` - Handle component
|
||||
* `.bottom` is added when position = 'bottom'
|
||||
* `.top` is added when position = 'top'
|
||||
* `.left` is added when position = 'left'
|
||||
* `.right` is added when position = 'right'
|
||||
* `.react-flow__handle-bottom` is added when position = 'bottom'
|
||||
* `.react-flow__handle-top` is added when position = 'top'
|
||||
* `.react-flow__handle-left` is added when position = 'left'
|
||||
* `.react-flow__handle-right` is added when position = 'right'
|
||||
* `.react-flow__handle-connecting` is added when connection line is above a handle
|
||||
* `.react-flow__handle-valid` is added when connection line is above a handle and the connection is valid
|
||||
* `.react-flow__background` - Background component
|
||||
* `.react-flow__minimap` - Mini map component
|
||||
* `.react-flow__controls` - Controls component
|
||||
@@ -394,11 +444,6 @@ 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.
|
||||
|
||||
|
||||
@@ -127,8 +127,8 @@ describe('Basic Graph Rendering', () => {
|
||||
|
||||
cy.window().then((win) => {
|
||||
cy.get('.react-flow__zoompane')
|
||||
.trigger('mousedown', { which: 1, view: win })
|
||||
.trigger('mousemove', newPosition)
|
||||
.trigger('mousedown', 'topLeft', { which: 1, view: win })
|
||||
.trigger('mousemove', 'bottomLeft')
|
||||
.trigger('mouseup', { force: true, view: win })
|
||||
.then(() => {
|
||||
const styleAfterDrag = Cypress.$('.react-flow__nodes').css('transform');
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import ReactFlow, { removeElements, addEdge, isNode, Background } from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
|
||||
const onLoad = reactFlowInstance => console.log('graph loaded:', reactFlowInstance);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
|
||||
const initialElements = [
|
||||
@@ -48,6 +48,9 @@ const BasicFlow = () => {
|
||||
onConnect={onConnect}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
className="react-flow-basic-example"
|
||||
defaultZoom={1.5}
|
||||
minZoom={0.2}
|
||||
maxZoom={4}
|
||||
>
|
||||
<Background variant="lines" />
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import ColorSelectorNode from './ColorSelectorNode';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
const onLoad = (graph) => console.log('graph loaded:', graph);
|
||||
const onLoad = reactFlowInstance => console.log('graph loaded:', reactFlowInstance);
|
||||
|
||||
const initBgColor = '#f0e742';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import CustomEdge from './CustomEdge';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
const onLoad = (graph) => graph.fitView();
|
||||
const onLoad = reactFlowInstance => reactFlowInstance.fitView();
|
||||
|
||||
const initialElements = [
|
||||
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
import ReactFlow, { removeElements, addEdge, MiniMap, Controls, Background } from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
|
||||
const onLoad = reactFlowInstance => console.log('graph loaded:', reactFlowInstance);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
|
||||
const EmptyFlow = () => {
|
||||
|
||||
@@ -2,9 +2,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
|
||||
|
||||
const onLoad = (graph) => {
|
||||
graph.fitView();
|
||||
};
|
||||
const onLoad = reactFlowInstance => reactFlowInstance.fitView();
|
||||
|
||||
const initialElements = [
|
||||
{ id: '1', sourcePosition: 'right', type: 'input', className: 'dark-node', data: { label: 'Input' }, position: { x: 0, y: 80 } },
|
||||
|
||||
@@ -6,9 +6,9 @@ const onNodeDragStart = node => console.log('drag start', node);
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
const onSelectionChange = elements => console.log('selection change', elements);
|
||||
const onLoad = (graph) => {
|
||||
console.log('graph loaded:', graph);
|
||||
graph.fitView();
|
||||
const onLoad = (reactFlowInstance) => {
|
||||
console.log('graph loaded:', reactFlowInstance);
|
||||
reactFlowInstance.fitView();
|
||||
};
|
||||
|
||||
const initialElements = [
|
||||
|
||||
@@ -3,10 +3,11 @@ import React, { useState } from 'react';
|
||||
import ReactFlow, { removeElements, addEdge, MiniMap, isNode, Controls, Background } from 'react-flow-renderer';
|
||||
import { getElements } from './utils';
|
||||
|
||||
const onLoad = graph => {
|
||||
console.log('graph loaded:', graph);
|
||||
graph.fitView();
|
||||
};
|
||||
const onLoad = reactFlowInstance => {
|
||||
reactFlowInstance.fitView();
|
||||
|
||||
console.log(reactFlowInstance.getElements());
|
||||
}
|
||||
|
||||
const initialElements = getElements(10, 10);
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { addEdge, Handle } from 'react-flow-renderer';
|
||||
|
||||
import './validation.css';
|
||||
|
||||
const initialElements = [
|
||||
{ id: '0', type: 'custominput', position: { x: 0, y: 150 } },
|
||||
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 } },
|
||||
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 } },
|
||||
{ id: 'C', type: 'customnode', position: { x: 250, y:300 } },
|
||||
];
|
||||
|
||||
const CustomInput = () => (
|
||||
<>
|
||||
<div>Only connectable with B</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position="right"
|
||||
isValidConnection={connection => connection.target === 'B'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const CustomNode = ({ id }) => (
|
||||
<>
|
||||
<Handle
|
||||
type="target"
|
||||
position="left"
|
||||
isValidConnection={_ => id === 'B'}
|
||||
/>
|
||||
<div>{id}</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position="right"
|
||||
isValidConnection={_ => id === 'B'}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const HorizontalFlow = () => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onConnect = (params) => setElements(els => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onConnect={onConnect}
|
||||
selectNodesOnDrag={false}
|
||||
onLoad={reactFlowInstance => reactFlowInstance.fitView()}
|
||||
className="validationflow"
|
||||
nodeTypes={{
|
||||
custominput: CustomInput,
|
||||
customnode: CustomNode
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default HorizontalFlow;
|
||||
@@ -0,0 +1,31 @@
|
||||
.validationflow .react-flow__node {
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
color: #555;
|
||||
border: 1px solid #ddd;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.validationflow .react-flow__node-customnode {
|
||||
background: #e6e6e9;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.react-flow__node-custominput .react-flow__handle {
|
||||
background: #e6e6e9;
|
||||
}
|
||||
|
||||
.validationflow .react-flow__node-custominput {
|
||||
background: #fff;
|
||||
|
||||
}
|
||||
|
||||
.validationflow .react-flow__handle-connecting {
|
||||
background: #ff6060;
|
||||
}
|
||||
|
||||
.validationflow .react-flow__handle-valid {
|
||||
background: #55dd99;
|
||||
}
|
||||
@@ -141,6 +141,14 @@ nav a.active:before {
|
||||
color: #f8f8f8;
|
||||
}
|
||||
|
||||
.react-flow__node-selectorNode {
|
||||
font-size: 12px;
|
||||
background: #eee;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
nav {
|
||||
position: relative;
|
||||
|
||||
@@ -9,6 +9,7 @@ import Stress from './Stress';
|
||||
import Inactive from './Inactive';
|
||||
import Empty from './Empty';
|
||||
import Edges from './Edges';
|
||||
import Validation from './Validation';
|
||||
import Horizontal from './Horizontal';
|
||||
|
||||
import './index.css';
|
||||
@@ -33,6 +34,10 @@ const routes = [{
|
||||
path: '/horizontal',
|
||||
component: Horizontal,
|
||||
label: 'Horizontal'
|
||||
}, {
|
||||
path: '/validation',
|
||||
component: Validation,
|
||||
label: 'Validation'
|
||||
}, {
|
||||
path: '/stress',
|
||||
component: Stress,
|
||||
|
||||
Generated
+1488
-252
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -23,7 +23,7 @@
|
||||
"release": "release-it"
|
||||
},
|
||||
"dependencies": {
|
||||
"@welldone-software/why-did-you-render": "^4.2.2",
|
||||
"@welldone-software/why-did-you-render": "^4.2.5",
|
||||
"classnames": "^2.2.6",
|
||||
"d3-selection": "^1.4.1",
|
||||
"d3-zoom": "^1.8.3",
|
||||
@@ -33,9 +33,9 @@
|
||||
"resize-observer": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.6",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/core": "^7.10.2",
|
||||
"@babel/preset-env": "^7.10.2",
|
||||
"@babel/preset-react": "^7.10.1",
|
||||
"@svgr/rollup": "^5.4.0",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/d3": "^5.7.2",
|
||||
@@ -44,13 +44,13 @@
|
||||
"autoprefixer": "^9.8.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"babel-preset-react-app": "^9.1.2",
|
||||
"cypress": "^4.6.0",
|
||||
"cypress": "^4.7.0",
|
||||
"postcss-nested": "^4.2.1",
|
||||
"prettier": "2.0.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.13.1",
|
||||
"release-it": "^13.6.1",
|
||||
"rollup": "^2.10.9",
|
||||
"rollup": "^2.12.0",
|
||||
"rollup-plugin-babel": "^4.4.0",
|
||||
"rollup-plugin-bundle-size": "^1.0.3",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useEffect, useState, CSSProperties } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { ElementId, Node, Transform, HandleElement, Position, ConnectionLineType } from '../../types';
|
||||
import { ElementId, Node, Transform, HandleElement, Position, ConnectionLineType, HandleType } from '../../types';
|
||||
|
||||
interface ConnectionLineProps {
|
||||
connectionSourceId: ElementId;
|
||||
connectionNodeId: ElementId;
|
||||
connectionHandleType: HandleType;
|
||||
connectionPositionX: number;
|
||||
connectionPositionY: number;
|
||||
connectionLineType: ConnectionLineType;
|
||||
@@ -16,7 +17,8 @@ interface ConnectionLineProps {
|
||||
}
|
||||
|
||||
export default ({
|
||||
connectionSourceId,
|
||||
connectionNodeId,
|
||||
connectionHandleType,
|
||||
connectionLineStyle,
|
||||
connectionPositionX,
|
||||
connectionPositionY,
|
||||
@@ -27,8 +29,8 @@ export default ({
|
||||
isInteractive,
|
||||
}: ConnectionLineProps) => {
|
||||
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||
const hasHandleId = connectionSourceId.includes('__');
|
||||
const sourceIdSplitted = connectionSourceId.split('__');
|
||||
const hasHandleId = connectionNodeId.includes('__');
|
||||
const sourceIdSplitted = connectionNodeId.split('__');
|
||||
const nodeId = sourceIdSplitted[0];
|
||||
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
|
||||
|
||||
@@ -42,14 +44,10 @@ export default ({
|
||||
}
|
||||
|
||||
const connectionLineClasses: string = cx('react-flow__connection', className);
|
||||
const hasSource = sourceNode.__rg.handleBounds.source !== null;
|
||||
|
||||
// output nodes don't have source handles so we need to use the target one
|
||||
const handleKey = hasSource ? 'source' : 'target';
|
||||
|
||||
const sourceHandle = handleId
|
||||
? sourceNode.__rg.handleBounds[handleKey].find((d: HandleElement) => d.id === handleId)
|
||||
: sourceNode.__rg.handleBounds[handleKey][0];
|
||||
? sourceNode.__rg.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
|
||||
: sourceNode.__rg.handleBounds[connectionHandleType][0];
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rg.width / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rg.height;
|
||||
const sourceX = sourceNode.__rg.position.x + sourceHandleX;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { memo, MouseEvent as ReactMouseEvent, CSSProperties } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection } from '../../types';
|
||||
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection, SetConnectionId } from '../../types';
|
||||
|
||||
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||
type SetSourceIdFunc = (nodeId: ElementId | null) => void;
|
||||
type SetSourceIdFunc = (params: SetConnectionId) => void;
|
||||
|
||||
interface BaseHandleProps {
|
||||
type: HandleType;
|
||||
nodeId: ElementId;
|
||||
onConnect: OnConnectFunc;
|
||||
position: Position;
|
||||
setSourceId: SetSourceIdFunc;
|
||||
setConnectionNodeId: SetSourceIdFunc;
|
||||
setPosition: (pos: XYPosition) => void;
|
||||
isValidConnection: ValidConnectionFunc;
|
||||
id?: ElementId | boolean;
|
||||
@@ -29,8 +29,8 @@ type Result = {
|
||||
function onMouseDown(
|
||||
evt: ReactMouseEvent,
|
||||
nodeId: ElementId,
|
||||
setSourceId: SetSourceIdFunc,
|
||||
setPosition: (pos: XYPosition) => any,
|
||||
setConnectionNodeId: SetSourceIdFunc,
|
||||
setPosition: (pos: XYPosition) => void,
|
||||
onConnect: OnConnectFunc,
|
||||
isTarget: boolean,
|
||||
isValidConnection: ValidConnectionFunc
|
||||
@@ -48,15 +48,15 @@ function onMouseDown(
|
||||
x: evt.clientX - containerBounds.left,
|
||||
y: evt.clientY - containerBounds.top,
|
||||
});
|
||||
setSourceId(nodeId);
|
||||
setConnectionNodeId({ connectionNodeId: nodeId, connectionHandleType: isTarget ? 'target' : 'source' });
|
||||
|
||||
function resetRecentHandle(): void {
|
||||
if (!recentHoveredHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
recentHoveredHandle.classList.remove('valid');
|
||||
recentHoveredHandle.classList.remove('connecting');
|
||||
recentHoveredHandle.classList.remove('react-flow__handle-valid');
|
||||
recentHoveredHandle.classList.remove('react-flow__handle-connecting');
|
||||
}
|
||||
|
||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||
@@ -107,8 +107,8 @@ function onMouseDown(
|
||||
|
||||
if (!isOwnHandle && elementBelow) {
|
||||
recentHoveredHandle = elementBelow;
|
||||
elementBelow.classList.add('connecting');
|
||||
elementBelow.classList.toggle('valid', isValid);
|
||||
elementBelow.classList.add('react-flow__handle-connecting');
|
||||
elementBelow.classList.toggle('react-flow__handle-valid', isValid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ function onMouseDown(
|
||||
}
|
||||
|
||||
resetRecentHandle();
|
||||
setSourceId(null);
|
||||
setConnectionNodeId({ connectionNodeId: null, connectionHandleType: null });
|
||||
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
@@ -136,7 +136,7 @@ const BaseHandle = memo(
|
||||
nodeId,
|
||||
onConnect,
|
||||
position,
|
||||
setSourceId,
|
||||
setConnectionNodeId,
|
||||
setPosition,
|
||||
className,
|
||||
id = false,
|
||||
@@ -144,7 +144,7 @@ const BaseHandle = memo(
|
||||
...rest
|
||||
}: BaseHandleProps) => {
|
||||
const isTarget = type === 'target';
|
||||
const handleClasses = cx('react-flow__handle', 'nodrag', className, position, {
|
||||
const handleClasses = cx('react-flow__handle', `react-flow__handle-${position}`, 'nodrag', className, {
|
||||
source: !isTarget,
|
||||
target: isTarget,
|
||||
});
|
||||
@@ -157,7 +157,7 @@ const BaseHandle = memo(
|
||||
data-handlepos={position}
|
||||
className={handleClasses}
|
||||
onMouseDown={(evt) =>
|
||||
onMouseDown(evt, nodeIdWithHandleId, setSourceId, setPosition, onConnect, isTarget, isValidConnection)
|
||||
onMouseDown(evt, nodeIdWithHandleId, setConnectionNodeId, setPosition, onConnect, isTarget, isValidConnection)
|
||||
}
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@ const Handle = memo(
|
||||
}: HandleProps) => {
|
||||
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||
const setPosition = useStoreActions((a) => a.setConnectionPosition);
|
||||
const setSourceId = useStoreActions((a) => a.setConnectionSourceId);
|
||||
const setConnectionNodeId = useStoreActions((a) => a.setConnectionNodeId);
|
||||
const onConnectAction = useStoreState((s) => s.onConnect);
|
||||
const onConnectExtended = (params: Connection) => {
|
||||
onConnectAction(params);
|
||||
@@ -29,7 +29,7 @@ const Handle = memo(
|
||||
<BaseHandle
|
||||
nodeId={nodeId}
|
||||
setPosition={setPosition}
|
||||
setSourceId={setSourceId}
|
||||
setConnectionNodeId={setConnectionNodeId}
|
||||
onConnect={onConnectExtended}
|
||||
type={type}
|
||||
position={position}
|
||||
|
||||
@@ -190,7 +190,8 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
|
||||
const [tX, tY, tScale] = useStoreState((s) => s.transform);
|
||||
const edges = useStoreState((s) => s.edges);
|
||||
const nodes = useStoreState((s) => s.nodes);
|
||||
const connectionSourceId = useStoreState((s) => s.connectionSourceId);
|
||||
const connectionNodeId = useStoreState((s) => s.connectionNodeId);
|
||||
const connectionHandleType = useStoreState((s) => s.connectionHandleType);
|
||||
const connectionPosition = useStoreState((s) => s.connectionPosition);
|
||||
const selectedElements = useStoreState((s) => s.selectedElements);
|
||||
const isInteractive = useStoreState((s) => s.isInteractive);
|
||||
@@ -202,15 +203,17 @@ const EdgeRenderer = memo((props: EdgeRendererProps) => {
|
||||
}
|
||||
|
||||
const transformStyle = `translate(${tX},${tY}) scale(${tScale})`;
|
||||
const renderConnectionLine = connectionNodeId && connectionHandleType;
|
||||
|
||||
return (
|
||||
<svg width={width} height={height} className="react-flow__edges">
|
||||
<g transform={transformStyle}>
|
||||
{edges.map((e: Edge) => renderEdge(e, props, nodes, selectedElements, isInteractive))}
|
||||
{connectionSourceId && (
|
||||
{renderConnectionLine && (
|
||||
<ConnectionLine
|
||||
nodes={nodes}
|
||||
connectionSourceId={connectionSourceId}
|
||||
connectionNodeId={connectionNodeId!}
|
||||
connectionHandleType={connectionHandleType!}
|
||||
connectionPositionX={connectionPosition.x}
|
||||
connectionPositionY={connectionPosition.y}
|
||||
transform={[tX, tY, tScale]}
|
||||
|
||||
@@ -11,7 +11,7 @@ import useD3Zoom from '../../hooks/useD3Zoom';
|
||||
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
|
||||
import useElementUpdater from '../../hooks/useElementUpdater';
|
||||
import { getDimensions } from '../../utils';
|
||||
import { fitView, zoomIn, zoomOut, project } from '../../utils/graph';
|
||||
import { fitView, zoomIn, zoomOut, project, getElements } from '../../utils/graph';
|
||||
import {
|
||||
Elements,
|
||||
NodeTypesType,
|
||||
@@ -43,6 +43,9 @@ export interface GraphViewProps {
|
||||
onlyRenderVisibleNodes: boolean;
|
||||
isInteractive: boolean;
|
||||
selectNodesOnDrag: boolean;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
defaultZoom: number;
|
||||
}
|
||||
|
||||
const GraphView = memo(
|
||||
@@ -66,6 +69,9 @@ const GraphView = memo(
|
||||
onlyRenderVisibleNodes,
|
||||
isInteractive,
|
||||
selectNodesOnDrag,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
defaultZoom,
|
||||
}: GraphViewProps) => {
|
||||
const zoomPane = useRef<HTMLDivElement>(null);
|
||||
const rendererNode = useRef<HTMLDivElement>(null);
|
||||
@@ -79,6 +85,9 @@ const GraphView = memo(
|
||||
const setOnConnect = useStoreActions((a) => a.setOnConnect);
|
||||
const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid);
|
||||
const setInteractive = useStoreActions((actions) => actions.setInteractive);
|
||||
const updateTransform = useStoreActions((actions) => actions.updateTransform);
|
||||
const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
|
||||
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||
const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });
|
||||
|
||||
@@ -106,6 +115,10 @@ const GraphView = memo(
|
||||
setOnConnect(onConnect);
|
||||
}
|
||||
|
||||
if (defaultZoom !== 1) {
|
||||
updateTransform({ x: 0, y: 0, k: defaultZoom });
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.onresize = null;
|
||||
};
|
||||
@@ -120,6 +133,7 @@ const GraphView = memo(
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
project,
|
||||
getElements,
|
||||
});
|
||||
}
|
||||
}, [d3Initialised, onLoad]);
|
||||
@@ -132,6 +146,10 @@ const GraphView = memo(
|
||||
setInteractive(isInteractive);
|
||||
}, [isInteractive]);
|
||||
|
||||
useEffect(() => {
|
||||
setMinMaxZoom({ minZoom, maxZoom });
|
||||
}, [minZoom, maxZoom]);
|
||||
|
||||
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
||||
useElementUpdater(elements);
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
||||
onlyRenderVisibleNodes: boolean;
|
||||
isInteractive: boolean;
|
||||
selectNodesOnDrag: boolean;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
defaultZoom: number;
|
||||
}
|
||||
|
||||
const ReactFlow = ({
|
||||
@@ -80,6 +83,9 @@ const ReactFlow = ({
|
||||
onlyRenderVisibleNodes,
|
||||
isInteractive,
|
||||
selectNodesOnDrag,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
defaultZoom,
|
||||
}: ReactFlowProps) => {
|
||||
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
||||
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
|
||||
@@ -107,6 +113,9 @@ const ReactFlow = ({
|
||||
onlyRenderVisibleNodes={onlyRenderVisibleNodes}
|
||||
isInteractive={isInteractive}
|
||||
selectNodesOnDrag={selectNodesOnDrag}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
defaultZoom={defaultZoom}
|
||||
/>
|
||||
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
|
||||
{children}
|
||||
@@ -136,6 +145,9 @@ ReactFlow.defaultProps = {
|
||||
onlyRenderVisibleNodes: true,
|
||||
isInteractive: true,
|
||||
selectNodesOnDrag: true,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
defaultZoom: 1,
|
||||
};
|
||||
|
||||
export default ReactFlow;
|
||||
|
||||
+11
-10
@@ -10,10 +10,6 @@ interface UseD3ZoomParams {
|
||||
onMove?: () => void;
|
||||
}
|
||||
|
||||
const d3ZoomInstance = zoom()
|
||||
.scaleExtent([0.5, 2])
|
||||
.filter(() => !event.button);
|
||||
|
||||
export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => {
|
||||
const transform = useStoreState((s) => s.transform);
|
||||
const d3Selection = useStoreState((s) => s.d3Selection);
|
||||
@@ -24,16 +20,21 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
|
||||
|
||||
useEffect(() => {
|
||||
if (zoomPane.current) {
|
||||
const selection = select(zoomPane.current).call(d3ZoomInstance);
|
||||
initD3({ zoom: d3ZoomInstance, selection });
|
||||
const nextD3ZoomInstance = zoom();
|
||||
const selection = select(zoomPane.current).call(nextD3ZoomInstance);
|
||||
initD3({ zoom: nextD3ZoomInstance, selection });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!d3Zoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionKeyPressed) {
|
||||
d3ZoomInstance.on('zoom', null);
|
||||
d3Zoom.on('zoom', null);
|
||||
} else {
|
||||
d3ZoomInstance.on('zoom', () => {
|
||||
d3Zoom.on('zoom', () => {
|
||||
if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) {
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +54,7 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
|
||||
}
|
||||
|
||||
return () => {
|
||||
d3ZoomInstance.on('zoom', null);
|
||||
d3Zoom.on('zoom', null);
|
||||
};
|
||||
}, [selectionKeyPressed]);
|
||||
}, [selectionKeyPressed, d3Zoom]);
|
||||
};
|
||||
|
||||
+33
-5
@@ -17,6 +17,8 @@ import {
|
||||
XYPosition,
|
||||
OnConnectFunc,
|
||||
SelectionRect,
|
||||
HandleType,
|
||||
SetConnectionId,
|
||||
} from '../types';
|
||||
|
||||
type TransformXYK = {
|
||||
@@ -45,6 +47,11 @@ type D3Init = {
|
||||
selection: D3Selection<Element, unknown, null, undefined>;
|
||||
};
|
||||
|
||||
type SetMinMaxZoom = {
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
};
|
||||
|
||||
type SetSnapGrid = {
|
||||
snapToGrid: boolean;
|
||||
snapGrid: [number, number];
|
||||
@@ -62,6 +69,8 @@ export interface StoreModel {
|
||||
d3Zoom: ZoomBehavior<Element, unknown> | null;
|
||||
d3Selection: D3Selection<Element, unknown, null, undefined> | null;
|
||||
d3Initialised: boolean;
|
||||
minZoom: number;
|
||||
maxZoom: number;
|
||||
|
||||
nodesSelectionActive: boolean;
|
||||
selectionActive: boolean;
|
||||
@@ -69,7 +78,8 @@ export interface StoreModel {
|
||||
|
||||
userSelectionRect: SelectionRect;
|
||||
|
||||
connectionSourceId: ElementId | null;
|
||||
connectionNodeId: ElementId | null;
|
||||
connectionHandleType: HandleType | null;
|
||||
connectionPosition: XYPosition;
|
||||
|
||||
snapToGrid: boolean;
|
||||
@@ -101,11 +111,13 @@ export interface StoreModel {
|
||||
|
||||
initD3: Action<StoreModel, D3Init>;
|
||||
|
||||
setMinMaxZoom: Action<StoreModel, SetMinMaxZoom>;
|
||||
|
||||
setSnapGrid: Action<StoreModel, SetSnapGrid>;
|
||||
|
||||
setConnectionPosition: Action<StoreModel, XYPosition>;
|
||||
|
||||
setConnectionSourceId: Action<StoreModel, ElementId | null>;
|
||||
setConnectionNodeId: Action<StoreModel, SetConnectionId>;
|
||||
|
||||
setInteractive: Action<StoreModel, boolean>;
|
||||
|
||||
@@ -126,6 +138,8 @@ const storeModel: StoreModel = {
|
||||
d3Zoom: null,
|
||||
d3Selection: null,
|
||||
d3Initialised: false,
|
||||
minZoom: 0.5,
|
||||
maxZoom: 2,
|
||||
|
||||
nodesSelectionActive: false,
|
||||
selectionActive: false,
|
||||
@@ -140,7 +154,8 @@ const storeModel: StoreModel = {
|
||||
height: 0,
|
||||
draw: false,
|
||||
},
|
||||
connectionSourceId: null,
|
||||
connectionNodeId: null,
|
||||
connectionHandleType: 'source',
|
||||
connectionPosition: { x: 0, y: 0 },
|
||||
|
||||
snapGrid: [16, 16],
|
||||
@@ -322,16 +337,29 @@ const storeModel: StoreModel = {
|
||||
|
||||
initD3: action((state, { zoom, selection }) => {
|
||||
state.d3Zoom = zoom;
|
||||
|
||||
state.d3Zoom.scaleExtent([state.minZoom, state.maxZoom]);
|
||||
|
||||
state.d3Selection = selection;
|
||||
state.d3Initialised = true;
|
||||
}),
|
||||
|
||||
setMinMaxZoom: action((state, { minZoom, maxZoom }) => {
|
||||
state.minZoom = minZoom;
|
||||
state.maxZoom = maxZoom;
|
||||
|
||||
if (state.d3Zoom) {
|
||||
state.d3Zoom.scaleExtent([minZoom, maxZoom]);
|
||||
}
|
||||
}),
|
||||
|
||||
setConnectionPosition: action((state, position) => {
|
||||
state.connectionPosition = position;
|
||||
}),
|
||||
|
||||
setConnectionSourceId: action((state, sourceId) => {
|
||||
state.connectionSourceId = sourceId;
|
||||
setConnectionNodeId: action((state, { connectionNodeId, connectionHandleType }) => {
|
||||
state.connectionNodeId = connectionNodeId;
|
||||
state.connectionHandleType = connectionHandleType;
|
||||
}),
|
||||
|
||||
setSnapGrid: action((state, { snapToGrid, snapGrid }) => {
|
||||
|
||||
+49
-61
@@ -5,27 +5,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.react-flow__renderer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.react-flow__zoompane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.react-flow__renderer,
|
||||
.react-flow__zoompane,
|
||||
.react-flow__selectionpane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.react-flow__zoompane {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.react-flow__selectionpane {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -92,23 +86,31 @@
|
||||
}
|
||||
|
||||
.react-flow__nodes {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
transform-origin: 0 0;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.react-flow__node {
|
||||
position: absolute;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
pointer-events: all;
|
||||
transform-origin: 0 0;
|
||||
cursor: grab;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.react-flow__node-default,
|
||||
.react-flow__node-input,
|
||||
.react-flow__node-output {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 150px;
|
||||
font-size: 12px;
|
||||
color: #222;
|
||||
text-align: center;
|
||||
|
||||
&.selected,
|
||||
&.selected:hover {
|
||||
@@ -118,34 +120,19 @@
|
||||
&:hover {
|
||||
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
|
||||
.react-flow__node-default {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 150px;
|
||||
background: #ff6060;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.react-flow__node-input {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 150px;
|
||||
background: #9999ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.react-flow__node-output {
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
width: 150px;
|
||||
background: #55dd99;
|
||||
font-size: 12px;
|
||||
|
||||
}
|
||||
|
||||
.react-flow__nodesselection {
|
||||
@@ -171,29 +158,30 @@
|
||||
width: 10px;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
|
||||
&.bottom {
|
||||
top: auto;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.top {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.react-flow__handle-bottom {
|
||||
top: auto;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.react-flow__handle-top {
|
||||
left: 50%;
|
||||
top: 0;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.react-flow__handle-left {
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.react-flow__handle-right {
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
@@ -144,6 +144,7 @@ type OnLoadParams = {
|
||||
zoomOut: () => void;
|
||||
fitView: FitViewFunc;
|
||||
project: ProjectFunc;
|
||||
getElements: () => Elements;
|
||||
};
|
||||
|
||||
export type OnLoadFunc = (params: OnLoadParams) => void;
|
||||
@@ -160,6 +161,11 @@ export enum ConnectionLineType {
|
||||
|
||||
export type OnConnectFunc = (connection: Connection) => void;
|
||||
|
||||
export type SetConnectionId = {
|
||||
connectionNodeId: ElementId | null;
|
||||
connectionHandleType: HandleType | null;
|
||||
};
|
||||
|
||||
export interface HandleElement extends XYPosition, Dimensions {
|
||||
id?: ElementId | null;
|
||||
position: Position;
|
||||
|
||||
@@ -210,3 +210,17 @@ const zoom = (amount: number): void => {
|
||||
export const zoomIn = (): void => zoom(0.2);
|
||||
|
||||
export const zoomOut = (): void => zoom(-0.2);
|
||||
|
||||
export const getElements = (): Elements => {
|
||||
const { nodes, edges } = store.getState();
|
||||
|
||||
return [
|
||||
...nodes.map((node) => {
|
||||
const n = { ...node };
|
||||
|
||||
delete n.__rg;
|
||||
return n;
|
||||
}),
|
||||
...edges.map((e) => ({ ...e })),
|
||||
];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user