Develop (#35)
* refactor(ts): add ReactFlowProps * Refactor/grid.tsx (#24) * chore(deps-dev): bump start-server-and-test from 1.10.4 to 1.10.5 Bumps [start-server-and-test](https://github.com/bahmutov/start-server-and-test) from 1.10.4 to 1.10.5. - [Release notes](https://github.com/bahmutov/start-server-and-test/releases) - [Commits](https://github.com/bahmutov/start-server-and-test/compare/v1.10.4...v1.10.5) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> * chore(deps-dev): bump typescript from 3.6.3 to 3.6.4 Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.6.3 to 3.6.4. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v3.6.3...v3.6.4) Signed-off-by: dependabot-preview[bot] <support@dependabot.com> * refactor: grid.js -> grid.tsx * refactor(bg): remove unused renderer * refactor(connectionline): use ts * refactor(ts): edges * chore(build): update * Refactor/typescript (WIP) (#25) * refactor(store): use ts * refactor(edgewrapper): use ts * fix(handle): provide onConnect default func * refactor(nodeselection): use ts * refactor(userselction): use ts * refactor(plugins): use ts * refactor(hooks): use ts * refactor(nodes): use ts * refactor(edgerenderer): use ts * refactor(graphview): use ts * refactor(utils): rename js to ts * refactor(app): fix ts errors * fix(ts): errors * fix(app): ts errors * refactor(app): ts erros * refactor(app): ts errors * fix(utils): removeElements * feat(example): add empty renderer closes #34 * fix(connect): dont drag node on connect * chore(build): update
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
Vendored
+1470
-2121
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1470
-2121
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Generated
+60
-7924
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,83 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import Graph, { removeElements, addEdge, getOutgoers } from 'react-flow';
|
||||||
|
|
||||||
|
const onNodeDragStop = node => console.log('drag stop', node);
|
||||||
|
|
||||||
|
class App extends PureComponent {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
graphLoaded: false,
|
||||||
|
elements: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(graphInstance) {
|
||||||
|
console.log('graph loaded:', graphInstance);
|
||||||
|
|
||||||
|
this.graphInstance = graphInstance;
|
||||||
|
this.setState({
|
||||||
|
graphLoaded: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onElementClick(element) {
|
||||||
|
console.log('click', element);
|
||||||
|
console.log('outgoers', getOutgoers(element, this.state.elements));
|
||||||
|
}
|
||||||
|
|
||||||
|
onElementsRemove(elementsToRemove) {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
elements: removeElements(elementsToRemove, prevState.elements)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onConnect(params) {
|
||||||
|
console.log('connect', params);
|
||||||
|
this.setState(prevState => ({
|
||||||
|
elements: addEdge(params, prevState.elements)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd() {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
const nodeId = (prevState.elements.length + 1).toString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prevState,
|
||||||
|
elements: prevState.elements.concat({
|
||||||
|
id: nodeId,
|
||||||
|
data: { label: `Node: ${nodeId}` },
|
||||||
|
position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Graph
|
||||||
|
elements={this.state.elements}
|
||||||
|
onLoad={graphInstance => this.onLoad(graphInstance)}
|
||||||
|
onElementClick={element => this.onElementClick(element)}
|
||||||
|
onElementsRemove={elements => this.onElementsRemove(elements)}
|
||||||
|
onConnect={params => this.onConnect(params)}
|
||||||
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
backgroundType="lines"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => this.onAdd()}
|
||||||
|
style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}
|
||||||
|
>
|
||||||
|
add
|
||||||
|
</button>
|
||||||
|
</Graph>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -4,6 +4,7 @@ import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
|||||||
|
|
||||||
import Advanced from './Advanced';
|
import Advanced from './Advanced';
|
||||||
import Basic from './Basic';
|
import Basic from './Basic';
|
||||||
|
import Empty from './Empty';
|
||||||
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
@@ -13,6 +14,9 @@ ReactDOM.render((
|
|||||||
<Route path="/basic">
|
<Route path="/basic">
|
||||||
<Basic />
|
<Basic />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/empty">
|
||||||
|
<Empty />
|
||||||
|
</Route>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Advanced />
|
<Advanced />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
Generated
+14
-3
@@ -1315,6 +1315,12 @@
|
|||||||
"rollup-pluginutils": "^2.8.1"
|
"rollup-pluginutils": "^2.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/classnames": {
|
||||||
|
"version": "2.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.9.tgz",
|
||||||
|
"integrity": "sha512-MNl+rT5UmZeilaPxAVs6YaPC2m6aA8rofviZbhbxpPpl61uKodfdQVsBtgJGTqGizEf02oW3tsVe7FYB8kK14A==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/d3": {
|
"@types/d3": {
|
||||||
"version": "5.7.2",
|
"version": "5.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-5.7.2.tgz",
|
||||||
@@ -1588,9 +1594,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "12.7.11",
|
"version": "12.7.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz",
|
||||||
"integrity": "sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw==",
|
"integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@types/normalize-package-data": {
|
"@types/normalize-package-data": {
|
||||||
@@ -7046,6 +7052,11 @@
|
|||||||
"integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=",
|
"integrity": "sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"resize-observer": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-observer/-/resize-observer-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-D7UFShDm2TgrEDEyeg+/tTEbvOgPWlvPAfJtxiKp+qutu6HowmcGJKjECgGru0PPDIj3SAucn3ZPpOx54fF7DQ=="
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz",
|
||||||
|
|||||||
@@ -13,12 +13,14 @@
|
|||||||
"easy-peasy": "^3.1.0",
|
"easy-peasy": "^3.1.0",
|
||||||
"fast-deep-equal": "^3.0.0-beta.2",
|
"fast-deep-equal": "^3.0.0-beta.2",
|
||||||
"react-draggable": "^4.0.3",
|
"react-draggable": "^4.0.3",
|
||||||
|
"resize-observer": "^1.0.0",
|
||||||
"scheduler": "^0.16.2"
|
"scheduler": "^0.16.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.6.4",
|
"@babel/core": "^7.6.4",
|
||||||
"@babel/preset-env": "^7.6.3",
|
"@babel/preset-env": "^7.6.3",
|
||||||
"@babel/preset-react": "^7.6.3",
|
"@babel/preset-react": "^7.6.3",
|
||||||
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/d3": "^5.7.2",
|
"@types/d3": "^5.7.2",
|
||||||
"@types/react": "^16.9.6",
|
"@types/react": "^16.9.6",
|
||||||
"@types/react-dom": "^16.9.2",
|
"@types/react-dom": "^16.9.2",
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, {memo} from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
import { useStoreState } from '../../store/hooks';
|
||||||
|
import { GridType } from '../../types';
|
||||||
|
|
||||||
|
interface GridProps {
|
||||||
|
backgroundType?: GridType;
|
||||||
|
gap?: number;
|
||||||
|
color?: string;
|
||||||
|
size?: number;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
className?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseStyles: React.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 = null, 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;
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import cx from 'classnames';
|
|
||||||
|
|
||||||
export default (props) => {
|
|
||||||
const [sourceNode, setSourceNode] = useState(null);
|
|
||||||
const hasHandleId = props.connectionSourceId.includes('__');
|
|
||||||
const sourceIdSplitted = props.connectionSourceId.split('__');
|
|
||||||
const nodeId = sourceIdSplitted[0];
|
|
||||||
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSourceNode(props.nodes.find(n => n.id === nodeId));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!sourceNode) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style = props.connectionLineStyle || {};
|
|
||||||
const className = cx('react-flow__edge', 'connection', props.className);
|
|
||||||
|
|
||||||
const sourceHandle = handleId ? sourceNode.__rg.handleBounds.source.find(d => d.id === handleId) : sourceNode.__rg.handleBounds.source[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;
|
|
||||||
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
|
||||||
|
|
||||||
const targetX = (props.connectionPositionX - props.transform[0]) * (1 / props.transform[2]);
|
|
||||||
const targetY = (props.connectionPositionY - props.transform[1]) * (1 / props.transform[2]);
|
|
||||||
|
|
||||||
let dAttr = '';
|
|
||||||
|
|
||||||
if (props.connectionLineType === 'bezier') {
|
|
||||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
|
||||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
|
||||||
dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
|
|
||||||
} else {
|
|
||||||
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g className={className}>
|
|
||||||
<path
|
|
||||||
d={dAttr}
|
|
||||||
{...style}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React, { useEffect, useState, SVGAttributes } from 'react';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
import { ElementId, Node, Transform, HandleElement } from '../../types';
|
||||||
|
|
||||||
|
interface ConnectionLineProps {
|
||||||
|
connectionSourceId: ElementId;
|
||||||
|
connectionPositionX: number;
|
||||||
|
connectionPositionY: number;
|
||||||
|
connectionLineType?: string | null;
|
||||||
|
nodes: Node[];
|
||||||
|
transform: Transform;
|
||||||
|
connectionLineStyle?: SVGAttributes<{}>;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({
|
||||||
|
connectionSourceId, connectionLineStyle = {}, connectionPositionX, connectionPositionY,
|
||||||
|
connectionLineType, nodes = [], className, transform
|
||||||
|
}: ConnectionLineProps) => {
|
||||||
|
const [sourceNode, setSourceNode] = useState<Node | null>(null);
|
||||||
|
const hasHandleId = connectionSourceId.includes('__');
|
||||||
|
const sourceIdSplitted = connectionSourceId.split('__');
|
||||||
|
const nodeId = sourceIdSplitted[0];
|
||||||
|
const handleId = hasHandleId ? sourceIdSplitted[1] : null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextSourceNode = nodes.find(n => n.id === nodeId) || null;
|
||||||
|
setSourceNode(nextSourceNode);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!sourceNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeClasses: string = cx('react-flow__edge', 'connection', className);
|
||||||
|
|
||||||
|
const sourceHandle = handleId ?
|
||||||
|
sourceNode.__rg.handleBounds.source.find((d: HandleElement) => d.id === handleId) :
|
||||||
|
sourceNode.__rg.handleBounds.source[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;
|
||||||
|
const sourceY = sourceNode.__rg.position.y + sourceHandleY;
|
||||||
|
|
||||||
|
const targetX = (connectionPositionX - transform[0]) * (1 / transform[2]);
|
||||||
|
const targetY = (connectionPositionY - transform[1]) * (1 / transform[2]);
|
||||||
|
|
||||||
|
let dAttr: string = '';
|
||||||
|
|
||||||
|
if (connectionLineType === 'bezier') {
|
||||||
|
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||||
|
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||||
|
dAttr = `M${sourceX},${sourceY} C${sourceX},${centerY} ${targetX},${centerY} ${targetX},${targetY}`;
|
||||||
|
} else {
|
||||||
|
dAttr = `M${sourceX},${sourceY} ${targetX},${targetY}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g className={edgeClasses}>
|
||||||
|
<path
|
||||||
|
d={dAttr}
|
||||||
|
{...connectionLineStyle}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { EdgeBezierProps } from '../../types';
|
||||||
|
|
||||||
export default memo(({
|
export default memo(({
|
||||||
sourceX, sourceY, targetX, targetY,
|
sourceX, sourceY, targetX, targetY,
|
||||||
sourcePosition, targetPosition, style = {}
|
sourcePosition = 'bottom', targetPosition = 'top', style = {}
|
||||||
}) => {
|
}: EdgeBezierProps) => {
|
||||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
export default memo((props) => {
|
import { EdgeProps } from '../../types';
|
||||||
const {
|
|
||||||
sourceX, sourceY, targetX, targetY, style = {}
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
|
export default memo(({
|
||||||
|
sourceX, sourceY, targetX, targetY, style = {}
|
||||||
|
} : EdgeProps) => {
|
||||||
const yOffset = Math.abs(targetY - sourceY) / 2;
|
const yOffset = Math.abs(targetY - sourceY) / 2;
|
||||||
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
|
||||||
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
export default memo((props) => {
|
import { EdgeProps } from '../../types';
|
||||||
const {
|
|
||||||
sourceX, sourceY, targetX, targetY, style = {}
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
|
export default memo(({
|
||||||
|
sourceX, sourceY, targetX, targetY, style = {}
|
||||||
|
}: EdgeProps) => {
|
||||||
return (
|
return (
|
||||||
<path
|
<path
|
||||||
{...style}
|
{...style}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import cx from 'classnames';
|
|
||||||
|
|
||||||
import { inInputDOMNode } from '../../utils';
|
|
||||||
import store from '../../store';
|
|
||||||
|
|
||||||
export default EdgeComponent => {
|
|
||||||
const EdgeWrapper = memo((props) => {
|
|
||||||
const {
|
|
||||||
id, source, target, type,
|
|
||||||
animated, selected, onClick
|
|
||||||
} = props;
|
|
||||||
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
|
||||||
const onEdgeClick = (evt) => {
|
|
||||||
if (inInputDOMNode(evt)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.dispatch.setSelectedElements({ id, source, target });
|
|
||||||
onClick({ id, source, target, type });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g
|
|
||||||
className={edgeClasses}
|
|
||||||
onClick={onEdgeClick}
|
|
||||||
>
|
|
||||||
<EdgeComponent {...props} />
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
EdgeWrapper.displayName = 'EdgeWrapper';
|
|
||||||
EdgeWrapper.whyDidYouRender = false;
|
|
||||||
|
|
||||||
return EdgeWrapper;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React, { memo, MouseEvent, ComponentType } from 'react';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
import { isInputDOMNode } from '../../utils';
|
||||||
|
import store from '../../store';
|
||||||
|
import { EdgeWrapperProps } from '../../types';
|
||||||
|
|
||||||
|
export default (EdgeComponent: ComponentType<EdgeWrapperProps>) => {
|
||||||
|
const EdgeWrapper = memo(({
|
||||||
|
id, source, target, type,
|
||||||
|
animated, selected, onClick,
|
||||||
|
...rest
|
||||||
|
}: EdgeWrapperProps) => {
|
||||||
|
const edgeClasses = cx('react-flow__edge', { selected, animated });
|
||||||
|
const onEdgeClick = (evt: MouseEvent) => {
|
||||||
|
if (isInputDOMNode(evt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatch.setSelectedElements({ id, source, target });
|
||||||
|
onClick({ id, source, target, type });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
className={edgeClasses}
|
||||||
|
onClick={onEdgeClick}
|
||||||
|
>
|
||||||
|
<EdgeComponent
|
||||||
|
id={id}
|
||||||
|
source={source}
|
||||||
|
target={target}
|
||||||
|
type={type}
|
||||||
|
animated={animated}
|
||||||
|
selected={selected}
|
||||||
|
onClick={onClick}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EdgeWrapper.displayName = 'EdgeWrapper';
|
||||||
|
|
||||||
|
return EdgeWrapper;
|
||||||
|
};
|
||||||
@@ -1,35 +1,69 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
|
||||||
function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarget, isValidConnection }) {
|
import { HandleType, ElementId, Position, XYPosition, OnConnectFunc, Connection } from '../../types';
|
||||||
const containerBounds = document.querySelector('.react-flow').getBoundingClientRect();
|
|
||||||
let recentHoveredHandle = null;
|
type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||||
|
|
||||||
|
interface BaseHandleProps {
|
||||||
|
type: HandleType;
|
||||||
|
nodeId: ElementId;
|
||||||
|
onConnect: OnConnectFunc;
|
||||||
|
position: Position;
|
||||||
|
setSourceId: (nodeId: ElementId) => void;
|
||||||
|
setPosition: (pos: XYPosition) => void;
|
||||||
|
isValidConnection: ValidConnectionFunc;
|
||||||
|
id?: ElementId | boolean;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
elementBelow: Element;
|
||||||
|
isValid: boolean;
|
||||||
|
connection: Connection;
|
||||||
|
isHoveringHandle: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onMouseDown(
|
||||||
|
evt: ReactMouseEvent, nodeId: ElementId, setSourceId: (nodeId: ElementId) => void, setPosition: (pos: XYPosition) => any,
|
||||||
|
onConnect: OnConnectFunc, isTarget: boolean, isValidConnection: ValidConnectionFunc
|
||||||
|
): void {
|
||||||
|
const reactFlowNode = document.querySelector('.react-flow');
|
||||||
|
|
||||||
|
if (!reactFlowNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||||
|
let recentHoveredHandle: Element = null;
|
||||||
|
|
||||||
setPosition({
|
setPosition({
|
||||||
x: evt.clientX - containerBounds.x,
|
x: evt.clientX - containerBounds.left,
|
||||||
y: evt.clientY - containerBounds.y,
|
y: evt.clientY - containerBounds.top,
|
||||||
});
|
});
|
||||||
setSourceId(nodeId);
|
setSourceId(nodeId);
|
||||||
|
|
||||||
function resetRecentHandle() {
|
function resetRecentHandle() {
|
||||||
if (recentHoveredHandle) {
|
if (!recentHoveredHandle) {
|
||||||
recentHoveredHandle.classList.remove('valid');
|
return false;
|
||||||
recentHoveredHandle.classList.remove('connecting');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recentHoveredHandle.classList.remove('valid');
|
||||||
|
recentHoveredHandle.classList.remove('connecting');
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
// checks if element below mouse is a handle and returns connection in form of an object { source: 123, target: 312 }
|
||||||
function checkElementBelowIsValid(evt) {
|
function checkElementBelowIsValid(evt: MouseEvent) {
|
||||||
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
|
const elementBelow = document.elementFromPoint(evt.clientX, evt.clientY);
|
||||||
const result = {
|
const result: Result = {
|
||||||
elementBelow,
|
elementBelow,
|
||||||
isValid: false,
|
isValid: false,
|
||||||
connection: null,
|
connection: { source: null, target: null },
|
||||||
isHoveringHandle: false
|
isHoveringHandle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
|
if (elementBelow && (elementBelow.classList.contains('target') || elementBelow.classList.contains('source'))) {
|
||||||
let connection = null;
|
let connection: Connection = { source: null, target: null };
|
||||||
|
|
||||||
if (isTarget) {
|
if (isTarget) {
|
||||||
const sourceId = elementBelow.getAttribute('data-nodeid');
|
const sourceId = elementBelow.getAttribute('data-nodeid');
|
||||||
@@ -49,10 +83,10 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(evt) {
|
function onMouseMove(evt: MouseEvent) {
|
||||||
setPosition({
|
setPosition({
|
||||||
x: evt.clientX - containerBounds.x,
|
x: evt.clientX - containerBounds.left,
|
||||||
y: evt.clientY - containerBounds.y,
|
y: evt.clientY - containerBounds.top,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(evt);
|
const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid(evt);
|
||||||
@@ -70,7 +104,7 @@ function onMouseDown(evt, { nodeId, setSourceId, setPosition, onConnect, isTarg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseUp(evt) {
|
function onMouseUp(evt: MouseEvent) {
|
||||||
const { connection, isValid } = checkElementBelowIsValid(evt);
|
const { connection, isValid } = checkElementBelowIsValid(evt);
|
||||||
|
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
@@ -92,7 +126,7 @@ const BaseHandle = memo(({
|
|||||||
type, nodeId, onConnect, position,
|
type, nodeId, onConnect, position,
|
||||||
setSourceId, setPosition, className,
|
setSourceId, setPosition, className,
|
||||||
id = false, isValidConnection, ...rest
|
id = false, isValidConnection, ...rest
|
||||||
}) => {
|
}: BaseHandleProps) => {
|
||||||
const isTarget = type === 'target';
|
const isTarget = type === 'target';
|
||||||
const handleClasses = cx(
|
const handleClasses = cx(
|
||||||
'react-flow__handle',
|
'react-flow__handle',
|
||||||
@@ -108,16 +142,15 @@ const BaseHandle = memo(({
|
|||||||
data-nodeid={nodeIdWithHandleId}
|
data-nodeid={nodeIdWithHandleId}
|
||||||
data-handlepos={position}
|
data-handlepos={position}
|
||||||
className={handleClasses}
|
className={handleClasses}
|
||||||
onMouseDown={evt => onMouseDown(evt, {
|
onMouseDown={evt => onMouseDown(evt,
|
||||||
nodeId: nodeIdWithHandleId, setSourceId, setPosition,
|
nodeIdWithHandleId, setSourceId, setPosition,
|
||||||
onConnect, isTarget, isValidConnection
|
onConnect, isTarget, isValidConnection
|
||||||
})}
|
)}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
BaseHandle.displayName = 'BaseHandle';
|
BaseHandle.displayName = 'BaseHandle';
|
||||||
BaseHandle.whyDidYouRender = false;
|
|
||||||
|
|
||||||
export default BaseHandle;
|
export default BaseHandle;
|
||||||
@@ -1,18 +1,29 @@
|
|||||||
import React, { memo, useContext } from 'react';
|
import React, { memo, useContext } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useStoreActions, useStoreState } from 'easy-peasy';
|
|
||||||
|
|
||||||
|
import { useStoreActions, useStoreState } from '../../store/hooks';
|
||||||
import BaseHandle from './BaseHandle';
|
import BaseHandle from './BaseHandle';
|
||||||
import NodeIdContext from '../../contexts/NodeIdContext'
|
import NodeIdContext from '../../contexts/NodeIdContext'
|
||||||
|
|
||||||
const Handle = memo(({ onConnect, ...rest }) => {
|
import { HandleType, ElementId, Position, OnConnectParams, OnConnectFunc } from '../../types';
|
||||||
const nodeId = useContext(NodeIdContext);
|
|
||||||
|
interface HandleProps {
|
||||||
|
type: HandleType,
|
||||||
|
position: Position,
|
||||||
|
onConnect?: OnConnectFunc,
|
||||||
|
isValidConnection?: () => boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
const Handle = memo(({
|
||||||
|
onConnect = _ => {}, type = 'source', position = 'top', isValidConnection = () => true,
|
||||||
|
...rest
|
||||||
|
}: HandleProps) => {
|
||||||
|
const nodeId = useContext(NodeIdContext) as ElementId;
|
||||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||||
setPosition: a.setConnectionPosition,
|
setPosition: a.setConnectionPosition,
|
||||||
setSourceId: a.setConnectionSourceId
|
setSourceId: a.setConnectionSourceId
|
||||||
}));
|
}));
|
||||||
const onConnectAction = useStoreState(s => s.onConnect);
|
const onConnectAction = useStoreState(s => s.onConnect);
|
||||||
const onConnectExtended = (params) => {
|
const onConnectExtended = (params: OnConnectParams) => {
|
||||||
onConnectAction(params);
|
onConnectAction(params);
|
||||||
onConnect(params);
|
onConnect(params);
|
||||||
};
|
};
|
||||||
@@ -23,6 +34,9 @@ const Handle = memo(({ onConnect, ...rest }) => {
|
|||||||
setPosition={setPosition}
|
setPosition={setPosition}
|
||||||
setSourceId={setSourceId}
|
setSourceId={setSourceId}
|
||||||
onConnect={onConnectExtended}
|
onConnect={onConnectExtended}
|
||||||
|
type={type}
|
||||||
|
position={position}
|
||||||
|
isValidConnection={isValidConnection}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -30,18 +44,4 @@ const Handle = memo(({ onConnect, ...rest }) => {
|
|||||||
|
|
||||||
Handle.displayName = 'Handle';
|
Handle.displayName = 'Handle';
|
||||||
|
|
||||||
Handle.propTypes = {
|
|
||||||
type: PropTypes.oneOf(['source', 'target']),
|
|
||||||
position: PropTypes.oneOf(['top', 'right', 'bottom', 'left']),
|
|
||||||
onConnect: PropTypes.func,
|
|
||||||
isValidConnection: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
Handle.defaultProps = {
|
|
||||||
type: 'source',
|
|
||||||
position: 'top',
|
|
||||||
onConnect: () => {},
|
|
||||||
isValidConnection: () => true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Handle;
|
export default Handle;
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
|
|
||||||
import Handle from '../../components/Handle';
|
import Handle from '../../components/Handle';
|
||||||
|
import { NodeProps } from '../../types';
|
||||||
|
|
||||||
const nodeStyles = {
|
const nodeStyles: CSSProperties = {
|
||||||
background: '#ff6060',
|
background: '#ff6060',
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
width: 150
|
width: 150
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ data, style }) => (
|
export default ({ data, style }: NodeProps) => (
|
||||||
<div style={{ ...nodeStyles, ...style }}>
|
<div style={{ ...nodeStyles, ...style }}>
|
||||||
<Handle type="target" position="top" />
|
<Handle type="target" position="top" />
|
||||||
{data.label}
|
{data.label}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
|
|
||||||
import Handle from '../../components/Handle';
|
import Handle from '../../components/Handle';
|
||||||
|
import { NodeProps } from '../../types';
|
||||||
|
|
||||||
const nodeStyles = {
|
const nodeStyles: CSSProperties = {
|
||||||
background: '#9999ff',
|
background: '#9999ff',
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
width: 150
|
width: 150
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ data, style }) => (
|
export default ({ data, style }: NodeProps) => (
|
||||||
<div style={{ ...nodeStyles, ...style }}>
|
<div style={{ ...nodeStyles, ...style }}>
|
||||||
{data.label}
|
{data.label}
|
||||||
<Handle type="source" position="bottom" />
|
<Handle type="source" position="bottom" />
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
|
|
||||||
import Handle from '../../components/Handle';
|
import Handle from '../../components/Handle';
|
||||||
|
import { NodeProps } from '../../types';
|
||||||
|
|
||||||
const nodeStyles = {
|
const nodeStyles: CSSProperties = {
|
||||||
background: '#55dd99',
|
background: '#55dd99',
|
||||||
padding: 10,
|
padding: 10,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
width: 150
|
width: 150
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ data, style }) => (
|
export default ({ data, style }: NodeProps) => (
|
||||||
<div style={{ ...nodeStyles, ...style }}>
|
<div style={{ ...nodeStyles, ...style }}>
|
||||||
<Handle type="target" position="top" />
|
<Handle type="target" position="top" />
|
||||||
{data.label}
|
{data.label}
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState, memo } from 'react';
|
|
||||||
import ReactDraggable from 'react-draggable';
|
|
||||||
import cx from 'classnames';
|
|
||||||
|
|
||||||
import { getDimensions, inInputDOMNode } from '../../utils';
|
|
||||||
import { Provider } from '../../contexts/NodeIdContext';
|
|
||||||
import store from '../../store';
|
|
||||||
|
|
||||||
const isHandle = e => (
|
|
||||||
e.target.className &&
|
|
||||||
e.target.className.includes &&
|
|
||||||
(e.target.className.includes('source') || e.target.className.includes('target'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasResizeObserver = !!window.ResizeObserver;
|
|
||||||
|
|
||||||
const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
|
||||||
const handles = nodeElement.querySelectorAll(sel);
|
|
||||||
|
|
||||||
if (!handles || !handles.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [].map.call(handles, (handle) => {
|
|
||||||
const bounds = handle.getBoundingClientRect();
|
|
||||||
const dimensions = getDimensions(handle);
|
|
||||||
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
|
||||||
const handlePosition = handle.getAttribute('data-handlepos');
|
|
||||||
const nodeIdSplitted = nodeIdAttr.split('__');
|
|
||||||
|
|
||||||
let handleId = null;
|
|
||||||
|
|
||||||
if (nodeIdSplitted) {
|
|
||||||
handleId = nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: handleId,
|
|
||||||
position: handlePosition,
|
|
||||||
x: (bounds.x - parentBounds.x) * (1 / k),
|
|
||||||
y: (bounds.y - parentBounds.y) * (1 / k),
|
|
||||||
...dimensions
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStart = (evt, { setOffset, onClick, id, type, data, position, transform }) => {
|
|
||||||
if (inInputDOMNode(evt) || isHandle(evt)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scaledClient = {
|
|
||||||
x: evt.clientX * (1 / [transform[2]]),
|
|
||||||
y: evt.clientY * (1 / [transform[2]])
|
|
||||||
};
|
|
||||||
const offsetX = scaledClient.x - position.x - transform[0];
|
|
||||||
const offsetY = scaledClient.y - position.y - transform[1];
|
|
||||||
const node = { id, type, position, data };
|
|
||||||
|
|
||||||
store.dispatch.setSelectedElements({ id, type });
|
|
||||||
setOffset({ x: offsetX, y: offsetY });
|
|
||||||
onClick(node);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDrag = (evt, { setDragging, id, offset, transform }) => {
|
|
||||||
const scaledClient = {
|
|
||||||
x: evt.clientX * (1 / transform[2]),
|
|
||||||
y: evt.clientY * (1 / transform[2])
|
|
||||||
};
|
|
||||||
|
|
||||||
setDragging(true);
|
|
||||||
store.dispatch.updateNodePos({ id, pos: {
|
|
||||||
x: scaledClient.x - transform[0] - offset.x,
|
|
||||||
y: scaledClient.y - transform[1] - offset.y
|
|
||||||
}});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStop = ({ onNodeDragStop, setDragging, isDragging, id, type, position, data }) => {
|
|
||||||
if (!isDragging) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
setDragging(false);
|
|
||||||
onNodeDragStop({
|
|
||||||
id, type, position, data
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NodeComponent => {
|
|
||||||
const NodeWrapper = memo((props) => {
|
|
||||||
const nodeElement = useRef(null);
|
|
||||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
|
||||||
const [isDragging, setDragging] = useState(false);
|
|
||||||
const {
|
|
||||||
id, type, data, transform, xPos, yPos, selected,
|
|
||||||
onClick, onNodeDragStop, style
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const position = { x: xPos, y: yPos };
|
|
||||||
const nodeClasses = cx('react-flow__node', { selected });
|
|
||||||
const nodeStyle = { zIndex: selected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)` };
|
|
||||||
|
|
||||||
const updateNode = () => {
|
|
||||||
const storeState = store.getState()
|
|
||||||
const bounds = nodeElement.current.getBoundingClientRect();
|
|
||||||
const dimensions = getDimensions(nodeElement.current);
|
|
||||||
const handleBounds = {
|
|
||||||
source: getHandleBounds('.source', nodeElement.current, bounds, storeState.transform[2]),
|
|
||||||
target: getHandleBounds('.target', nodeElement.current, bounds, storeState.transform[2])
|
|
||||||
};
|
|
||||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateNode();
|
|
||||||
|
|
||||||
let resizeObserver = null;
|
|
||||||
|
|
||||||
if (hasResizeObserver) {
|
|
||||||
resizeObserver = new ResizeObserver(entries => {
|
|
||||||
for (let entry of entries) {
|
|
||||||
updateNode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserver.observe(nodeElement.current);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (hasResizeObserver && resizeObserver) {
|
|
||||||
resizeObserver.unobserve(nodeElement.current);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ReactDraggable.DraggableCore
|
|
||||||
onStart={evt => onStart(evt, { onClick, id, type, data, setOffset, transform, position })}
|
|
||||||
onDrag={evt => onDrag(evt, { setDragging, id, offset, transform })}
|
|
||||||
onStop={() => onStop({ onNodeDragStop, isDragging, setDragging, id, type, position, data })}
|
|
||||||
scale={transform[2]}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={nodeClasses}
|
|
||||||
ref={nodeElement}
|
|
||||||
style={nodeStyle}
|
|
||||||
>
|
|
||||||
<Provider value={id}>
|
|
||||||
<NodeComponent
|
|
||||||
id={id}
|
|
||||||
data={data}
|
|
||||||
type={type}
|
|
||||||
style={style}
|
|
||||||
selected={selected}
|
|
||||||
/>
|
|
||||||
</Provider>
|
|
||||||
</div>
|
|
||||||
</ReactDraggable.DraggableCore>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NodeWrapper.displayName = 'NodeWrapper';
|
|
||||||
NodeWrapper.whyDidYouRender = false;
|
|
||||||
|
|
||||||
return NodeWrapper;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import React, { useEffect, useRef, useState, memo, ComponentType } from 'react';
|
||||||
|
import { DraggableCore, DraggableEvent } from 'react-draggable';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { ResizeObserver } from 'resize-observer';
|
||||||
|
|
||||||
|
import { getDimensions, isInputDOMNode } from '../../utils';
|
||||||
|
import { Provider } from '../../contexts/NodeIdContext';
|
||||||
|
import store from '../../store';
|
||||||
|
import { NodeComponentProps, Node, XYPosition, HandleElement, Position, Transform, ElementId } from '../../types';
|
||||||
|
|
||||||
|
const isHandle = (evt: MouseEvent | DraggableEvent) => {
|
||||||
|
const target = evt.target as HTMLElement;
|
||||||
|
|
||||||
|
return (
|
||||||
|
target.className &&
|
||||||
|
target.className.includes &&
|
||||||
|
(target.className.includes('source') || target.className.includes('target'))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHandleBounds = (
|
||||||
|
selector: string, nodeElement: HTMLDivElement, parentBounds: ClientRect | DOMRect, k: number
|
||||||
|
): HandleElement => {
|
||||||
|
const handles = nodeElement.querySelectorAll(selector);
|
||||||
|
|
||||||
|
if (!handles || !handles.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [].map.call(handles, (handle: HTMLDivElement): HandleElement => {
|
||||||
|
const bounds = handle.getBoundingClientRect();
|
||||||
|
const dimensions = getDimensions(handle);
|
||||||
|
const nodeIdAttr = handle.getAttribute('data-nodeid');
|
||||||
|
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position;
|
||||||
|
const nodeIdSplitted = nodeIdAttr.split('__');
|
||||||
|
|
||||||
|
let handleId = null;
|
||||||
|
|
||||||
|
if (nodeIdSplitted) {
|
||||||
|
handleId = (nodeIdSplitted.length ? nodeIdSplitted[1] : nodeIdSplitted) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: handleId,
|
||||||
|
position: handlePosition,
|
||||||
|
x: (bounds.left - parentBounds.left) * (1 / k),
|
||||||
|
y: (bounds.top - parentBounds.top) * (1 / k),
|
||||||
|
...dimensions
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStart = (
|
||||||
|
evt: MouseEvent, onClick: (node: Node) => void, id: ElementId, type: string,
|
||||||
|
data: any, setOffset: (pos: XYPosition) => void, transform: Transform, position: XYPosition
|
||||||
|
): false | void => {
|
||||||
|
if (isInputDOMNode(evt) || isHandle(evt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaledClient: XYPosition = {
|
||||||
|
x: evt.clientX * (1 / transform[2]),
|
||||||
|
y: evt.clientY * (1 / transform[2])
|
||||||
|
};
|
||||||
|
const offsetX = scaledClient.x - position.x - transform[0];
|
||||||
|
const offsetY = scaledClient.y - position.y - transform[1];
|
||||||
|
const node = { id, type, position, data };
|
||||||
|
|
||||||
|
store.dispatch.setSelectedElements({ id, type });
|
||||||
|
setOffset({ x: offsetX, y: offsetY });
|
||||||
|
onClick(node);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrag = (
|
||||||
|
evt: MouseEvent, setDragging: (isDragging: boolean) => void, id: ElementId, offset: XYPosition,
|
||||||
|
transform: Transform
|
||||||
|
): void => {
|
||||||
|
const scaledClient = {
|
||||||
|
x: evt.clientX * (1 / transform[2]),
|
||||||
|
y: evt.clientY * (1 / transform[2])
|
||||||
|
};
|
||||||
|
|
||||||
|
setDragging(true);
|
||||||
|
store.dispatch.updateNodePos({ id, pos: {
|
||||||
|
x: scaledClient.x - transform[0] - offset.x,
|
||||||
|
y: scaledClient.y - transform[1] - offset.y
|
||||||
|
}});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStop = (
|
||||||
|
onNodeDragStop: (params: Node) => void, isDragging: boolean, setDragging: (isDragging: boolean) => void, id: ElementId,
|
||||||
|
type: string, position: XYPosition, data: any
|
||||||
|
): void => {
|
||||||
|
if (isDragging) {
|
||||||
|
setDragging(false);
|
||||||
|
onNodeDragStop({
|
||||||
|
id, type, position, data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
|
||||||
|
const NodeWrapper = memo(({
|
||||||
|
id, type, data, transform,
|
||||||
|
xPos, yPos, selected, onClick,
|
||||||
|
onNodeDragStop, style
|
||||||
|
}: NodeComponentProps) => {
|
||||||
|
const nodeElement = useRef<HTMLDivElement>(null);
|
||||||
|
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||||
|
const [isDragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
const position = { x: xPos, y: yPos };
|
||||||
|
const nodeClasses = cx('react-flow__node', { selected });
|
||||||
|
const nodeStyle = { zIndex: selected ? 10 : 3, transform: `translate(${xPos}px,${yPos}px)` };
|
||||||
|
|
||||||
|
const updateNode = () => {
|
||||||
|
if (!nodeElement.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeState = store.getState()
|
||||||
|
const bounds = nodeElement.current.getBoundingClientRect();
|
||||||
|
const dimensions = getDimensions(nodeElement.current);
|
||||||
|
const handleBounds = {
|
||||||
|
source: getHandleBounds('.source', nodeElement.current, bounds, storeState.transform[2]),
|
||||||
|
target: getHandleBounds('.target', nodeElement.current, bounds, storeState.transform[2])
|
||||||
|
};
|
||||||
|
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nodeElement.current) {
|
||||||
|
updateNode();
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(entries => {
|
||||||
|
for (let _ of entries) {
|
||||||
|
updateNode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserver.observe(nodeElement.current);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (resizeObserver && nodeElement.current) {
|
||||||
|
resizeObserver.unobserve(nodeElement.current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [nodeElement.current]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableCore
|
||||||
|
onStart={evt => onStart(evt as MouseEvent, onClick, id, type, data, setOffset, transform, position)}
|
||||||
|
onDrag={evt => onDrag(evt as MouseEvent, setDragging, id, offset, transform)}
|
||||||
|
onStop={() => onStop(onNodeDragStop, isDragging, setDragging, id, type, position, data)}
|
||||||
|
scale={transform[2]}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={nodeClasses}
|
||||||
|
ref={nodeElement}
|
||||||
|
style={nodeStyle}
|
||||||
|
>
|
||||||
|
<Provider value={id}>
|
||||||
|
<NodeComponent
|
||||||
|
id={id}
|
||||||
|
data={data}
|
||||||
|
type={type}
|
||||||
|
style={style}
|
||||||
|
selected={selected}
|
||||||
|
/>
|
||||||
|
</Provider>
|
||||||
|
</div>
|
||||||
|
</DraggableCore>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NodeWrapper.displayName = 'NodeWrapper';
|
||||||
|
|
||||||
|
return NodeWrapper;
|
||||||
|
};
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useState, memo } from 'react';
|
import React, { useState, memo } from 'react';
|
||||||
import ReactDraggable from 'react-draggable';
|
import ReactDraggable from 'react-draggable';
|
||||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
|
||||||
|
|
||||||
|
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||||
import { isNode } from '../../utils/graph';
|
import { isNode } from '../../utils/graph';
|
||||||
|
import { Node, Elements, XYPosition } from '../../types';
|
||||||
|
|
||||||
function getStartPositions(elements) {
|
function getStartPositions(elements: Elements) {
|
||||||
return elements
|
return elements
|
||||||
.filter(isNode)
|
.filter(isNode)
|
||||||
.reduce((res, node) => {
|
.reduce((res, node: Node) => {
|
||||||
const startPosition = {
|
const startPosition = {
|
||||||
x: node.__rg.position.x || node.position.x,
|
x: node.__rg.position.x || node.position.x,
|
||||||
y: node.__rg.position.y || node.position.x
|
y: node.__rg.position.y || node.position.x
|
||||||
@@ -31,31 +32,33 @@ export default memo(() => {
|
|||||||
const [x, y, k] = state.transform;
|
const [x, y, k] = state.transform;
|
||||||
const position = state.selectedNodesBbox;
|
const position = state.selectedNodesBbox;
|
||||||
|
|
||||||
const onStart = (evt) => {
|
const onStart = (evt: MouseEvent) => {
|
||||||
const scaledClient = {
|
const scaledClient: XYPosition = {
|
||||||
x: evt.clientX * (1 / k),
|
x: evt.clientX * (1 / k),
|
||||||
y: evt.clientY * (1 / k)
|
y: evt.clientY * (1 / k)
|
||||||
};
|
};
|
||||||
const offsetX = scaledClient.x - position.x - x;
|
const offsetX: number = scaledClient.x - position.x - x;
|
||||||
const offsetY = scaledClient.y - position.y - y;
|
const offsetY: number = scaledClient.y - position.y - y;
|
||||||
const startPositions = getStartPositions(state.selectedElements);
|
const startPositions = getStartPositions(state.selectedElements);
|
||||||
|
|
||||||
setOffset({ x: offsetX, y: offsetY });
|
setOffset({ x: offsetX, y: offsetY });
|
||||||
setStartPositions(startPositions);
|
setStartPositions(startPositions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDrag = (evt) => {
|
const onDrag = (evt: MouseEvent) => {
|
||||||
const scaledClient = {
|
const scaledClient: XYPosition = {
|
||||||
x: evt.clientX * (1 / k),
|
x: evt.clientX * (1 / k),
|
||||||
y: evt.clientY * (1 / k)
|
y: evt.clientY * (1 / k)
|
||||||
};
|
};
|
||||||
|
|
||||||
state.selectedElements.filter(isNode).forEach(node => {
|
state.selectedElements
|
||||||
updateNodePos({ id: node.id, pos: {
|
.filter(isNode)
|
||||||
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - x ,
|
.forEach((node: Node) => {
|
||||||
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - y
|
updateNodePos({ id: node.id, pos: {
|
||||||
}});
|
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - x ,
|
||||||
});
|
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - y
|
||||||
|
}});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,8 +70,8 @@ export default memo(() => {
|
|||||||
>
|
>
|
||||||
<ReactDraggable
|
<ReactDraggable
|
||||||
scale={k}
|
scale={k}
|
||||||
onStart={onStart}
|
onStart={(evt: MouseEvent) => onStart(evt)}
|
||||||
onDrag={onDrag}
|
onDrag={(evt: MouseEvent) => onDrag(evt)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="react-flow__nodesselection-rect"
|
className="react-flow__nodesselection-rect"
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import React, { useEffect, useRef, useState, memo } from 'react';
|
import React, { useEffect, useRef, useState, memo, MouseEvent } from 'react';
|
||||||
import { useStoreActions } from 'easy-peasy';
|
|
||||||
|
|
||||||
const initialRect = {
|
import { useStoreActions } from '../../store/hooks';
|
||||||
|
import { SelectionRect } from '../../types';
|
||||||
|
|
||||||
|
const initialRect: SelectionRect = {
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
x: 0,
|
x: 0,
|
||||||
@@ -11,8 +13,13 @@ const initialRect = {
|
|||||||
draw: false
|
draw: false
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMousePosition(evt) {
|
function getMousePosition(evt: MouseEvent) {
|
||||||
const containerBounds = document.querySelector('.react-flow').getBoundingClientRect();
|
const reactFlowNode = document.querySelector('.react-flow');
|
||||||
|
if (!reactFlowNode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerBounds = reactFlowNode.getBoundingClientRect();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: evt.clientX - containerBounds.left,
|
x: evt.clientX - containerBounds.left,
|
||||||
@@ -28,8 +35,11 @@ export default memo(() => {
|
|||||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function onMouseDown(evt) {
|
function onMouseDown(evt: MouseEvent) {
|
||||||
const mousePos = getMousePosition(evt);
|
const mousePos = getMousePosition(evt);
|
||||||
|
if (!mousePos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
setRect((currentRect) => ({
|
setRect((currentRect) => ({
|
||||||
...currentRect,
|
...currentRect,
|
||||||
@@ -43,13 +53,17 @@ export default memo(() => {
|
|||||||
setSelection(true);
|
setSelection(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onMouseMove(evt) {
|
function onMouseMove(evt: MouseEvent) {
|
||||||
setRect((currentRect) => {
|
setRect((currentRect) => {
|
||||||
if (!currentRect.draw) {
|
if (!currentRect.draw) {
|
||||||
return currentRect;
|
return currentRect;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mousePos = getMousePosition(evt);
|
const mousePos = getMousePosition(evt);
|
||||||
|
if (!mousePos) {
|
||||||
|
return currentRect;
|
||||||
|
}
|
||||||
|
|
||||||
const negativeX = mousePos.x < currentRect.startX;
|
const negativeX = mousePos.x < currentRect.startX;
|
||||||
const negativeY = mousePos.y < currentRect.startY;
|
const negativeY = mousePos.y < currentRect.startY;
|
||||||
const nextRect = {
|
const nextRect = {
|
||||||
@@ -94,7 +108,7 @@ export default memo(() => {
|
|||||||
className="react-flow__selectionpane"
|
className="react-flow__selectionpane"
|
||||||
ref={selectionPane}
|
ref={selectionPane}
|
||||||
>
|
>
|
||||||
{(rect.draw || rect.fixed) && (
|
{rect.draw && (
|
||||||
<div
|
<div
|
||||||
className="react-flow__selection"
|
className="react-flow__selection"
|
||||||
style={{
|
style={{
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import React, {memo} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {useStoreState} from 'easy-peasy';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
|
|
||||||
const baseStyles = {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const createGridLines = (width, height, xOffset, yOffset, gap) => {
|
|
||||||
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, height, xOffset, yOffset, gap, size) => {
|
|
||||||
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, color, size, style, className, backgroundType}) => {
|
|
||||||
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';
|
|
||||||
|
|
||||||
Grid.propTypes = {
|
|
||||||
gap: PropTypes.number,
|
|
||||||
color: PropTypes.string,
|
|
||||||
size: PropTypes.number,
|
|
||||||
style: PropTypes.object,
|
|
||||||
className: PropTypes.string,
|
|
||||||
backgroundType: PropTypes.oneOf(['lines', 'dots']),
|
|
||||||
};
|
|
||||||
|
|
||||||
Grid.defaultProps = {
|
|
||||||
gap: 24,
|
|
||||||
color: '#aaa',
|
|
||||||
size: .5,
|
|
||||||
style: {},
|
|
||||||
className: null,
|
|
||||||
backgroundType: 'dots',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Grid;
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import Grid from './Grid';
|
|
||||||
|
|
||||||
const bgComponents = {
|
|
||||||
lines: Grid,
|
|
||||||
dots: Grid
|
|
||||||
};
|
|
||||||
|
|
||||||
const BackgroundRenderer = memo(({
|
|
||||||
backgroundType, ...rest
|
|
||||||
}) => {
|
|
||||||
const BackgroundComponent = bgComponents[backgroundType];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackgroundComponent
|
|
||||||
backgroundType={backgroundType}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
BackgroundRenderer.displayName = 'BackgroundRenderer';
|
|
||||||
|
|
||||||
BackgroundRenderer.propTypes = {
|
|
||||||
backgroundType: PropTypes.oneOf(['lines', 'dots'])
|
|
||||||
};
|
|
||||||
|
|
||||||
BackgroundRenderer.defaultProps = {
|
|
||||||
backgroundType: 'dots'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BackgroundRenderer;
|
|
||||||
@@ -1,10 +1,36 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo, SVGAttributes } from 'react';
|
||||||
import { useStoreState } from 'easy-peasy';
|
|
||||||
|
|
||||||
import ConnectionLine from '../../components/ConnectionLine';
|
import { useStoreState } from '../../store/hooks';
|
||||||
|
import ConnectionLine from '../../components/ConnectionLine/index';
|
||||||
import { isEdge } from '../../utils/graph';
|
import { isEdge } from '../../utils/graph';
|
||||||
|
import { XYPosition, Position, Edge, Node, ElementId, Transform, HandleElement } from '../../types';
|
||||||
|
|
||||||
function getHandlePosition(position, node, handle = null) {
|
interface EdgeRendererProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
edgeTypes: any;
|
||||||
|
connectionLineStyle?: SVGAttributes<{}>;
|
||||||
|
connectionLineType?: string;
|
||||||
|
onElementClick?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EdgeRendererState {
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
transform: Transform;
|
||||||
|
selectedElements: any;
|
||||||
|
connectionSourceId: ElementId | null;
|
||||||
|
position: XYPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EdgePositions {
|
||||||
|
sourceX: number;
|
||||||
|
sourceY: number;
|
||||||
|
targetX: number;
|
||||||
|
targetY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
|
||||||
if (!handle) {
|
if (!handle) {
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case 'top': return {
|
case 'top': return {
|
||||||
@@ -24,6 +50,8 @@ function getHandlePosition(position, node, handle = null) {
|
|||||||
y: node.__rg.height / 2
|
y: node.__rg.height / 2
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
@@ -46,7 +74,7 @@ function getHandlePosition(position, node, handle = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHandle(bounds, handleId) {
|
function getHandle(bounds: HandleElement[], handleId: ElementId): HandleElement | null {
|
||||||
let handle = null;
|
let handle = null;
|
||||||
|
|
||||||
if (!bounds) {
|
if (!bounds) {
|
||||||
@@ -64,7 +92,10 @@ function getHandle(bounds, handleId) {
|
|||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEdgePositions({ sourceNode, sourceHandle, sourcePosition, targetNode, targetHandle, targetPosition }) {
|
function getEdgePositions(
|
||||||
|
sourceNode: Node, sourceHandle: HandleElement, sourcePosition: Position,
|
||||||
|
targetNode: Node, targetHandle: HandleElement, targetPosition: Position
|
||||||
|
): EdgePositions {
|
||||||
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle)
|
||||||
const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x;
|
const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x;
|
||||||
const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y;
|
const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y;
|
||||||
@@ -78,17 +109,17 @@ function getEdgePositions({ sourceNode, sourceHandle, sourcePosition, targetNode
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEdge(e, props, state) {
|
function renderEdge(edge: Edge, props: EdgeRendererProps, state: EdgeRendererState) {
|
||||||
const edgeType = e.type || 'default';
|
const edgeType = edge.type || 'default';
|
||||||
|
|
||||||
const hasSourceHandleId = e.source.includes('__');
|
const hasSourceHandleId = edge.source.includes('__');
|
||||||
const hasTargetHandleId = e.target.includes('__');
|
const hasTargetHandleId = edge.target.includes('__');
|
||||||
|
|
||||||
const sourceId = hasSourceHandleId ? e.source.split('__')[0] : e.source;
|
const sourceId = hasSourceHandleId ? edge.source.split('__')[0] : edge.source;
|
||||||
const targetId = hasTargetHandleId ? e.target.split('__')[0] : e.target;
|
const targetId = hasTargetHandleId ? edge.target.split('__')[0] : edge.target;
|
||||||
|
|
||||||
const sourceHandleId = hasSourceHandleId ? e.source.split('__')[1] : null;
|
const sourceHandleId = hasSourceHandleId ? edge.source.split('__')[1] : null;
|
||||||
const targetHandleId = hasTargetHandleId ? e.target.split('__')[1] : null;
|
const targetHandleId = hasTargetHandleId ? edge.target.split('__')[1] : null;
|
||||||
|
|
||||||
const sourceNode = state.nodes.find(n => n.id === sourceId);
|
const sourceNode = state.nodes.find(n => n.id === sourceId);
|
||||||
const targetNode = state.nodes.find(n => n.id === targetId);
|
const targetNode = state.nodes.find(n => n.id === targetId);
|
||||||
@@ -107,23 +138,23 @@ function renderEdge(e, props, state) {
|
|||||||
const sourcePosition = sourceHandle ? sourceHandle.position : 'bottom';
|
const sourcePosition = sourceHandle ? sourceHandle.position : 'bottom';
|
||||||
const targetPosition = targetHandle ? targetHandle.position : 'top';
|
const targetPosition = targetHandle ? targetHandle.position : 'top';
|
||||||
|
|
||||||
const { sourceX, sourceY, targetX, targetY } = getEdgePositions({
|
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
|
||||||
sourceNode, sourceHandle, sourcePosition,
|
sourceNode, sourceHandle, sourcePosition,
|
||||||
targetNode, targetHandle, targetPosition
|
targetNode, targetHandle, targetPosition
|
||||||
});
|
);
|
||||||
const selected = state.selectedElements
|
const selected = state.selectedElements
|
||||||
.filter(isEdge)
|
.filter(isEdge)
|
||||||
.find(elm => elm.source === sourceId && elm.target === targetId);
|
.find((elm: Edge) => elm.source === sourceId && elm.target === targetId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EdgeComponent
|
<EdgeComponent
|
||||||
key={e.id}
|
key={edge.id}
|
||||||
id={e.id}
|
id={edge.id}
|
||||||
type={e.type}
|
type={edge.type}
|
||||||
onClick={props.onElementClick}
|
onClick={props.onElementClick}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
animated={e.animated}
|
animated={edge.animated}
|
||||||
style={e.style}
|
style={edge.style}
|
||||||
source={sourceId}
|
source={sourceId}
|
||||||
target={targetId}
|
target={targetId}
|
||||||
sourceHandleId={sourceHandleId}
|
sourceHandleId={sourceHandleId}
|
||||||
@@ -138,8 +169,10 @@ function renderEdge(e, props, state) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const EdgeRenderer = memo((props) => {
|
const EdgeRenderer = memo(({
|
||||||
const state = useStoreState(s => ({
|
width, height, connectionLineStyle, connectionLineType, ...rest
|
||||||
|
}: EdgeRendererProps) => {
|
||||||
|
const state: EdgeRendererState = useStoreState(s => ({
|
||||||
nodes: s.nodes,
|
nodes: s.nodes,
|
||||||
edges: s.edges,
|
edges: s.edges,
|
||||||
transform: s.transform,
|
transform: s.transform,
|
||||||
@@ -147,10 +180,6 @@ const EdgeRenderer = memo((props) => {
|
|||||||
connectionSourceId: s.connectionSourceId,
|
connectionSourceId: s.connectionSourceId,
|
||||||
position: s.connectionPosition
|
position: s.connectionPosition
|
||||||
}));
|
}));
|
||||||
const {
|
|
||||||
width, height, connectionLineStyle, connectionLineType
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!width) {
|
if (!width) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -165,7 +194,7 @@ const EdgeRenderer = memo((props) => {
|
|||||||
className="react-flow__edges"
|
className="react-flow__edges"
|
||||||
>
|
>
|
||||||
<g transform={transformStyle}>
|
<g transform={transformStyle}>
|
||||||
{edges.map(e => renderEdge(e, props, state))}
|
{edges.map((e: Edge) => renderEdge(e, { width, height, connectionLineStyle, connectionLineType, ...rest }, state))}
|
||||||
{connectionSourceId && (
|
{connectionSourceId && (
|
||||||
<ConnectionLine
|
<ConnectionLine
|
||||||
nodes={nodes}
|
nodes={nodes}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import StraightEdge from '../../components/Edges/StraightEdge';
|
|
||||||
import BezierEdge from '../../components/Edges/BezierEdge';
|
|
||||||
import wrapEdge from '../../components/Edges/wrapEdge';
|
|
||||||
|
|
||||||
export function createEdgeTypes(edgeTypes) {
|
|
||||||
const standardTypes = {
|
|
||||||
default: wrapEdge(edgeTypes.default || BezierEdge),
|
|
||||||
straight: wrapEdge(edgeTypes.bezier || StraightEdge)
|
|
||||||
};
|
|
||||||
|
|
||||||
const specialTypes = Object
|
|
||||||
.keys(edgeTypes)
|
|
||||||
.filter(k => !['default', 'bezier'].includes(k))
|
|
||||||
.reduce((res, key) => {
|
|
||||||
res[key] = wrapEdge(edgeTypes[key] ||BezierEdge);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...standardTypes,
|
|
||||||
...specialTypes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
import StraightEdge from '../../components/Edges/StraightEdge';
|
||||||
|
import BezierEdge from '../../components/Edges/BezierEdge';
|
||||||
|
import wrapEdge from '../../components/Edges/wrapEdge';
|
||||||
|
|
||||||
|
import { EdgeTypesType, EdgeWrapperProps } from '../../types';
|
||||||
|
|
||||||
|
export function createEdgeTypes(edgeTypes: EdgeTypesType): EdgeTypesType{
|
||||||
|
const standardTypes: EdgeTypesType = {
|
||||||
|
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeWrapperProps>),
|
||||||
|
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeWrapperProps>)
|
||||||
|
};
|
||||||
|
|
||||||
|
const specialTypes: EdgeTypesType = Object
|
||||||
|
.keys(edgeTypes)
|
||||||
|
.filter(k => !['default', 'bezier'].includes(k))
|
||||||
|
.reduce((res, key) => {
|
||||||
|
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeWrapperProps>);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...standardTypes,
|
||||||
|
...specialTypes
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,17 +1,38 @@
|
|||||||
import React, { useEffect, useRef, memo } from 'react';
|
import React, { useEffect, useRef, memo, SVGAttributes } from 'react';
|
||||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
|
||||||
|
|
||||||
|
import { useStoreState, useStoreActions } from '../../store/hooks';
|
||||||
import NodeRenderer from '../NodeRenderer';
|
import NodeRenderer from '../NodeRenderer';
|
||||||
import EdgeRenderer from '../EdgeRenderer';
|
import EdgeRenderer from '../EdgeRenderer';
|
||||||
import BackgroundRenderer from '../BackgroundRenderer';
|
|
||||||
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 } from '../../utils/graph';
|
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
||||||
|
import { Elements, NodeTypesType, EdgeTypesType, GridType, OnLoadFunc } from '../../types'
|
||||||
|
|
||||||
|
export interface GraphViewProps {
|
||||||
|
elements: Elements,
|
||||||
|
onElementClick: () => void,
|
||||||
|
onElementsRemove: (elements: Elements) => void,
|
||||||
|
onNodeDragStop: () => void,
|
||||||
|
onConnect: () => void,
|
||||||
|
onLoad: OnLoadFunc,
|
||||||
|
onMove: () => void,
|
||||||
|
selectionKeyCode: number,
|
||||||
|
nodeTypes: NodeTypesType,
|
||||||
|
edgeTypes: EdgeTypesType,
|
||||||
|
connectionLineType: string,
|
||||||
|
connectionLineStyle: SVGAttributes<{}>,
|
||||||
|
deleteKeyCode: number,
|
||||||
|
showBackground: boolean,
|
||||||
|
backgroundGap: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
backgroundType: GridType,
|
||||||
|
};
|
||||||
|
|
||||||
const GraphView = memo(({
|
const GraphView = memo(({
|
||||||
nodeTypes, edgeTypes, onMove, onLoad,
|
nodeTypes, edgeTypes, onMove, onLoad,
|
||||||
@@ -19,9 +40,9 @@ const GraphView = memo(({
|
|||||||
selectionKeyCode, onElementsRemove, deleteKeyCode, elements,
|
selectionKeyCode, onElementsRemove, deleteKeyCode, elements,
|
||||||
showBackground, backgroundGap, backgroundColor, backgroundType,
|
showBackground, backgroundGap, backgroundColor, backgroundType,
|
||||||
onConnect
|
onConnect
|
||||||
}) => {
|
}: GraphViewProps) => {
|
||||||
const zoomPane = useRef();
|
const zoomPane = useRef<HTMLDivElement>(null);
|
||||||
const rendererNode = useRef();
|
const rendererNode = useRef<HTMLDivElement>(null);
|
||||||
const state = useStoreState(s => ({
|
const state = useStoreState(s => ({
|
||||||
width: s.width,
|
width: s.width,
|
||||||
height: s.height,
|
height: s.height,
|
||||||
@@ -65,12 +86,12 @@ const GraphView = memo(({
|
|||||||
}, [state.d3Initialised]);
|
}, [state.d3Initialised]);
|
||||||
|
|
||||||
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
|
||||||
useElementUpdater({ elements });
|
useElementUpdater(elements);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__renderer" ref={rendererNode}>
|
<div className="react-flow__renderer" ref={rendererNode}>
|
||||||
{showBackground && (
|
{showBackground && (
|
||||||
<BackgroundRenderer
|
<BackgroundGrid
|
||||||
gap={backgroundGap}
|
gap={backgroundGap}
|
||||||
color={backgroundColor}
|
color={backgroundColor}
|
||||||
backgroundType={backgroundType}
|
backgroundType={backgroundType}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import React, { memo } from 'react';
|
|
||||||
import { useStoreState } from 'easy-peasy';
|
|
||||||
|
|
||||||
import { isNode } from '../../utils/graph';
|
|
||||||
|
|
||||||
function renderNode(d, props, state) {
|
|
||||||
const nodeType = d.type || 'default';
|
|
||||||
|
|
||||||
if (!props.nodeTypes[nodeType]) {
|
|
||||||
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeComponent = props.nodeTypes[nodeType] || props.nodeTypes.default;
|
|
||||||
const selected = state.selectedElements
|
|
||||||
.filter(isNode)
|
|
||||||
.map(e => e.id)
|
|
||||||
.includes(d.id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeComponent
|
|
||||||
key={d.id}
|
|
||||||
id={d.id}
|
|
||||||
type={d.type}
|
|
||||||
data={d.data}
|
|
||||||
xPos={d.__rg.position.x}
|
|
||||||
yPos={d.__rg.position.y}
|
|
||||||
onClick={props.onElementClick}
|
|
||||||
onNodeDragStop={props.onNodeDragStop}
|
|
||||||
transform={state.transform}
|
|
||||||
selected={selected}
|
|
||||||
style={d.style}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeRenderer = memo((props) => {
|
|
||||||
const state = useStoreState(s => ({
|
|
||||||
nodes: s.nodes,
|
|
||||||
transform: s.transform,
|
|
||||||
selectedElements: s.selectedElements
|
|
||||||
}));
|
|
||||||
|
|
||||||
const { transform, nodes } = state;
|
|
||||||
const transformStyle = { transform : `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="react-flow__nodes"
|
|
||||||
style={transformStyle}
|
|
||||||
>
|
|
||||||
{nodes.map(d => renderNode(d, props, state))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NodeRenderer.displayName = 'NodeRenderer';
|
|
||||||
NodeRenderer.whyDidYouRender = false;
|
|
||||||
|
|
||||||
export default NodeRenderer;
|
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { memo, ComponentType } from 'react';
|
||||||
|
|
||||||
|
import { useStoreState } from '../../store/hooks';
|
||||||
|
import { isNode } from '../../utils/graph';
|
||||||
|
import { Node, Transform, NodeTypesType, NodeComponentProps, } from '../../types';
|
||||||
|
|
||||||
|
interface NodeRendererProps {
|
||||||
|
nodeTypes: NodeTypesType;
|
||||||
|
onElementClick: () => void;
|
||||||
|
onNodeDragStop: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NodeRendererState {
|
||||||
|
nodes: Node[];
|
||||||
|
transform: Transform;
|
||||||
|
selectedElements: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderNode(node: Node, props: NodeRendererProps, state: NodeRendererState) {
|
||||||
|
const nodeType = node.type || 'default';
|
||||||
|
|
||||||
|
if (!props.nodeTypes[nodeType]) {
|
||||||
|
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<NodeComponentProps>;
|
||||||
|
const selected = state.selectedElements
|
||||||
|
.filter(isNode)
|
||||||
|
.map((e: Node) => e.id)
|
||||||
|
.includes(node.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeComponent
|
||||||
|
key={node.id}
|
||||||
|
id={node.id}
|
||||||
|
type={node.type}
|
||||||
|
data={node.data}
|
||||||
|
xPos={node.__rg.position.x}
|
||||||
|
yPos={node.__rg.position.y}
|
||||||
|
onClick={props.onElementClick}
|
||||||
|
onNodeDragStop={props.onNodeDragStop}
|
||||||
|
transform={state.transform}
|
||||||
|
selected={selected}
|
||||||
|
style={node.style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeRenderer = memo((props: NodeRendererProps) => {
|
||||||
|
const state: NodeRendererState = useStoreState(s => ({
|
||||||
|
nodes: s.nodes,
|
||||||
|
transform: s.transform,
|
||||||
|
selectedElements: s.selectedElements
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { transform, nodes } = state;
|
||||||
|
const transformStyle = { transform : `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="react-flow__nodes"
|
||||||
|
style={transformStyle}
|
||||||
|
>
|
||||||
|
{nodes.map(node => renderNode(node, props, state))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
NodeRenderer.displayName = 'NodeRenderer';
|
||||||
|
|
||||||
|
export default NodeRenderer;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import DefaultNode from '../../components/Nodes/DefaultNode';
|
|
||||||
import InputNode from '../../components/Nodes/InputNode';
|
|
||||||
import OutputNode from '../../components/Nodes/OutputNode';
|
|
||||||
import wrapNode from '../../components/Nodes/wrapNode';
|
|
||||||
|
|
||||||
export function createNodeTypes(nodeTypes) {
|
|
||||||
const standardTypes = {
|
|
||||||
input: wrapNode(nodeTypes.input || InputNode),
|
|
||||||
default: wrapNode(nodeTypes.default || DefaultNode),
|
|
||||||
output: wrapNode(nodeTypes.output || OutputNode)
|
|
||||||
};
|
|
||||||
|
|
||||||
const specialTypes = Object
|
|
||||||
.keys(nodeTypes)
|
|
||||||
.filter(k => !['input', 'default', 'output'].includes(k))
|
|
||||||
.reduce((res, key) => {
|
|
||||||
res[key] = wrapNode(nodeTypes[key] || DefaultNode);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...standardTypes,
|
|
||||||
...specialTypes
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ComponentType } from 'react';
|
||||||
|
|
||||||
|
import DefaultNode from '../../components/Nodes/DefaultNode';
|
||||||
|
import InputNode from '../../components/Nodes/InputNode';
|
||||||
|
import OutputNode from '../../components/Nodes/OutputNode';
|
||||||
|
import wrapNode from '../../components/Nodes/wrapNode';
|
||||||
|
import { NodeTypesType, NodeComponentProps } from '../../types';
|
||||||
|
|
||||||
|
export function createNodeTypes(nodeTypes: NodeTypesType): NodeTypesType {
|
||||||
|
const standardTypes: NodeTypesType = {
|
||||||
|
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeComponentProps>),
|
||||||
|
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeComponentProps>),
|
||||||
|
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeComponentProps>)
|
||||||
|
};
|
||||||
|
|
||||||
|
const specialTypes: NodeTypesType = Object
|
||||||
|
.keys(nodeTypes)
|
||||||
|
.filter(k => !['input', 'default', 'output'].includes(k))
|
||||||
|
.reduce((res, key) => {
|
||||||
|
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeComponentProps>);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...standardTypes,
|
||||||
|
...specialTypes
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, CSSProperties, ReactNode, SVGAttributes } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { StoreProvider } from 'easy-peasy';
|
import { StoreProvider } from 'easy-peasy';
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
const nodeEnv: string = (process.env.NODE_ENV as string);
|
||||||
|
|
||||||
|
if (nodeEnv !== 'production') {
|
||||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||||
whyDidYouRender(React);
|
whyDidYouRender(React);
|
||||||
}
|
}
|
||||||
@@ -17,16 +18,40 @@ 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 } from '../../types';
|
||||||
|
|
||||||
import '../../style.css';
|
import '../../style.css';
|
||||||
|
|
||||||
|
export interface ReactFlowProps {
|
||||||
|
elements: Elements,
|
||||||
|
style?: CSSProperties,
|
||||||
|
className?: string,
|
||||||
|
children?: ReactNode[],
|
||||||
|
onElementClick: () => void,
|
||||||
|
onElementsRemove: (elements: Elements) => void,
|
||||||
|
onNodeDragStop: () => void,
|
||||||
|
onConnect: () => void,
|
||||||
|
onLoad: OnLoadFunc,
|
||||||
|
onMove: () => void,
|
||||||
|
nodeTypes: NodeTypesType,
|
||||||
|
edgeTypes: EdgeTypesType,
|
||||||
|
connectionLineType: string,
|
||||||
|
connectionLineStyle: SVGAttributes<{}>,
|
||||||
|
deleteKeyCode: number,
|
||||||
|
selectionKeyCode: number,
|
||||||
|
showBackground: boolean,
|
||||||
|
backgroundGap: number,
|
||||||
|
backgroundColor: string,
|
||||||
|
backgroundType: GridType
|
||||||
|
};
|
||||||
|
|
||||||
const ReactFlow = ({
|
const ReactFlow = ({
|
||||||
style, onElementClick, elements, children,
|
style, onElementClick, elements, children,
|
||||||
nodeTypes, edgeTypes, onLoad, onMove,
|
nodeTypes, edgeTypes, onLoad, onMove,
|
||||||
onElementsRemove, onConnect, onNodeDragStop, connectionLineType,
|
onElementsRemove, onConnect, onNodeDragStop, connectionLineType,
|
||||||
connectionLineStyle, deleteKeyCode, selectionKeyCode,
|
connectionLineStyle, deleteKeyCode, selectionKeyCode,
|
||||||
showBackground, backgroundGap, backgroundType, backgroundColor
|
showBackground, backgroundGap, backgroundType, backgroundColor
|
||||||
}) => {
|
}: ReactFlowProps) => {
|
||||||
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
const nodeTypesParsed = useMemo(() => createNodeTypes(nodeTypes), []);
|
||||||
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
|
const edgeTypesParsed = useMemo(() => createEdgeTypes(edgeTypes), []);
|
||||||
|
|
||||||
@@ -60,25 +85,6 @@ const ReactFlow = ({
|
|||||||
|
|
||||||
ReactFlow.displayName = 'ReactFlow';
|
ReactFlow.displayName = 'ReactFlow';
|
||||||
|
|
||||||
ReactFlow.propTypes = {
|
|
||||||
onElementClick: PropTypes.func,
|
|
||||||
onElementsRemove: PropTypes.func,
|
|
||||||
onNodeDragStop: PropTypes.func,
|
|
||||||
onConnect: PropTypes.func,
|
|
||||||
onLoad: PropTypes.func,
|
|
||||||
onMove: PropTypes.func,
|
|
||||||
nodeTypes: PropTypes.object,
|
|
||||||
edgeTypes: PropTypes.object,
|
|
||||||
connectionLineType: PropTypes.string,
|
|
||||||
connectionLineStyle: PropTypes.object,
|
|
||||||
deleteKeyCode: PropTypes.number,
|
|
||||||
selectionKeyCode: PropTypes.number,
|
|
||||||
gridColor: PropTypes.string,
|
|
||||||
gridGap: PropTypes.number,
|
|
||||||
showBackground: PropTypes.bool,
|
|
||||||
backgroundType: PropTypes.oneOf(['lines', 'dots'])
|
|
||||||
};
|
|
||||||
|
|
||||||
ReactFlow.defaultProps = {
|
ReactFlow.defaultProps = {
|
||||||
onElementClick: () => {},
|
onElementClick: () => {},
|
||||||
onElementsRemove: () => {},
|
onElementsRemove: () => {},
|
||||||
@@ -103,7 +109,7 @@ ReactFlow.defaultProps = {
|
|||||||
backgroundColor: '#eee',
|
backgroundColor: '#eee',
|
||||||
backgroundGap: 24,
|
backgroundGap: 24,
|
||||||
showBackground: true,
|
showBackground: true,
|
||||||
backgroundType: 'dots'
|
backgroundType: GridType.Dots
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ReactFlow;
|
export default ReactFlow;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
export const NodeIdContext = createContext(null);
|
import { ElementId } from '../types';
|
||||||
|
|
||||||
|
type ContextProps = ElementId | null;
|
||||||
|
|
||||||
|
export const NodeIdContext = createContext<Partial<ContextProps>>(null);
|
||||||
export const Provider = NodeIdContext.Provider;
|
export const Provider = NodeIdContext.Provider;
|
||||||
export const Consumer = NodeIdContext.Consumer;
|
export const Consumer = NodeIdContext.Consumer;
|
||||||
|
|
||||||
Provider.displayName = 'NodeIdProvider';
|
|
||||||
|
|
||||||
export default NodeIdContext;
|
export default NodeIdContext;
|
||||||
Vendored
+13
@@ -0,0 +1,13 @@
|
|||||||
|
declare module '*.css' {
|
||||||
|
const content: { [className: string]: string };
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SvgrComponent extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
|
||||||
|
|
||||||
|
declare module '*.svg' {
|
||||||
|
const svgUrl: string;
|
||||||
|
const svgComponent: SvgrComponent;
|
||||||
|
export default svgUrl;
|
||||||
|
export { svgComponent as ReactComponent }
|
||||||
|
}
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, MutableRefObject } from 'react';
|
||||||
import * as d3Zoom from 'd3-zoom';
|
import * as d3Zoom from 'd3-zoom';
|
||||||
import { select, event } from 'd3-selection';
|
import { select, event } from 'd3-selection';
|
||||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
|
||||||
|
import { useStoreState, useStoreActions } from '../store/hooks';
|
||||||
|
|
||||||
const d3ZoomInstance = d3Zoom
|
const d3ZoomInstance = d3Zoom
|
||||||
.zoom()
|
.zoom()
|
||||||
.scaleExtent([0.5, 2])
|
.scaleExtent([0.5, 2])
|
||||||
.filter(() => !event.button);
|
.filter(() => !event.button);
|
||||||
|
|
||||||
export default function useD3Zoom(zoomPane, onMove, shiftPressed) {
|
export default (zoomPane: MutableRefObject<HTMLDivElement>, onMove: () => void, shiftPressed: boolean): void => {
|
||||||
const state = useStoreState(s => ({
|
const state = useStoreState(s => ({
|
||||||
transform: s.transform,
|
transform: s.transform,
|
||||||
d3Selection: s.d3Selection,
|
d3Selection: s.d3Selection,
|
||||||
d3Zoom: s.d3Zoom,
|
d3Zoom: s.d3Zoom,
|
||||||
edges: s.edged,
|
|
||||||
d3Initialised: s.d3Initialised,
|
|
||||||
nodesSelectionActive: s.nodesSelectionActive
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const initD3 = useStoreActions(actions => actions.initD3);
|
const initD3 = useStoreActions(actions => actions.initD3);
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
|
||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
|
|
||||||
|
import { useStoreState, useStoreActions } from '../store/hooks';
|
||||||
import { parseElement, isNode, isEdge } from '../utils/graph';
|
import { parseElement, isNode, isEdge } from '../utils/graph';
|
||||||
|
import { Elements, Node, Edge } from '../types';
|
||||||
|
|
||||||
const useElementUpdater = ({ elements }) => {
|
const useElementUpdater = (elements: Elements): void => {
|
||||||
const state = useStoreState(s => ({
|
const state = useStoreState(s => ({
|
||||||
nodes: s.nodes,
|
nodes: s.nodes,
|
||||||
edges: s.edges,
|
edges: s.edges,
|
||||||
@@ -15,10 +16,10 @@ const useElementUpdater = ({ elements }) => {
|
|||||||
const setEdges = useStoreActions(a => a.setEdges);
|
const setEdges = useStoreActions(a => a.setEdges);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nodes = elements.filter(isNode);
|
const nodes = elements.filter(isNode) as Node[];
|
||||||
const edges = elements.filter(isEdge).map(parseElement);
|
const edges = elements.filter(isEdge).map(e => parseElement(e)) as Edge[];
|
||||||
|
|
||||||
const nextNodes = nodes.map(propNode => {
|
const nextNodes = nodes.map((propNode) => {
|
||||||
const existingNode = state.nodes.find(n => n.id === propNode.id);
|
const existingNode = state.nodes.find(n => n.id === propNode.id);
|
||||||
|
|
||||||
if (existingNode) {
|
if (existingNode) {
|
||||||
@@ -32,10 +33,10 @@ const useElementUpdater = ({ elements }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return parseElement(propNode, state.transform);
|
return parseElement(propNode, state.transform);
|
||||||
});
|
}) as Node[];
|
||||||
|
|
||||||
const nodesChanged = !isEqual(state.nodes, nextNodes);
|
const nodesChanged: boolean = !isEqual(state.nodes, nextNodes);
|
||||||
const edgesChanged = !isEqual(state.edges, edges);
|
const edgesChanged: boolean = !isEqual(state.edges, edges);
|
||||||
|
|
||||||
if (nodesChanged) {
|
if (nodesChanged) {
|
||||||
setNodes(nextNodes);
|
setNodes(nextNodes);
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
|
||||||
|
|
||||||
|
import { useStoreState, useStoreActions } from '../store/hooks';
|
||||||
import useKeyPress from './useKeyPress';
|
import useKeyPress from './useKeyPress';
|
||||||
import { isEdge, getConnectedEdges } from '../utils/graph';
|
import { isEdge, getConnectedEdges } from '../utils/graph';
|
||||||
|
import { Elements, Node } from '../types';
|
||||||
|
|
||||||
export default ({ deleteKeyCode, onElementsRemove }) => {
|
interface HookParams {
|
||||||
|
deleteKeyCode: number;
|
||||||
|
onElementsRemove: (elements: Elements) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
|
||||||
const state = useStoreState(s => ({ selectedElements: s.selectedElements, edges: s.edges }))
|
const state = useStoreState(s => ({ selectedElements: s.selectedElements, edges: s.edges }))
|
||||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
@@ -15,7 +21,8 @@ export default ({ deleteKeyCode, onElementsRemove }) => {
|
|||||||
|
|
||||||
// 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 (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) {
|
||||||
const connectedEdges = getConnectedEdges(state.selectedElements, state.edges);
|
const node = state.selectedElements[0] as unknown as Node;
|
||||||
|
const connectedEdges = getConnectedEdges([node], state.edges);
|
||||||
elementsToRemove = [...state.selectedElements, ...connectedEdges];
|
elementsToRemove = [...state.selectedElements, ...connectedEdges];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import { inInputDOMNode } from '../utils';
|
import { isInputDOMNode } from '../utils';
|
||||||
|
|
||||||
export default function useKeyPress(keyCode) {
|
export default (keyCode: number) => {
|
||||||
const [keyPressed, setKeyPressed] = useState(false);
|
const [keyPressed, setKeyPressed] = useState(false);
|
||||||
|
|
||||||
function downHandler(evt) {
|
function downHandler(evt: KeyboardEvent) {
|
||||||
if (evt.keyCode === keyCode && !inInputDOMNode(evt.target)) {
|
if (evt.keyCode === keyCode && !isInputDOMNode(evt)) {
|
||||||
setKeyPressed(true);
|
setKeyPressed(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const upHandler = (evt) => {
|
const upHandler = (evt: KeyboardEvent) => {
|
||||||
if (evt.keyCode === keyCode && !inInputDOMNode(evt.target)) {
|
if (evt.keyCode === keyCode && !isInputDOMNode(evt)) {
|
||||||
setKeyPressed(false);
|
setKeyPressed(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -20,6 +20,7 @@ export default function useKeyPress(keyCode) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('keydown', downHandler);
|
window.addEventListener('keydown', downHandler);
|
||||||
window.addEventListener('keyup', upHandler);
|
window.addEventListener('keyup', upHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('keydown', downHandler);
|
window.removeEventListener('keydown', downHandler);
|
||||||
window.removeEventListener('keyup', upHandler);
|
window.removeEventListener('keyup', upHandler);
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
import React from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
|
||||||
import PlusIcon from '../../../assets/icons/plus.svg';
|
import PlusIcon from '../../../assets/icons/plus.svg';
|
||||||
import MinusIcon from '../../../assets/icons/minus.svg';
|
import MinusIcon from '../../../assets/icons/minus.svg';
|
||||||
import FitviewIcon from '../../../assets/icons/fitview.svg';
|
import FitviewIcon from '../../../assets/icons/fitview.svg';
|
||||||
|
|
||||||
const baseStyle = {
|
const baseStyle: CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
left: 10,
|
left: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ style, className }) => {
|
interface ControlProps {
|
||||||
const mapClasses = classnames('react-flow__controls', className);
|
style?: CSSProperties;
|
||||||
|
className?: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({ style, className }: ControlProps) => {
|
||||||
|
const mapClasses: string = classnames('react-flow__controls', className);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -38,7 +43,7 @@ export default ({ style, className }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="react-flow__controls-button react-flow__controls-fitview"
|
className="react-flow__controls-button react-flow__controls-fitview"
|
||||||
onClick={fitView}
|
onClick={() => fitView()}
|
||||||
>
|
>
|
||||||
<FitviewIcon />
|
<FitviewIcon />
|
||||||
</div>
|
</div>
|
||||||
@@ -1,11 +1,20 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect, CSSProperties } from 'react';
|
||||||
import { useStoreState } from 'easy-peasy';
|
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import { isFunction } from '../../utils'
|
import { useStoreState } from '../../store/hooks';
|
||||||
import { getNodesInside } from '../../utils/graph';
|
import { getNodesInside } from '../../utils/graph';
|
||||||
|
import { Node } from '../../types';
|
||||||
|
|
||||||
const baseStyle = {
|
type StringFunc = (node: Node) => string;
|
||||||
|
|
||||||
|
interface MiniMapProps {
|
||||||
|
style?: CSSProperties;
|
||||||
|
className?: string | null;
|
||||||
|
bgColor?: string;
|
||||||
|
nodeColor?: string | StringFunc;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseStyle: CSSProperties = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
zIndex: 5,
|
zIndex: 5,
|
||||||
bottom: 10,
|
bottom: 10,
|
||||||
@@ -13,23 +22,26 @@ const baseStyle = {
|
|||||||
width: 200
|
width: 200
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ({ style = {}, className, bgColor = '#f8f8f8', nodeColor = '#ddd' }) => {
|
export default (
|
||||||
|
{ style = {}, className, bgColor = '#f8f8f8', nodeColor = '#ddd' }: MiniMapProps
|
||||||
|
) => {
|
||||||
const canvasNode = useRef(null);
|
const canvasNode = useRef(null);
|
||||||
const state = useStoreState(s => ({
|
const state = useStoreState(s => ({
|
||||||
width: s.width,
|
width: s.width,
|
||||||
height: s.height,
|
height: s.height,
|
||||||
nodes: s.nodes,
|
nodes: s.nodes,
|
||||||
transform: s.transform,
|
transform: s.transform,
|
||||||
})); const mapClasses = classnames('react-flow__minimap', className);
|
}));
|
||||||
|
const mapClasses = classnames('react-flow__minimap', className);
|
||||||
const nodePositions = state.nodes.map(n => n.__rg.position);
|
const nodePositions = state.nodes.map(n => n.__rg.position);
|
||||||
const width = style.width || baseStyle.width;
|
const width: number = +(style.width || baseStyle.width || 0);
|
||||||
const height = (state.height / (state.width || 1)) * width;
|
const height = (state.height / (state.width || 1)) * width;
|
||||||
const bbox = { x: 0, y: 0, width: state.width, height: state.height };
|
const bbox = { x: 0, y: 0, width: state.width, height: state.height };
|
||||||
const scaleFactor = width / state.width;
|
const scaleFactor = width / state.width;
|
||||||
const nodeColorFunc = isFunction(nodeColor) ? nodeColor : () => nodeColor;
|
const nodeColorFunc = (nodeColor instanceof Function ? nodeColor: () => nodeColor) as StringFunc;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canvasNode) {
|
if (canvasNode && canvasNode.current) {
|
||||||
const ctx = canvasNode.current.getContext('2d');
|
const ctx = canvasNode.current.getContext('2d');
|
||||||
const nodesInside = getNodesInside(state.nodes, bbox, state.transform, true);
|
const nodesInside = getNodesInside(state.nodes, bbox, state.transform, true);
|
||||||
|
|
||||||
@@ -53,7 +65,7 @@ export default ({ style = {}, className, bgColor = '#f8f8f8', nodeColor = '#ddd'
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [nodePositions, state.transform, height])
|
}, [canvasNode.current, nodePositions, state.transform, height])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<canvas
|
<canvas
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createTypedHooks } from 'easy-peasy';
|
||||||
|
|
||||||
|
import { StoreModel } from './index';
|
||||||
|
|
||||||
|
const typedHooks = createTypedHooks<StoreModel>();
|
||||||
|
|
||||||
|
export const useStoreActions = typedHooks.useStoreActions;
|
||||||
|
export const useStoreDispatch = typedHooks.useStoreDispatch;
|
||||||
|
export const useStoreState = typedHooks.useStoreState;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { createStore } from 'easy-peasy';
|
|
||||||
|
|
||||||
import actions from './actions';
|
|
||||||
|
|
||||||
const store = createStore({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
transform: [0, 0, 1],
|
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
selectedElements: [],
|
|
||||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
|
||||||
|
|
||||||
d3Zoom: null,
|
|
||||||
d3Selection: null,
|
|
||||||
d3Initialised: false,
|
|
||||||
|
|
||||||
nodesSelectionActive: false,
|
|
||||||
selectionActive: false,
|
|
||||||
selection: {},
|
|
||||||
|
|
||||||
connectionSourceId: null,
|
|
||||||
connectionPosition: { x: 0, y: 0 },
|
|
||||||
|
|
||||||
onConnect: () => {},
|
|
||||||
|
|
||||||
...actions
|
|
||||||
});
|
|
||||||
|
|
||||||
export default store;
|
|
||||||
@@ -1,9 +1,118 @@
|
|||||||
import { action } from 'easy-peasy';
|
import { createStore, Action, action } from 'easy-peasy';
|
||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
||||||
|
|
||||||
import { getBoundingBox, getNodesInside, getConnectedEdges } from '../utils/graph';
|
import { getBoundingBox, getNodesInside, getConnectedEdges } from '../utils/graph';
|
||||||
|
import {
|
||||||
|
ElementId, Elements, Transform, Node,
|
||||||
|
Edge, Rect, Dimensions, XYPosition,
|
||||||
|
OnConnectFunc, SelectionRect, HandleElement
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
type TransformXYK = {
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
k: number
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodePosUpdate = {
|
||||||
|
id: ElementId,
|
||||||
|
pos: XYPosition
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeUpdate = {
|
||||||
|
id: ElementId,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
handleBounds: {
|
||||||
|
source: HandleElement,
|
||||||
|
target: HandleElement
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectionUpdate = {
|
||||||
|
isActive: boolean;
|
||||||
|
selection?: SelectionRect;
|
||||||
|
};
|
||||||
|
|
||||||
|
type D3Init = {
|
||||||
|
zoom: ZoomBehavior<Element, unknown>;
|
||||||
|
selection: D3Selection<Element, unknown, null, undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface StoreModel {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
transform: Transform;
|
||||||
|
nodes: Node[];
|
||||||
|
edges: Edge[];
|
||||||
|
selectedElements: Elements;
|
||||||
|
selectedNodesBbox: Rect;
|
||||||
|
|
||||||
|
d3Zoom: ZoomBehavior<Element, unknown>;
|
||||||
|
d3Selection: D3Selection<Element, unknown, null, undefined>;
|
||||||
|
d3Initialised: boolean;
|
||||||
|
|
||||||
|
nodesSelectionActive: boolean;
|
||||||
|
selectionActive: boolean;
|
||||||
|
selection: SelectionRect | null;
|
||||||
|
|
||||||
|
connectionSourceId: ElementId | null;
|
||||||
|
connectionPosition: XYPosition;
|
||||||
|
|
||||||
|
onConnect: OnConnectFunc;
|
||||||
|
|
||||||
|
setOnConnect: Action<StoreModel, OnConnectFunc>;
|
||||||
|
|
||||||
|
setNodes: Action<StoreModel, Node[]>;
|
||||||
|
|
||||||
|
setEdges: Action<StoreModel, Edge[]>;
|
||||||
|
|
||||||
|
updateNodeData: Action<StoreModel, NodeUpdate>;
|
||||||
|
|
||||||
|
updateNodePos: Action<StoreModel, NodePosUpdate>;
|
||||||
|
|
||||||
|
setSelection: Action<StoreModel, boolean>;
|
||||||
|
|
||||||
|
setNodesSelection: Action<StoreModel, SelectionUpdate>;
|
||||||
|
|
||||||
|
setSelectedElements: Action<StoreModel, Elements | Node | Edge>
|
||||||
|
|
||||||
|
updateSelection: Action<StoreModel, SelectionRect>;
|
||||||
|
|
||||||
|
updateTransform: Action<StoreModel, TransformXYK>;
|
||||||
|
|
||||||
|
updateSize: Action<StoreModel, Dimensions>;
|
||||||
|
|
||||||
|
initD3: Action<StoreModel, D3Init>;
|
||||||
|
|
||||||
|
setConnectionPosition: Action<StoreModel, XYPosition>;
|
||||||
|
|
||||||
|
setConnectionSourceId: Action<StoreModel, ElementId>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeModel: StoreModel = {
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
transform: [0, 0, 1],
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
selectedElements: [],
|
||||||
|
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
||||||
|
|
||||||
|
d3Zoom: null,
|
||||||
|
d3Selection: null,
|
||||||
|
d3Initialised: false,
|
||||||
|
|
||||||
|
nodesSelectionActive: false,
|
||||||
|
selectionActive: false,
|
||||||
|
selection: null,
|
||||||
|
|
||||||
|
connectionSourceId: null,
|
||||||
|
connectionPosition: { x: 0, y: 0 },
|
||||||
|
|
||||||
|
onConnect: () => {},
|
||||||
|
|
||||||
export default {
|
|
||||||
setOnConnect: action((state, onConnect) => {
|
setOnConnect: action((state, onConnect) => {
|
||||||
state.onConnect = onConnect;
|
state.onConnect = onConnect;
|
||||||
}),
|
}),
|
||||||
@@ -100,3 +209,7 @@ export default {
|
|||||||
state.connectionSourceId = sourceId;
|
state.connectionSourceId = sourceId;
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const store = createStore(storeModel);
|
||||||
|
|
||||||
|
export default store;
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { CSSProperties, SVGAttributes } from 'react';
|
||||||
|
|
||||||
|
export type ElementId = string;
|
||||||
|
|
||||||
|
export type Elements = Array<Node | Edge>;
|
||||||
|
|
||||||
|
export type Transform = [number, number, number];
|
||||||
|
|
||||||
|
export type Position = 'left' | 'top' | 'right' | 'bottom';
|
||||||
|
|
||||||
|
export type XYPosition = {
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum GridType {
|
||||||
|
Lines = 'lines',
|
||||||
|
Dots = 'dots',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HandleType = 'source' | 'target';
|
||||||
|
|
||||||
|
export type NodeTypesType = { [key: string]: React.ReactNode };
|
||||||
|
|
||||||
|
export type EdgeTypesType = NodeTypesType;
|
||||||
|
|
||||||
|
export interface Dimensions {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rect extends Dimensions {
|
||||||
|
x: number,
|
||||||
|
y: number
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SelectionRect extends Rect {
|
||||||
|
startX: number;
|
||||||
|
startY: number;
|
||||||
|
draw: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Node {
|
||||||
|
id: ElementId,
|
||||||
|
position?: XYPosition,
|
||||||
|
type?: string,
|
||||||
|
__rg?: any,
|
||||||
|
data?: any,
|
||||||
|
style?: CSSProperties
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Edge {
|
||||||
|
id: ElementId,
|
||||||
|
type?: string,
|
||||||
|
source: ElementId,
|
||||||
|
target: ElementId,
|
||||||
|
style?: SVGAttributes<{}>
|
||||||
|
animated?: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EdgeProps {
|
||||||
|
sourceX: number,
|
||||||
|
sourceY: number,
|
||||||
|
targetX: number,
|
||||||
|
targetY: number,
|
||||||
|
style?: SVGAttributes<{}>
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EdgeBezierProps extends EdgeProps{
|
||||||
|
sourcePosition: Position,
|
||||||
|
targetPosition: Position
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NodeProps {
|
||||||
|
id: ElementId,
|
||||||
|
type: string,
|
||||||
|
data: any;
|
||||||
|
selected: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NodeComponentProps {
|
||||||
|
id: ElementId,
|
||||||
|
type: string;
|
||||||
|
data: any;
|
||||||
|
selected?: boolean;
|
||||||
|
transform?: Transform;
|
||||||
|
xPos?: number;
|
||||||
|
yPos?: number;
|
||||||
|
onClick?: () => any;
|
||||||
|
onNodeDragStop?: () => any;
|
||||||
|
style?: CSSProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FitViewParams = {
|
||||||
|
padding: number
|
||||||
|
};
|
||||||
|
export type FitViewFunc = (fitViewOptions: FitViewParams) => void;
|
||||||
|
|
||||||
|
type OnLoadParams = {
|
||||||
|
zoomIn: () => void;
|
||||||
|
zoomOut: () => void;
|
||||||
|
fitView: FitViewFunc
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnLoadFunc = (params: OnLoadParams) => void;
|
||||||
|
|
||||||
|
export type OnConnectParams = {
|
||||||
|
source: ElementId;
|
||||||
|
target: ElementId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnConnectFunc = (params: OnConnectParams) => void;
|
||||||
|
|
||||||
|
export type Connection = {
|
||||||
|
source: ElementId;
|
||||||
|
target: ElementId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface HandleElement {
|
||||||
|
id?: ElementId;
|
||||||
|
position: Position;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EdgeWrapperProps {
|
||||||
|
id: ElementId,
|
||||||
|
source: ElementId,
|
||||||
|
target: ElementId,
|
||||||
|
type: any,
|
||||||
|
onClick?: (edge: Edge) => void
|
||||||
|
animated?: boolean,
|
||||||
|
selected?: boolean,
|
||||||
|
};
|
||||||
@@ -1,51 +1,54 @@
|
|||||||
import { zoomIdentity } from 'd3-zoom';
|
import { zoomIdentity } from 'd3-zoom';
|
||||||
|
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import { isDefined } from './index';
|
import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams } from '../types';
|
||||||
|
|
||||||
export const isEdge = element => element.source && element.target;
|
export const isEdge = (element: Node | Edge): boolean =>
|
||||||
|
element.hasOwnProperty('source') && element.hasOwnProperty('target');
|
||||||
|
|
||||||
export const isNode = element => !element.source && !element.target;
|
export const isNode = (element: Node | Edge): boolean =>
|
||||||
|
!element.hasOwnProperty('source') && !element.hasOwnProperty('target');
|
||||||
|
|
||||||
export const getOutgoers = (node, elements) => {
|
export const getOutgoers = (node: Node, elements: Elements): Elements => {
|
||||||
if (!isNode(node)) {
|
if (!isNode(node)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const outgoerIds = elements.filter(e => e.source === node.id).map(e => e.target);
|
const outgoerIds = elements.filter((e: Edge) => e.source === node.id).map((e: Edge) => e.target);
|
||||||
return elements.filter(e => outgoerIds.includes(e.id));
|
return elements.filter(e => outgoerIds.includes(e.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeElements = (elementsToRemove, elements) => {
|
export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => {
|
||||||
const nodeIdsToRemove = elementsToRemove.map(n => n.id);
|
const nodeIdsToRemove = elementsToRemove.map(n => n.id);
|
||||||
|
|
||||||
return elements.filter(e => {
|
return elements.filter((element) => {
|
||||||
return (
|
const edgeElement = element as Edge;
|
||||||
!nodeIdsToRemove.includes(e.id) &&
|
return !(
|
||||||
!nodeIdsToRemove.includes(e.target) &&
|
nodeIdsToRemove.includes(element.id) ||
|
||||||
!nodeIdsToRemove.includes(e.source)
|
nodeIdsToRemove.includes(edgeElement.target) ||
|
||||||
|
nodeIdsToRemove.includes(edgeElement.source)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function getEdgeId(params) {
|
function getEdgeId(edgeParams: Edge): ElementId {
|
||||||
return `reactflow__edge-${params.source}-${params.target}`;
|
return `reactflow__edge-${edgeParams.source}-${edgeParams.target}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addEdge = (edgeParams, elements) => {
|
export const addEdge = (edgeParams: Edge, elements: Elements): Elements => {
|
||||||
if (!edgeParams.source || !edgeParams.target) {
|
if (!edgeParams.source || !edgeParams.target) {
|
||||||
throw new Error('Can not create edge. An edge needs a source and a target');
|
throw new Error('Can not create edge. An edge needs a source and a target');
|
||||||
}
|
}
|
||||||
|
|
||||||
return elements.concat({
|
return elements.concat({
|
||||||
...edgeParams,
|
...edgeParams,
|
||||||
id: isDefined(edgeParams.id) ? edgeParams.id : getEdgeId(edgeParams)
|
id: typeof edgeParams.id !== 'undefined' ? edgeParams.id : getEdgeId(edgeParams)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pointToRendererPoint = ({ x, y }, transform) => {
|
const pointToRendererPoint = ({ x, y }: XYPosition, transform: Transform): XYPosition => {
|
||||||
const rendererX = (x - transform[0]) * (1 / [transform[2]]);
|
const rendererX = (x - transform[0]) * (1 / transform[2]);
|
||||||
const rendererY = (y - transform[1]) * (1 / [transform[2]]);
|
const rendererY = (y - transform[1]) * (1 / transform[2]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: rendererX,
|
x: rendererX,
|
||||||
@@ -53,25 +56,27 @@ const pointToRendererPoint = ({ x, y }, transform) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parseElement = (e, transform) => {
|
export const parseElement = (element: Node | Edge, transform?: Transform): Node | Edge => {
|
||||||
if (!e.id) {
|
if (!element.id) {
|
||||||
throw new Error('All elements (nodes and edges) need to have an id.',)
|
throw new Error('All elements (nodes and edges) need to have an id.',)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEdge(e)) {
|
if (isEdge(element)) {
|
||||||
return {
|
return {
|
||||||
...e,
|
...element,
|
||||||
id: e.id.toString(),
|
id: element.id.toString(),
|
||||||
type: e.type || 'default'
|
type: element.type || 'default'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodeElement = element as Node;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...e,
|
...nodeElement,
|
||||||
id: e.id.toString(),
|
id: nodeElement.id.toString(),
|
||||||
type: e.type || 'default',
|
type: nodeElement.type || 'default',
|
||||||
__rg: {
|
__rg: {
|
||||||
position: pointToRendererPoint(e.position, transform),
|
position: pointToRendererPoint(nodeElement.position, transform),
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
handleBounds : {}
|
handleBounds : {}
|
||||||
@@ -79,7 +84,7 @@ export const parseElement = (e, transform) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundingBox = (nodes) => {
|
export const getBoundingBox = (nodes: Node[]): Rect => {
|
||||||
const bbox = nodes.reduce((res, node) => {
|
const bbox = nodes.reduce((res, node) => {
|
||||||
const { position } = node.__rg;
|
const { position } = node.__rg;
|
||||||
const x2 = position.x + node.__rg.width;
|
const x2 = position.x + node.__rg.width;
|
||||||
@@ -117,16 +122,16 @@ export const getBoundingBox = (nodes) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const graphPosToZoomedPos = (pos, transform) => {
|
export const graphPosToZoomedPos = (pos: XYPosition, transform: Transform): XYPosition => {
|
||||||
return {
|
return {
|
||||||
x: (pos.x * transform[2]) + transform[0],
|
x: (pos.x * transform[2]) + transform[0],
|
||||||
y: (pos.y * transform[2]) + transform[1]
|
y: (pos.y * transform[2]) + transform[1]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNodesInside = (nodes, bbox, transform = [0, 0, 1], partially = false) => {
|
export const getNodesInside = (nodes: Node[], bbox: Rect, transform: Transform = [0, 0, 1], partially: boolean = false): Node[] => {
|
||||||
return nodes.
|
return nodes
|
||||||
filter(n => {
|
.filter(n => {
|
||||||
const bboxPos = {
|
const bboxPos = {
|
||||||
x: (bbox.x - transform[0]) * (1 / transform[2]),
|
x: (bbox.x - transform[0]) * (1 / transform[2]),
|
||||||
y: (bbox.y - transform[1]) * (1 / transform[2])
|
y: (bbox.y - transform[1]) * (1 / transform[2])
|
||||||
@@ -146,7 +151,7 @@ export const getNodesInside = (nodes, bbox, transform = [0, 0, 1], partially = f
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConnectedEdges = (nodes, edges) => {
|
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
|
||||||
const nodeIds = nodes.map(n => n.id);
|
const nodeIds = nodes.map(n => n.id);
|
||||||
|
|
||||||
return edges.filter(e => {
|
return edges.filter(e => {
|
||||||
@@ -160,7 +165,7 @@ export const getConnectedEdges = (nodes, edges) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fitView = ({ padding = 0 } = {}) => {
|
export const fitView = ({ padding }: FitViewParams = { padding: 0 }): void => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
const bounds = getBoundingBox(state.nodes);
|
const bounds = getBoundingBox(state.nodes);
|
||||||
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
||||||
@@ -173,12 +178,12 @@ export const fitView = ({ padding = 0 } = {}) => {
|
|||||||
state.d3Selection.call(state.d3Zoom.transform, fittedTransform);
|
state.d3Selection.call(state.d3Zoom.transform, fittedTransform);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomIn = () => {
|
export const zoomIn = (): void => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] + 0.2);
|
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] + 0.2);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const zoomOut = () => {
|
export const zoomOut = (): void => {
|
||||||
const state = store.getState();
|
const state = store.getState();
|
||||||
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] - 0.2);
|
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] - 0.2);
|
||||||
};
|
};
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export const isFunction = obj => !!(obj && obj.constructor && obj.call && obj.apply);
|
|
||||||
|
|
||||||
export const isDefined = obj => typeof obj !== 'undefined';
|
|
||||||
|
|
||||||
export const inInputDOMNode = e => e && e.target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
|
|
||||||
|
|
||||||
export const getDimensions = (node = {}) => ({
|
|
||||||
width: node.offsetWidth,
|
|
||||||
height: node.offsetHeight
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { DraggableEvent } from 'react-draggable';
|
||||||
|
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
|
|
||||||
|
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
return e && target && ['INPUT', 'SELECT', 'TEXTAREA'].includes(target.nodeName);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDimensions = (node: HTMLDivElement) => ({
|
||||||
|
width: node.offsetWidth,
|
||||||
|
height: node.offsetHeight
|
||||||
|
});
|
||||||
+5
-3
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "build",
|
"outDir": "dist",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["es6", "dom", "es2016", "es2017"],
|
"lib": ["es6", "dom", "es2016", "es2017"],
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": false,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"allowSyntheticDefaultImports": true
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"suppressImplicitAnyIndexErrors": true
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["node_modules", "build", "dist", "example", "rollup.config.js"]
|
"exclude": ["node_modules", "build", "dist", "example", "rollup.config.js"]
|
||||||
|
|||||||
Reference in New Issue
Block a user