Merge pull request #277 from wbkd/develop

Develop
This commit is contained in:
Moritz
2020-06-02 16:05:48 +02:00
committed by GitHub
26 changed files with 1855 additions and 400 deletions
+64 -19
View File
@@ -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 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.
## 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.
+2 -2
View File
@@ -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');
+4 -1
View File
@@ -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" />
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 } },
+1 -1
View File
@@ -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 = () => {
+1 -3
View File
@@ -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 } },
+3 -3
View File
@@ -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 = [
+5 -4
View File
@@ -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);
+60
View File
@@ -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;
+31
View File
@@ -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;
}
+8
View File
@@ -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;
+5
View File
@@ -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,
+1488 -252
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -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",
+9 -11
View File
@@ -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;
+14 -14
View File
@@ -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}
/>
+2 -2
View File
@@ -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}
+6 -3
View File
@@ -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]}
+19 -1
View File
@@ -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);
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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%);
}
+6
View File
@@ -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;
+14
View File
@@ -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 })),
];
};