Merge pull request #214 from wbkd/develop
add horizontal flow, custom edges, interactive control
This commit is contained in:
@@ -199,6 +199,7 @@ edgeTypes={{
|
||||
|
||||
You can now use type `special` for an edge.
|
||||
The `straight`, `default` and `step` types will be still available except you overwrite one of them.
|
||||
There is an implementation of a custom edge in the [edges example](/example/src/Edges/index.js).
|
||||
|
||||
## Helper Functions
|
||||
|
||||
@@ -254,7 +255,7 @@ const GraphWithMiniMap = () => (
|
||||
|
||||
### Controls
|
||||
|
||||
The control panel contains a zoom-in, zoom-out and a fit-view button. You can use it by passing it as a children to your React Flow component:
|
||||
The control panel contains a zoom-in, zoom-out, fit-view and a lock/unlock button. You can use it by passing it as a children to the React Flow component:
|
||||
|
||||
```javascript
|
||||
import ReactFlow, { Controls } from 'react-flow-renderer';
|
||||
@@ -272,6 +273,9 @@ const GraphWithControls = () => (
|
||||
|
||||
- `style`: css properties
|
||||
- `className`: class name
|
||||
- `showZoom`: boolean - default: true
|
||||
- `showFitView`: boolean - default: true
|
||||
- `showInteractive`: boolean - default: true
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -280,6 +284,7 @@ You can find all examples in the [example](example) folder or check out the live
|
||||
- [rich](https://react-flow.netlify.app/)
|
||||
- [basic](https://react-flow.netlify.app/basic)
|
||||
- [custom node](https://react-flow.netlify.app/custom-node)
|
||||
- [horizontal](https://react-flow.netlify.app/horizontal)
|
||||
- [stress](https://react-flow.netlify.app/stress)
|
||||
- [edges](https://react-flow.netlify.app/edges)
|
||||
- [empty](https://react-flow.netlify.app/empty)
|
||||
|
||||
1
assets/icons/lock.svg
Normal file
1
assets/icons/lock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M376 186h-20v-40c0-55-45-100-100-100S156 91 156 146v40h-20c-22.002 0-40 17.998-40 40v200c0 22.002 17.998 40 40 40h240c22.002 0 40-17.998 40-40V226c0-22.002-17.998-40-40-40zM256 368c-22.002 0-40-17.998-40-40s17.998-40 40-40 40 17.998 40 40-17.998 40-40 40zm62.002-182H193.998v-40c0-34.004 28.003-62.002 62.002-62.002 34.004 0 62.002 27.998 62.002 62.002v40z"/></svg>
|
||||
|
After Width: | Height: | Size: 437 B |
1
assets/icons/unlock.svg
Normal file
1
assets/icons/unlock.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="m376,187l-20,0l0,-40c0,-55 -45,-100 -100,-100c-54.002,23.996 -17.999,27.998 4,31.998c21.999,4 58.002,33.998 58.002,68.002l-0.002,0l0,40l-182,0c-22.002,0 -40,17.998 -40,40l0,200c0,22.002 17.998,40 40,40l240,0c22.002,0 40,-17.998 40,-40l0,-200c0,-22.002 -17.998,-40 -40,-40zm-120,182c-22.002,0 -40,-17.998 -40,-40s17.998,-40 40,-40s40,17.998 40,40s-17.998,40 -40,40z"/></svg>
|
||||
|
After Width: | Height: | Size: 445 B |
@@ -13,7 +13,7 @@ describe('Empty Flow Rendering', () => {
|
||||
.get('.react-flow__selectionpane')
|
||||
.trigger('mousedown', 'topLeft', { which: 1, force: true })
|
||||
.trigger('mousemove', 'bottomRight', { which: 1 })
|
||||
.trigger('mouseup', 'bottomRight', { force: true })
|
||||
.trigger('mouseup', 'bottomRight', { force: true });
|
||||
});
|
||||
|
||||
it('renders a control panel', () => {
|
||||
@@ -32,6 +32,10 @@ describe('Empty Flow Rendering', () => {
|
||||
cy.get('.react-flow__controls-fitview').click();
|
||||
});
|
||||
|
||||
it('uses lock view control', () => {
|
||||
cy.get('.react-flow__controls-interactive').click();
|
||||
});
|
||||
|
||||
it('renders an empty mini map', () => {
|
||||
cy.get('.react-flow__minimap');
|
||||
cy.get('.react-flow__minimap-node').should('not.exist');
|
||||
@@ -43,10 +47,7 @@ describe('Empty Flow Rendering', () => {
|
||||
});
|
||||
|
||||
it('connects nodes', () => {
|
||||
cy.get('.react-flow__node')
|
||||
.first()
|
||||
.find('.react-flow__handle.source')
|
||||
.trigger('mousedown', { which: 1 });
|
||||
cy.get('.react-flow__node').first().find('.react-flow__handle.source').trigger('mousedown', { which: 1 });
|
||||
|
||||
cy.get('.react-flow__node')
|
||||
.last()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
|
||||
import Graph, { removeElements, addEdge } from 'react-flow-renderer';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onLoad = graphInstance => console.log('graph loaded:', graphInstance);
|
||||
@@ -11,7 +11,7 @@ const initialElements = [
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true, label: 'edge text' },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true },
|
||||
{ id: 'e1-3', source: '1', target: '3' },
|
||||
];
|
||||
|
||||
@@ -22,7 +22,7 @@ const BasicGraph = () => {
|
||||
const onConnect = (params) => setElements(els => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
<Graph
|
||||
elements={elements}
|
||||
onLoad={onLoad}
|
||||
onElementClick={onElementClick}
|
||||
|
||||
16
example/src/Edges/CustomEdge.js
Normal file
16
example/src/Edges/CustomEdge.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function CustomEdge({
|
||||
id, sourceX, sourceY, targetX, targetY, label, style = {}
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<path id={id} style={style} className="react-flow__edge-path" d={`M ${sourceX},${sourceY}L ${targetX},${targetY}`} />
|
||||
<text>
|
||||
<textPath href={`#${id}`} style={{ fontSize: '12px' }} startOffset="50%" textAnchor="middle">
|
||||
{label}
|
||||
</textPath>
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,8 @@ import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer';
|
||||
|
||||
import CustomEdge from './CustomEdge';
|
||||
|
||||
const onNodeDragStop = node => console.log('drag stop', node);
|
||||
const onElementClick = element => console.log('click', element);
|
||||
const onLoad = (graph) => {
|
||||
@@ -9,14 +11,15 @@ const onLoad = (graph) => {
|
||||
};
|
||||
|
||||
const initialElements = [
|
||||
{ id: '1', type: 'input', data: { label: 'Input Node 1' }, position: { x: 250, y: 0 } },
|
||||
{ id: '1', type: 'input', data: { label: 'Input 1' }, position: { x: 250, y: 0 } },
|
||||
{ id: '2', data: { label: 'Node 2' }, position: { x: 150, y: 100 } },
|
||||
{ id: '3', data: { label: 'Node 3' }, position: { x: 250, y: 200 } },
|
||||
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 300 } },
|
||||
{ id: '3a', data: { label: 'Node 3a' }, position: { x: 150, y: 300 } },
|
||||
{ id: '5', data: { label: 'Node 5' }, position: { x: 450, y: 400 } },
|
||||
{ id: '6', type: 'output', data: { label: 'Output Node 5' }, position: { x: 250, y: 550 } },
|
||||
{ id: '7', type: 'output', data: { label: 'Output Node 6' }, position: { x: 550, y: 550 } },
|
||||
{ id: '5', data: { label: 'Node 5' }, position: { x: 250, y: 400 } },
|
||||
{ id: '6', type: 'output', data: { label: 'Output 6' }, position: { x: 50, y: 550 } },
|
||||
{ id: '7', type: 'output', data: { label: 'Output 7' }, position: { x: 250, y: 550 } },
|
||||
{ id: '8', type: 'output', data: { label: 'Output 8' }, position: { x: 525, y: 600 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', label: 'bezier edge' },
|
||||
{ id: 'e2-3', source: '2', target: '3', type: 'step', label: 'step edge' },
|
||||
{ id: 'e3-4', source: '3', target: '4', type: 'straight', label: 'straight edge' },
|
||||
@@ -24,6 +27,7 @@ const initialElements = [
|
||||
{ id: 'e3-5', source: '4', target: '5', animated: true, label: 'animated styled edge', style: { stroke: 'red' } },
|
||||
{ id: 'e5-6', source: '5', target: '6', label: 'styled label', labelStyle: { fill: 'red', fontWeight: 700 } },
|
||||
{ id: 'e5-7', source: '5', target: '7', label: 'label with styled bg', labelBgStyle: { fill: '#eee', fillOpacity: 0.7 } },
|
||||
{ id: 'e5-8', source: '5', target: '8', type: 'custom', label: 'custom edge' },
|
||||
];
|
||||
|
||||
const EdgesFlow = () => {
|
||||
@@ -42,6 +46,9 @@ const EdgesFlow = () => {
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
onLoad={onLoad}
|
||||
snapToGrid={true}
|
||||
edgeTypes={{
|
||||
custom: CustomEdge
|
||||
}}
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
|
||||
35
example/src/Horizontal/index.js
Normal file
35
example/src/Horizontal/index.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import ReactFlow, { removeElements, addEdge } from 'react-flow-renderer';
|
||||
|
||||
const onLoad = (graph) => {
|
||||
graph.fitView();
|
||||
};
|
||||
|
||||
const initialElements = [
|
||||
{ id: '1', sourcePosition: 'right', type: 'input', data: { label: 'Input' }, position: { x: 0, y: 80 } },
|
||||
{ id: '2', sourcePosition: 'right', targetPosition: 'left', data: { label: 'A Node' }, position: { x: 250, y: 0 } },
|
||||
{ id: '3', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Another node' }, position: { x: 250, y: 160 } },
|
||||
{ id: '4', sourcePosition: 'right', targetPosition: 'left', data: { label: 'Node 4' }, position: { x: 500, y: 80 } },
|
||||
{ id: 'e1-2', source: '1', target: '2', animated: true, },
|
||||
{ id: 'e1-3', source: '1', target: '3', animated: true, },
|
||||
{ id: 'e1-4', source: '2', target: '4', label: 'edge label' }
|
||||
];
|
||||
|
||||
const HorizontalFlow = () => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onElementsRemove = (elementsToRemove) =>
|
||||
setElements(els => removeElements(elementsToRemove, els));
|
||||
const onConnect = (params) => setElements(els => addEdge(params, els));
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onElementsRemove={onElementsRemove}
|
||||
onConnect={onConnect}
|
||||
onLoad={onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default HorizontalFlow;
|
||||
@@ -9,6 +9,7 @@ import Stress from './Stress';
|
||||
import Inactive from './Inactive';
|
||||
import Empty from './Empty';
|
||||
import Edges from './Edges';
|
||||
import Horizontal from './Horizontal';
|
||||
|
||||
import './index.css';
|
||||
|
||||
@@ -28,6 +29,10 @@ const routes = [{
|
||||
path: '/custom-node',
|
||||
component: CustomNode,
|
||||
label: 'CustomNode'
|
||||
}, {
|
||||
path: '/horizontal',
|
||||
component: Horizontal,
|
||||
label: 'Horizontal'
|
||||
}, {
|
||||
path: '/stress',
|
||||
component: Stress,
|
||||
|
||||
25
package.json
25
package.json
@@ -9,6 +9,19 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/wbkd/react-flow.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c --environment NODE_ENV:production",
|
||||
"start": "rollup -w -c",
|
||||
"dev": "npm run build && npm start & cd example && npm start",
|
||||
"start:examples": "npm run build && cd example && npm start",
|
||||
"dev:wait": "start-server-and-test start:examples http-get://localhost:3000",
|
||||
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
|
||||
"cy:run": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"cypress": "npm run dev:wait cy:open",
|
||||
"test": "npm run dev:wait cy:run",
|
||||
"release": "release-it"
|
||||
},
|
||||
"dependencies": {
|
||||
"@welldone-software/why-did-you-render": "^4.2.0",
|
||||
"classnames": "^2.2.6",
|
||||
@@ -56,18 +69,6 @@
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c --environment NODE_ENV:production",
|
||||
"start": "rollup -w -c",
|
||||
"dev": "npm run build && npm start & cd example && npm start",
|
||||
"dev:wait": "start-server-and-test dev http-get://localhost:3000",
|
||||
"build:example": "npm install && npm run build && cd example && npm install && npm run build",
|
||||
"cy:run": "cypress run",
|
||||
"cy:open": "cypress open",
|
||||
"cypress": "npm run dev:wait cy:open",
|
||||
"test": "npm run dev:wait cy:run",
|
||||
"release": "release-it"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
|
||||
@@ -89,6 +89,11 @@ const GraphView = memo(
|
||||
}
|
||||
|
||||
const size = getDimensions(rendererNode.current);
|
||||
|
||||
if (size.height === 0 || size.width === 0) {
|
||||
throw new Error('The React Flow parent container needs a width and a height to render the graph.');
|
||||
}
|
||||
|
||||
updateSize(size);
|
||||
};
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
||||
const ReactFlow = ({
|
||||
style,
|
||||
onElementClick,
|
||||
elements,
|
||||
elements = [],
|
||||
children,
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
|
||||
@@ -3,12 +3,7 @@ import ReactFlow from './container/ReactFlow';
|
||||
export default ReactFlow;
|
||||
|
||||
export { default as Handle } from './components/Handle';
|
||||
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';
|
||||
|
||||
@@ -2,9 +2,13 @@ import React, { CSSProperties } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
||||
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||
|
||||
import PlusIcon from '../../../assets/icons/plus.svg';
|
||||
import MinusIcon from '../../../assets/icons/minus.svg';
|
||||
import FitviewIcon from '../../../assets/icons/fitview.svg';
|
||||
import LockIcon from '../../../assets/icons/lock.svg';
|
||||
import UnlockIcon from '../../../assets/icons/unlock.svg';
|
||||
|
||||
const baseStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
@@ -13,11 +17,18 @@ const baseStyle: CSSProperties = {
|
||||
left: 10,
|
||||
};
|
||||
|
||||
interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
showZoom?: boolean;
|
||||
showFitView?: boolean;
|
||||
showInteractive?: boolean;
|
||||
}
|
||||
|
||||
export default ({ style, className }: ControlProps) => {
|
||||
export default ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => {
|
||||
const mapClasses: string = classnames('react-flow__controls', className);
|
||||
|
||||
const setInteractive = useStoreActions((actions) => actions.setInteractive);
|
||||
const { isInteractive } = useStoreState(({ isInteractive }) => ({ isInteractive }));
|
||||
|
||||
return (
|
||||
<div
|
||||
className={mapClasses}
|
||||
@@ -26,24 +37,29 @@ export default ({ style, className }: ControlProps) => {
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-zoomin"
|
||||
onClick={zoomIn}
|
||||
>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-zoomout"
|
||||
onClick={zoomOut}
|
||||
>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-fitview"
|
||||
onClick={() => fitView()}
|
||||
>
|
||||
<FitviewIcon />
|
||||
</div>
|
||||
{showZoom && (
|
||||
<>
|
||||
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={zoomIn}>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={zoomOut}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{showFitView && (
|
||||
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={() => fitView()}>
|
||||
<FitviewIcon />
|
||||
</div>
|
||||
)}
|
||||
{showInteractive && (
|
||||
<div
|
||||
className="react-flow__controls-button react-flow__controls-interactive"
|
||||
onClick={() => setInteractive(!isInteractive)}
|
||||
>
|
||||
{isInteractive ? <UnlockIcon /> : <LockIcon />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user