Merge pull request #1 from wbkd/refactor-state
refactor(state): use easy-peasy
This commit is contained in:
61
package-lock.json
generated
61
package-lock.json
generated
@@ -3057,6 +3057,20 @@
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"easy-peasy": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/easy-peasy/-/easy-peasy-3.0.2.tgz",
|
||||
"integrity": "sha512-qcsr81F6rZN5hEZ1lYRDUHtatxn5Qb4cF5uj5cNXKEXLXmATCxktNC53B2XvccA7K8zSCwUeoFckrCOvLeINWA==",
|
||||
"requires": {
|
||||
"immer-peasy": "3.1.3",
|
||||
"memoizerific": "^1.11.3",
|
||||
"prop-types": "^15.6.2",
|
||||
"redux": "^4.0.4",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"shallowequal": "^1.1.0",
|
||||
"ts-toolbelt": "^3.8.0"
|
||||
}
|
||||
},
|
||||
"ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
@@ -4523,6 +4537,11 @@
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
|
||||
"dev": true
|
||||
},
|
||||
"immer-peasy": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer-peasy/-/immer-peasy-3.1.3.tgz",
|
||||
"integrity": "sha512-WzoZ96A93jOmcDOLNChMWAqy+ZU8vEYQx2DcKjgo7P5SToiJs+GL+5yQbWzH8X02Lhvv6xrGgVNa1xbki66Eow=="
|
||||
},
|
||||
"import-cwd": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
|
||||
@@ -5207,6 +5226,11 @@
|
||||
"integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=",
|
||||
"dev": true
|
||||
},
|
||||
"map-or-similar": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/map-or-similar/-/map-or-similar-1.5.0.tgz",
|
||||
"integrity": "sha1-beJlMXSt+12e3DPGnT6Sobdvrwg="
|
||||
},
|
||||
"map-visit": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz",
|
||||
@@ -5287,6 +5311,14 @@
|
||||
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
|
||||
"dev": true
|
||||
},
|
||||
"memoizerific": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/memoizerific/-/memoizerific-1.11.3.tgz",
|
||||
"integrity": "sha1-fIekZGREwy11Q4VwkF8tvRsagFo=",
|
||||
"requires": {
|
||||
"map-or-similar": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"merge-source-map": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz",
|
||||
@@ -7090,6 +7122,20 @@
|
||||
"readable-stream": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.4.tgz",
|
||||
"integrity": "sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
|
||||
"integrity": "sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw=="
|
||||
},
|
||||
"regenerate": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
|
||||
@@ -7700,6 +7746,11 @@
|
||||
"integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=",
|
||||
"dev": true
|
||||
},
|
||||
"shallowequal": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
@@ -8179,6 +8230,11 @@
|
||||
"util.promisify": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"symbol-observable": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
@@ -8321,6 +8377,11 @@
|
||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
||||
"dev": true
|
||||
},
|
||||
"ts-toolbelt": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-3.8.1.tgz",
|
||||
"integrity": "sha512-XDAWEHVscqto6pG0gPSbYaEfEBcl9V0JK5pQIQJ3M+6sYnxK5ZOPyyRA/aq78WxKlLvBSXCPJ1fPX+7qCCg2Zg=="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"classnames": "^2.2.6",
|
||||
"d3-selection": "^1.4.0",
|
||||
"d3-zoom": "^1.7.3",
|
||||
"easy-peasy": "^3.0.2",
|
||||
"fast-deep-equal": "^3.0.0-beta.1",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0",
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import React, { createContext, useState, memo, useMemo } from 'react';
|
||||
|
||||
export const ConnectionContext = createContext({});
|
||||
|
||||
export const Provider = memo(({ onConnect, children }) => {
|
||||
const [sourceId, setSourceId] = useState(null);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const connectionContext = useMemo(() => ({
|
||||
sourceId,
|
||||
setSourceId,
|
||||
position,
|
||||
setPosition,
|
||||
onConnect
|
||||
}), [sourceId, position.x, position.y]);
|
||||
|
||||
return (
|
||||
<ConnectionContext.Provider value={connectionContext}>
|
||||
{children}
|
||||
</ConnectionContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
Provider.displayName = 'ConnectionProvider';
|
||||
Provider.whyDidYouRender = false;
|
||||
|
||||
export const { Consumer } = ConnectionContext;
|
||||
@@ -1,14 +1,14 @@
|
||||
import React, { memo } from 'react';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { setSelectedElements } from '../../state/actions';
|
||||
import { isInputNode } from '../../utils';
|
||||
import store from '../../store';
|
||||
|
||||
export default EdgeComponent => {
|
||||
const EdgeWrapper = memo((props) => {
|
||||
const {
|
||||
source, target, animated, type,
|
||||
dispatch, selected, onClick
|
||||
selected, onClick
|
||||
} = props;
|
||||
const edgeClasses = cx('react-graph__edge', { selected, animated });
|
||||
const onEdgeClick = (evt) => {
|
||||
@@ -16,7 +16,7 @@ export default EdgeComponent => {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch(setSelectedElements({ source, target }));
|
||||
store.dispatch.setSelectedElements({ source, target });
|
||||
onClick({ source, target, type });
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { ConnectionContext } from '../ConnectionContext';
|
||||
import ConnectionLine from '../ConnectionLine';
|
||||
import { isEdge } from '../graph-utils';
|
||||
|
||||
@@ -36,10 +35,10 @@ function getEdgePositions(sourceNode, targetNode) {
|
||||
};
|
||||
}
|
||||
|
||||
function renderEdge(e, props, graphContext) {
|
||||
function renderEdge(e, props, state) {
|
||||
const edgeType = e.type || 'default';
|
||||
const sourceNode = graphContext.state.nodes.find(n => n.id === e.source);
|
||||
const targetNode = graphContext.state.nodes.find(n => n.id === e.target);
|
||||
const sourceNode = state.nodes.find(n => n.id === e.source);
|
||||
const targetNode = state.nodes.find(n => n.id === e.target);
|
||||
|
||||
if (!sourceNode) {
|
||||
throw new Error(`couldn't create edge for source id: ${e.source}`);
|
||||
@@ -51,7 +50,7 @@ function renderEdge(e, props, graphContext) {
|
||||
|
||||
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
|
||||
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(sourceNode, targetNode);
|
||||
const selected = graphContext.state.selectedElements
|
||||
const selected = state.selectedElements
|
||||
.filter(isEdge)
|
||||
.find(elm => elm.source === e.source && elm.target === e.target);
|
||||
|
||||
@@ -62,7 +61,6 @@ function renderEdge(e, props, graphContext) {
|
||||
type={e.type}
|
||||
onClick={props.onElementClick}
|
||||
selected={selected}
|
||||
dispatch={graphContext.dispatch}
|
||||
animated={e.animated}
|
||||
style={e.style}
|
||||
source={e.source}
|
||||
@@ -76,8 +74,14 @@ function renderEdge(e, props, graphContext) {
|
||||
}
|
||||
|
||||
const EdgeRenderer = memo((props) => {
|
||||
const graphContext = useContext(GraphContext);
|
||||
const { position, sourceId : connectionSourceId } = useContext(ConnectionContext);
|
||||
const state = useStoreState(s => ({
|
||||
nodes: s.nodes,
|
||||
edges: s.edges,
|
||||
transform: s.transform,
|
||||
selectedElements: s.selectedElements,
|
||||
connectionSourceId: s.connectionSourceId,
|
||||
position: s.connectionPosition
|
||||
}));
|
||||
const {
|
||||
width, height, connectionLineStyle, connectionLineType
|
||||
} = props;
|
||||
@@ -86,7 +90,7 @@ const EdgeRenderer = memo((props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { transform, edges, nodes } = graphContext.state;
|
||||
const { transform, edges, nodes, connectionSourceId, position } = state;
|
||||
const transformStyle = `translate(${transform[0]},${transform[1]}) scale(${transform[2]})`;
|
||||
|
||||
return (
|
||||
@@ -96,7 +100,7 @@ const EdgeRenderer = memo((props) => {
|
||||
className="react-graph__edges"
|
||||
>
|
||||
<g transform={transformStyle}>
|
||||
{edges.map(e => renderEdge(e, props, graphContext))}
|
||||
{edges.map(e => renderEdge(e, props, state))}
|
||||
{connectionSourceId && (
|
||||
<ConnectionLine
|
||||
nodes={nodes}
|
||||
@@ -113,4 +117,6 @@ const EdgeRenderer = memo((props) => {
|
||||
);
|
||||
});
|
||||
|
||||
EdgeRenderer.displayName = 'EdgeRenderer';
|
||||
|
||||
export default EdgeRenderer;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useContext, memo } from 'react';
|
||||
import { useEffect, memo } from 'react';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
|
||||
import useKeyPress from '../hooks/useKeyPress';
|
||||
import { setNodesSelection } from '../state/actions';
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { isEdge, getConnectedEdges } from '../graph-utils';
|
||||
|
||||
export default memo((props) => {
|
||||
const { state, dispatch } = useContext(GraphContext);
|
||||
const state = useStoreState(s => ({ selectedElements: s.selectedElements, edges: s.edges }))
|
||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||
const removePressed = useKeyPress('Backspace');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,7 +20,7 @@ export default memo((props) => {
|
||||
}
|
||||
|
||||
props.onElementsRemove(elementsToRemove);
|
||||
dispatch(setNodesSelection({ isActive: false }));
|
||||
setNodesSelection({ isActive: false });
|
||||
}
|
||||
}, [removePressed])
|
||||
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import React, { createContext, useEffect, useReducer, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
import { reducer, initialState } from '../state';
|
||||
import { setNodes, setEdges } from '../state/actions';
|
||||
import { parseElement, isNode, isEdge } from '../graph-utils';
|
||||
|
||||
export const GraphContext = createContext({});
|
||||
|
||||
export const Provider = ({
|
||||
elements,
|
||||
children,
|
||||
}) => {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
useEffect(() => {
|
||||
const nodes = elements.filter(isNode);
|
||||
const edges = elements.filter(isEdge).map(parseElement, state.transform);
|
||||
|
||||
const nextNodes = nodes.map(propNode => {
|
||||
const existingNode = state.nodes.find(n => n.id === propNode.id);
|
||||
|
||||
if (existingNode) {
|
||||
const data = !isEqual(existingNode.data, propNode.data) ?
|
||||
{ ...existingNode.data, ...propNode.data } : existingNode.data;
|
||||
|
||||
return {
|
||||
...existingNode,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
return parseElement(propNode, state.transform);
|
||||
});
|
||||
|
||||
const nodesChanged = !isEqual(state.nodes, nextNodes);
|
||||
const edgesChanged = !isEqual(state.edges, edges);
|
||||
|
||||
if (nodesChanged) {
|
||||
dispatch(setNodes(nextNodes));
|
||||
}
|
||||
|
||||
if (edgesChanged) {
|
||||
dispatch(setEdges(edges));
|
||||
}
|
||||
});
|
||||
|
||||
const graphContext = useMemo(() => ({
|
||||
state,
|
||||
dispatch
|
||||
}), [state]);
|
||||
|
||||
return (
|
||||
<GraphContext.Provider
|
||||
value={graphContext}
|
||||
>
|
||||
{children}
|
||||
</GraphContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const { Consumer } = GraphContext;
|
||||
|
||||
Provider.displayName = 'GraphProvider';
|
||||
|
||||
Provider.propTypes = {
|
||||
elements: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
Provider.defaultProps = {
|
||||
elements: [],
|
||||
};
|
||||
@@ -1,25 +1,33 @@
|
||||
import React, { useEffect, useContext, useRef, memo } from 'react';
|
||||
import React, { useEffect, useRef, memo } from 'react';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
|
||||
import { Provider } from '../ConnectionContext';
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import NodeRenderer from '../NodeRenderer';
|
||||
import EdgeRenderer from '../EdgeRenderer';
|
||||
import UserSelection from '../UserSelection';
|
||||
import NodesSelection from '../NodesSelection';
|
||||
import { setNodesSelection } from '../state/actions';
|
||||
import { updateSize, fitView, zoomIn, zoomOut } from '../state/view-actions';
|
||||
import useKeyPress from '../hooks/useKeyPress';
|
||||
import useD3Zoom from '../hooks/useD3Zoom';
|
||||
import { getDimensions } from '../utils';
|
||||
import { fitView, zoomIn, zoomOut } from '../graph-utils';
|
||||
|
||||
const GraphView = memo((props) => {
|
||||
const zoomPane = useRef();
|
||||
const rendererNode = useRef();
|
||||
const { state, dispatch } = useContext(GraphContext);
|
||||
const state = useStoreState(s => ({
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
nodes: s.nodes,
|
||||
edges: s.edges,
|
||||
d3Initialised: s.d3Initialised,
|
||||
nodesSelectionActive: s.nodesSelectionActive
|
||||
}));
|
||||
const updateSize = useStoreActions(actions => actions.updateSize);
|
||||
const setNodesSelection = useStoreActions(actions => actions.setNodesSelection);
|
||||
|
||||
const shiftPressed = useKeyPress('Shift');
|
||||
const updateDimensions = () => {
|
||||
const size = getDimensions(rendererNode.current);
|
||||
dispatch(updateSize(size));
|
||||
updateSize(size);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,37 +44,33 @@ const GraphView = memo((props) => {
|
||||
useEffect(() => {
|
||||
if (state.d3Initialised) {
|
||||
props.onLoad({
|
||||
nodes: state.nodes,
|
||||
edges: state.edges,
|
||||
fitView: opts => dispatch(fitView(opts)),
|
||||
zoomIn: () => dispatch(zoomIn()),
|
||||
zoomOut: () => dispatch(zoomOut())
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
});
|
||||
}
|
||||
}, [state.d3Initialised]);
|
||||
|
||||
return (
|
||||
<div className="react-graph__renderer" ref={rendererNode}>
|
||||
<Provider onConnect={props.onConnect}>
|
||||
<NodeRenderer
|
||||
nodeTypes={props.nodeTypes}
|
||||
onElementClick={props.onElementClick}
|
||||
onNodeDragStop={props.onNodeDragStop}
|
||||
/>
|
||||
<EdgeRenderer
|
||||
width={state.width}
|
||||
height={state.height}
|
||||
edgeTypes={props.edgeTypes}
|
||||
onElementClick={props.onElementClick}
|
||||
connectionLineType={props.connectionLineType}
|
||||
connectionLineStyle={props.connectionLineStyle}
|
||||
/>
|
||||
</Provider>
|
||||
<NodeRenderer
|
||||
nodeTypes={props.nodeTypes}
|
||||
onElementClick={props.onElementClick}
|
||||
onNodeDragStop={props.onNodeDragStop}
|
||||
/>
|
||||
<EdgeRenderer
|
||||
width={state.width}
|
||||
height={state.height}
|
||||
edgeTypes={props.edgeTypes}
|
||||
onElementClick={props.onElementClick}
|
||||
connectionLineType={props.connectionLineType}
|
||||
connectionLineStyle={props.connectionLineStyle}
|
||||
/>
|
||||
{shiftPressed && <UserSelection />}
|
||||
{state.nodesSelectionActive && <NodesSelection />}
|
||||
<div
|
||||
className="react-graph__zoompane"
|
||||
onClick={() => dispatch(setNodesSelection({ isActive: false }))}
|
||||
onClick={() => setNodesSelection({ isActive: false })}
|
||||
ref={zoomPane}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { useStoreActions, useStoreState } from 'easy-peasy';
|
||||
|
||||
import BaseHandle from './BaseHandle';
|
||||
import NodeIdContext from '../NodeIdContext'
|
||||
import { ConnectionContext } from '../../ConnectionContext';
|
||||
|
||||
export default memo((props) => {
|
||||
const SourceHandle = memo((props) => {
|
||||
const nodeId = useContext(NodeIdContext);
|
||||
const { setPosition, setSourceId, onConnect } = useContext(ConnectionContext);
|
||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||
setPosition: a.setConnectionPosition,
|
||||
setSourceId: a.setConnectionSourceId
|
||||
}));
|
||||
const onConnect = useStoreState(s => s.onConnect);
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
@@ -19,3 +23,7 @@ export default memo((props) => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
SourceHandle.displayName = 'SourceHandle';
|
||||
|
||||
export default SourceHandle;
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import { useStoreActions, useStoreState } from 'easy-peasy';
|
||||
|
||||
import BaseHandle from './BaseHandle';
|
||||
import { ConnectionContext } from '../../ConnectionContext';
|
||||
import NodeIdContext from '../NodeIdContext'
|
||||
|
||||
const TargetHandle = memo((props) => {
|
||||
const nodeId = useContext(NodeIdContext);
|
||||
const { setPosition, setSourceId, onConnect } = useContext(ConnectionContext);
|
||||
const { setPosition, setSourceId } = useStoreActions(a => ({
|
||||
setPosition: a.setConnectionPosition,
|
||||
setSourceId: a.setConnectionSourceId
|
||||
}));
|
||||
const onConnect = useStoreState(s => s.onConnect);
|
||||
|
||||
return (
|
||||
<BaseHandle
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { useEffect, useRef, useState, memo } from 'react';
|
||||
import ReactDraggable from 'react-draggable';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { updateNodeData, updateNodePos, setSelectedElements } from '../../state/actions';
|
||||
import { getDimensions, isInputNode } from '../../utils';
|
||||
import { Provider } from '../NodeIdContext';
|
||||
import store from '../../store';
|
||||
|
||||
const isHandle = e => (
|
||||
e.target.className &&
|
||||
@@ -29,7 +29,7 @@ const getHandleBounds = (sel, nodeElement, parentBounds, k) => {
|
||||
};
|
||||
};
|
||||
|
||||
const onStart = (evt, { dispatch, setOffset, onClick, id, type, data, position, transform }) => {
|
||||
const onStart = (evt, { setOffset, onClick, id, type, data, position, transform }) => {
|
||||
if (isInputNode(evt) || isHandle(evt)) {
|
||||
return false;
|
||||
}
|
||||
@@ -42,22 +42,22 @@ const onStart = (evt, { dispatch, setOffset, onClick, id, type, data, position,
|
||||
const offsetY = scaledClient.y - position.y - transform[1];
|
||||
const node = { id, type, position, data };
|
||||
|
||||
dispatch(setSelectedElements({ id, type }));
|
||||
store.dispatch.setSelectedElements({ id, type });
|
||||
setOffset({ x: offsetX, y: offsetY });
|
||||
onClick(node);
|
||||
};
|
||||
|
||||
const onDrag = (evt, { dispatch, setDragging, id, offset, transform }) => {
|
||||
const onDrag = (evt, { setDragging, id, offset, transform }) => {
|
||||
const scaledClient = {
|
||||
x: evt.clientX * (1 / transform[2]),
|
||||
y: evt.clientY * (1 / transform[2])
|
||||
};
|
||||
|
||||
setDragging(true);
|
||||
dispatch(updateNodePos(id, {
|
||||
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 }) => {
|
||||
@@ -78,7 +78,7 @@ export default NodeComponent => {
|
||||
const [isDragging, setDragging] = useState(false);
|
||||
const {
|
||||
id, type, data, transform, xPos, yPos, selected,
|
||||
dispatch, onClick, onNodeDragStop, style
|
||||
onClick, onNodeDragStop, style
|
||||
} = props;
|
||||
|
||||
const position = { x: xPos, y: yPos };
|
||||
@@ -93,13 +93,13 @@ export default NodeComponent => {
|
||||
target: getHandleBounds('.target', nodeElement.current, bounds, transform[2])
|
||||
};
|
||||
|
||||
dispatch(updateNodeData(id, { ...dimensions, handleBounds }));
|
||||
store.dispatch.updateNodeData({ id, ...dimensions, handleBounds });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ReactDraggable.DraggableCore
|
||||
onStart={evt => onStart(evt, { dispatch, onClick, id, type, data, setOffset, transform, position })}
|
||||
onDrag={evt => onDrag(evt, { dispatch, setDragging, id, offset, transform })}
|
||||
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]}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { isNode } from '../graph-utils';
|
||||
|
||||
function renderNode(d, props, graphContext) {
|
||||
function renderNode(d, props, state) {
|
||||
const nodeType = d.type || 'default';
|
||||
|
||||
if (!props.nodeTypes[nodeType]) {
|
||||
@@ -11,7 +11,7 @@ function renderNode(d, props, graphContext) {
|
||||
}
|
||||
|
||||
const NodeComponent = props.nodeTypes[nodeType] || props.nodeTypes.default;
|
||||
const selected = graphContext.state.selectedElements
|
||||
const selected = state.selectedElements
|
||||
.filter(isNode)
|
||||
.map(e => e.id)
|
||||
.includes(d.id);
|
||||
@@ -26,9 +26,7 @@ function renderNode(d, props, graphContext) {
|
||||
yPos={d.__rg.position.y}
|
||||
onClick={props.onElementClick}
|
||||
onNodeDragStop={props.onNodeDragStop}
|
||||
dispatch={graphContext.dispatch}
|
||||
transform={graphContext.state.transform}
|
||||
getNodeById={graphContext.getNodeById}
|
||||
transform={state.transform}
|
||||
selected={selected}
|
||||
style={d.style}
|
||||
/>
|
||||
@@ -36,8 +34,13 @@ function renderNode(d, props, graphContext) {
|
||||
}
|
||||
|
||||
const NodeRenderer = memo((props) => {
|
||||
const graphContext = useContext(GraphContext);
|
||||
const { transform, nodes } = graphContext.state;
|
||||
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 (
|
||||
@@ -45,7 +48,7 @@ const NodeRenderer = memo((props) => {
|
||||
className="react-graph__nodes"
|
||||
style={transformStyle}
|
||||
>
|
||||
{nodes.map(d => renderNode(d, props, graphContext))}
|
||||
{nodes.map(d => renderNode(d, props, state))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import React, { useContext, useState, memo } from 'react';
|
||||
import React, { useState, memo } from 'react';
|
||||
import ReactDraggable from 'react-draggable';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { isNode } from '../graph-utils';
|
||||
import { updateNodePos } from '../state/actions';
|
||||
|
||||
function getStartPositions(elements) {
|
||||
return elements
|
||||
@@ -21,10 +20,14 @@ function getStartPositions(elements) {
|
||||
}
|
||||
|
||||
export default memo(() => {
|
||||
const graphContext = useContext(GraphContext);
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 });
|
||||
const [startPositions, setStartPositions] = useState({});
|
||||
const { state, dispatch } = graphContext;
|
||||
const state = useStoreState(s => ({
|
||||
transform: s.transform,
|
||||
selectedNodesBbox: s.selectedNodesBbox,
|
||||
selectedElements: s.selectedElements
|
||||
}));
|
||||
const updateNodePos = useStoreActions(a => a.updateNodePos);
|
||||
const [x, y, k] = state.transform;
|
||||
const position = state.selectedNodesBbox;
|
||||
|
||||
@@ -48,10 +51,10 @@ export default memo(() => {
|
||||
};
|
||||
|
||||
state.selectedElements.filter(isNode).forEach(node => {
|
||||
dispatch(updateNodePos(node.id, {
|
||||
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
|
||||
}));
|
||||
}});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -59,7 +62,7 @@ export default memo(() => {
|
||||
<div
|
||||
className="react-graph__nodesselection"
|
||||
style={{
|
||||
transform: `translate(${state.transform[0]}px,${state.transform[1]}px) scale(${state.transform[2]})`
|
||||
transform: `translate(${x}px,${y}px) scale(${k})`
|
||||
}}
|
||||
>
|
||||
<ReactDraggable
|
||||
@@ -79,4 +82,4 @@ export default memo(() => {
|
||||
</ReactDraggable>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import React, { useRef, useContext, useEffect } from 'react';
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import { useStoreState } from 'easy-peasy';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { isFunction } from '../../utils'
|
||||
import { getNodesInside } from '../../graph-utils';
|
||||
import { GraphContext } from '../../GraphContext';
|
||||
|
||||
const baseStyle = {
|
||||
position: 'absolute',
|
||||
zIndex: 5,
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
width: 200,
|
||||
width: 200
|
||||
};
|
||||
|
||||
export default ({ style = {}, className, bgColor = '#f8f8f8', nodeColor = '#ddd' }) => {
|
||||
const canvasNode = useRef(null);
|
||||
const { state } = useContext(GraphContext);
|
||||
const mapClasses = classnames('react-graph__minimap', className);
|
||||
const state = useStoreState(s => ({
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
nodes: s.nodes,
|
||||
transform: s.transform,
|
||||
})); const mapClasses = classnames('react-graph__minimap', className);
|
||||
const nodePositions = state.nodes.map(n => n.__rg.position);
|
||||
const width = style.width || baseStyle.width;
|
||||
const height = (state.height / (state.width || 1)) * width;
|
||||
|
||||
59
src/ReactGraph/ElementUpdater.js
Normal file
59
src/ReactGraph/ElementUpdater.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useEffect, memo } from 'react';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
import { parseElement, isNode, isEdge } from '../graph-utils';
|
||||
|
||||
const ElementUpdater = (props) => {
|
||||
const state = useStoreState(s => ({
|
||||
nodes: s.nodes,
|
||||
edges: s.edges,
|
||||
transform: s.transform
|
||||
}));
|
||||
|
||||
const setNodes = useStoreActions(a => a.setNodes);
|
||||
const setEdges = useStoreActions(a => a.setEdges);
|
||||
const setOnConnect = useStoreActions(a => a.setOnConnect);
|
||||
|
||||
useEffect(() => {
|
||||
const nodes = props.elements.filter(isNode);
|
||||
const edges = props.elements.filter(isEdge).map(parseElement);
|
||||
|
||||
const nextNodes = nodes.map(propNode => {
|
||||
const existingNode = state.nodes.find(n => n.id === propNode.id);
|
||||
|
||||
if (existingNode) {
|
||||
const data = !isEqual(existingNode.data, propNode.data) ?
|
||||
{ ...existingNode.data, ...propNode.data } : existingNode.data;
|
||||
|
||||
return {
|
||||
...existingNode,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
return parseElement(propNode, state.transform);
|
||||
});
|
||||
|
||||
const nodesChanged = !isEqual(state.nodes, nextNodes);
|
||||
const edgesChanged = !isEqual(state.edges, edges);
|
||||
|
||||
if (nodesChanged) {
|
||||
setNodes(nextNodes);
|
||||
}
|
||||
|
||||
if (edgesChanged) {
|
||||
setEdges(edges);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setOnConnect(props.onConnect);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
ElementUpdater.displayName = 'ElementUpdater';
|
||||
|
||||
export default memo(ElementUpdater);
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { StoreProvider } from 'easy-peasy';
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const whyDidYouRender = require('@welldone-software/why-did-you-render');
|
||||
@@ -7,7 +9,6 @@ if (process.env.NODE_ENV !== 'production') {
|
||||
|
||||
import GraphView from '../GraphView';
|
||||
import GlobalKeyHandler from '../GlobalKeyHandler';
|
||||
import { Provider } from '../GraphContext';
|
||||
|
||||
import DefaultNode from '../NodeRenderer/NodeTypes/DefaultNode';
|
||||
import InputNode from '../NodeRenderer/NodeTypes/InputNode';
|
||||
@@ -18,6 +19,8 @@ import BezierEdge from '../EdgeRenderer/EdgeTypes/BezierEdge';
|
||||
import StraightEdge from '../EdgeRenderer/EdgeTypes/StraightEdge';
|
||||
import StepEdge from '../EdgeRenderer/EdgeTypes/StepEdge';
|
||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import store from '../store';
|
||||
import ElementUpdater from './ElementUpdater';
|
||||
|
||||
import '../style.css';
|
||||
|
||||
@@ -31,7 +34,8 @@ const ReactGraph = ({
|
||||
|
||||
return (
|
||||
<div style={style} className="react-graph">
|
||||
<Provider elements={elements}>
|
||||
<StoreProvider store={store}>
|
||||
<ElementUpdater elements={elements} onConnect={onConnect} />
|
||||
<GraphView
|
||||
onLoad={onLoad}
|
||||
onMove={onMove}
|
||||
@@ -41,13 +45,12 @@ const ReactGraph = ({
|
||||
edgeTypes={edgeTypesParsed}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
onConnect={onConnect}
|
||||
/>
|
||||
<GlobalKeyHandler
|
||||
onElementsRemove={onElementsRemove}
|
||||
/>
|
||||
{children}
|
||||
</Provider>
|
||||
</StoreProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import React, { useEffect, useRef, useState, useContext, memo } from 'react';
|
||||
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { updateSelection, setSelection, setNodesSelection } from '../state/actions';
|
||||
import React, { useEffect, useRef, useState, memo } from 'react';
|
||||
import { useStoreActions } from 'easy-peasy';
|
||||
|
||||
const initialRect = {
|
||||
startX: 0,
|
||||
@@ -25,7 +23,9 @@ function getMousePosition(evt) {
|
||||
export default memo(() => {
|
||||
const selectionPane = useRef(null);
|
||||
const [rect, setRect] = useState(initialRect);
|
||||
const { dispatch } = useContext(GraphContext);
|
||||
const setSelection = useStoreActions(a => a.setSelection);
|
||||
const updateSelection = useStoreActions(a => a.updateSelection);
|
||||
const setNodesSelection = useStoreActions(a => a.setNodesSelection);
|
||||
|
||||
useEffect(() => {
|
||||
function onMouseDown(evt) {
|
||||
@@ -40,7 +40,7 @@ export default memo(() => {
|
||||
draw: true
|
||||
}));
|
||||
|
||||
dispatch(setSelection(true));
|
||||
setSelection(true);
|
||||
}
|
||||
|
||||
function onMouseMove(evt) {
|
||||
@@ -60,7 +60,7 @@ export default memo(() => {
|
||||
height: negativeY ? currentRect.startY - mousePos.y : mousePos.y - currentRect.startY,
|
||||
};
|
||||
|
||||
dispatch(updateSelection(nextRect));
|
||||
updateSelection(nextRect);
|
||||
|
||||
return nextRect;
|
||||
});
|
||||
@@ -68,8 +68,8 @@ export default memo(() => {
|
||||
|
||||
function onMouseUp() {
|
||||
setRect((currentRect) => {
|
||||
dispatch(setNodesSelection({ isActive: true, selection: currentRect }));
|
||||
dispatch(setSelection(false));
|
||||
setNodesSelection({ isActive: true, selection: currentRect });
|
||||
setSelection(false);
|
||||
|
||||
return {
|
||||
...currentRect,
|
||||
|
||||
102
src/actions.js
Normal file
102
src/actions.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import { action } from 'easy-peasy';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
import { getBoundingBox, getNodesInside, getConnectedEdges } from './graph-utils';
|
||||
|
||||
export default {
|
||||
setOnConnect: action((state, onConnect) => {
|
||||
state.onConnect = onConnect;
|
||||
}),
|
||||
|
||||
setNodes: action((state, nodes) => {
|
||||
state.nodes = nodes;
|
||||
}),
|
||||
|
||||
setEdges: action((state, edges) => {
|
||||
state.edges = edges;
|
||||
}),
|
||||
|
||||
updateNodeData: action((state, { id, ...data }) => {
|
||||
state.nodes.forEach((n) => {
|
||||
if (n.id === id) {
|
||||
n.__rg = {
|
||||
...n.__rg,
|
||||
...data
|
||||
};
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
updateNodePos: action((state, { id, pos }) => {
|
||||
state.nodes.forEach((n) => {
|
||||
if (n.id === id) {
|
||||
n.__rg = {
|
||||
...n.__rg,
|
||||
position: pos
|
||||
};
|
||||
}
|
||||
});
|
||||
}),
|
||||
|
||||
setSelection: action((state, isActive) => {
|
||||
state.selectionActive = isActive;
|
||||
}),
|
||||
|
||||
setNodesSelection: action((state, { isActive, selection }) => {
|
||||
if (!isActive) {
|
||||
state.nodesSelectionActive = false;
|
||||
state.selectedElements = [];
|
||||
|
||||
return;
|
||||
}
|
||||
const selectedNodes = getNodesInside(state.nodes, selection, state.transform);
|
||||
const selectedNodesBbox = getBoundingBox(selectedNodes);
|
||||
|
||||
state.selection = selection;
|
||||
state.nodesSelectionActive = true;
|
||||
state.selectedNodesBbox = selectedNodesBbox;
|
||||
state.nodesSelectionActive = true;
|
||||
}),
|
||||
|
||||
setSelectedElements: action((state, elements) => {
|
||||
const selectedElementsArr = Array.isArray(elements) ? elements : [elements];
|
||||
const selectedElementsUpdated = !isEqual(selectedElementsArr, state.selectedElements);
|
||||
const selectedElements = selectedElementsUpdated ? selectedElementsArr : state.selectedElements;
|
||||
|
||||
state.selectedElements = selectedElements;
|
||||
}),
|
||||
|
||||
updateSelection: action((state, selection) => {
|
||||
const selectedNodes = getNodesInside(state.nodes, selection, state.transform);
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
||||
|
||||
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
||||
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
||||
|
||||
state.selection = selection;
|
||||
state.selectedElements = selectedElementsUpdated ? nextSelectedElements: state.selectedElements
|
||||
}),
|
||||
|
||||
updateTransform: action((state, transform) => {
|
||||
state.transform = [transform.x, transform.y, transform.k];
|
||||
}),
|
||||
|
||||
updateSize: action((state, size) => {
|
||||
state.width = size.width;
|
||||
state.height = size.height;
|
||||
}),
|
||||
|
||||
initD3: action((state, { zoom, selection }) => {
|
||||
state.d3Zoom = zoom;
|
||||
state.d3Selection = selection;
|
||||
state.d3Initialised = true;
|
||||
}),
|
||||
|
||||
setConnectionPosition: action((state, position) => {
|
||||
state.connectionPosition = position;
|
||||
}),
|
||||
|
||||
setConnectionSourceId: action((state, sourceId) => {
|
||||
state.connectionSourceId = sourceId;
|
||||
})
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
import store from './store';
|
||||
|
||||
export const isEdge = element => element.source && element.target;
|
||||
|
||||
export const isNode = element => !element.source && !element.target;
|
||||
@@ -142,11 +145,37 @@ export const getConnectedEdges = (nodes, edges) => {
|
||||
return edges.filter(e => nodeIds.includes(e.source) || nodeIds.includes(e.target))
|
||||
};
|
||||
|
||||
export const fitView = ({ padding = 0 } = {}) => {
|
||||
const state = store.getState();
|
||||
const bounds = getBoundingBox(state.nodes);
|
||||
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
||||
const k = Math.min(state.width, state.height) / (maxBoundsSize + (maxBoundsSize * padding));
|
||||
const boundsCenterX = bounds.x + (bounds.width / 2);
|
||||
const boundsCenterY = bounds.y + (bounds.height / 2);
|
||||
const transform = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)];
|
||||
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
|
||||
|
||||
state.d3Selection.call(state.d3Zoom.transform, fittedTransform);
|
||||
};
|
||||
|
||||
export const zoomIn = () => {
|
||||
const state = store.getState();
|
||||
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] + 0.2);
|
||||
};
|
||||
|
||||
export const zoomOut = () => {
|
||||
const state = store.getState();
|
||||
state.d3Zoom.scaleTo(state.d3Selection, state.transform[2] - 0.2);
|
||||
};
|
||||
|
||||
export default {
|
||||
isEdge,
|
||||
separateElements,
|
||||
getBoundingBox,
|
||||
graphPosToZoomedPos,
|
||||
getConnectedEdges,
|
||||
parseElement
|
||||
parseElement,
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut
|
||||
};
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import * as d3Zoom from 'd3-zoom';
|
||||
import { select, event } from 'd3-selection';
|
||||
|
||||
import { updateTransform, initD3 } from '../state/view-actions';
|
||||
import { GraphContext } from '../GraphContext';
|
||||
import { useStoreState, useStoreActions } from 'easy-peasy';
|
||||
|
||||
const d3ZoomInstance = d3Zoom.zoom().scaleExtent([0.5, 2]);
|
||||
|
||||
export default function useD3Zoom(zoomPane, onMove, shiftPressed) {
|
||||
const { state, dispatch } = useContext(GraphContext);
|
||||
const state = useStoreState(s => ({
|
||||
transform: s.transform,
|
||||
d3Selection: s.d3Selection,
|
||||
d3Zoom: s.d3Zoom,
|
||||
edges: s.edged,
|
||||
d3Initialised: s.d3Initialised,
|
||||
nodesSelectionActive: s.nodesSelectionActive
|
||||
}));
|
||||
|
||||
const initD3 = useStoreActions(actions => actions.initD3);
|
||||
const updateTransform = useStoreActions(actions => actions.updateTransform);
|
||||
|
||||
useEffect(() => {
|
||||
const selection = select(zoomPane.current).call(d3ZoomInstance);
|
||||
dispatch(initD3({ zoom: d3ZoomInstance, selection }));
|
||||
initD3({ zoom: d3ZoomInstance, selection });
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,7 +32,7 @@ export default function useD3Zoom(zoomPane, onMove, shiftPressed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch(updateTransform(event.transform));
|
||||
updateTransform(event.transform);
|
||||
|
||||
onMove();
|
||||
});
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import {
|
||||
SET_NODES, SET_EDGES, UPDATE_NODE_DATA, UPDATE_NODE_POS,
|
||||
UPDATE_SELECTION, SET_SELECTION, SET_NODES_SELECTION,
|
||||
SET_SELECTED_ELEMENTS, REMOVE_NODES,
|
||||
} from './index';
|
||||
|
||||
export const setNodes = nodes => ({
|
||||
type: SET_NODES,
|
||||
payload: { nodes }
|
||||
});
|
||||
|
||||
export const setEdges = edges => ({
|
||||
type: SET_EDGES,
|
||||
payload: { edges }
|
||||
});
|
||||
|
||||
export const updateNodeData = (id, data) => ({
|
||||
type: UPDATE_NODE_DATA,
|
||||
payload: { id, data }
|
||||
});
|
||||
|
||||
export const updateNodePos = (id, pos) => ({
|
||||
type: UPDATE_NODE_POS,
|
||||
payload: { id, pos }
|
||||
});
|
||||
|
||||
export const setSelection = isActive => ({
|
||||
type: SET_SELECTION,
|
||||
payload: { selectionActive: isActive }
|
||||
});
|
||||
|
||||
export const setNodesSelection = ({ isActive, selection }) => ({
|
||||
type: SET_NODES_SELECTION,
|
||||
payload: { nodesSelectionActive: isActive, selection }
|
||||
});
|
||||
|
||||
export const setSelectedElements = elements => ({
|
||||
type: SET_SELECTED_ELEMENTS,
|
||||
payload: {
|
||||
selectedElements: Array.isArray(elements) ? elements : [elements]
|
||||
}
|
||||
});
|
||||
|
||||
export const removeNodes = ids => ({
|
||||
type: REMOVE_NODES,
|
||||
payload: { ids: Array.isArray(ids) ? ids : [ids] }
|
||||
});
|
||||
|
||||
export const updateSelection = selection => ({
|
||||
type: UPDATE_SELECTION,
|
||||
payload: { selection }
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import { zoomIdentity } from 'd3-zoom';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
|
||||
import { getBoundingBox, getNodesInside, getConnectedEdges } from '../graph-utils';
|
||||
|
||||
export const SET_EDGES = 'SET_EDGES';
|
||||
export const SET_NODES = 'SET_NODES';
|
||||
export const UPDATE_NODE_DATA = 'UPDATE_NODE_DATA';
|
||||
export const UPDATE_NODE_POS = 'UPDATE_NODE_POS';
|
||||
export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
|
||||
export const UPDATE_SIZE = 'UPDATE_SIZE';
|
||||
export const INIT_D3 = 'INIT_D3';
|
||||
export const FIT_VIEW = 'FIT_VIEW';
|
||||
export const ZOOM_IN = 'ZOOM_IN';
|
||||
export const ZOOM_OUT = 'ZOOM_OUT';
|
||||
export const UPDATE_SELECTION = 'UPDATE_SELECTION';
|
||||
export const SET_SELECTION = 'SET_SELECTION';
|
||||
export const SET_NODES_SELECTION = 'SET_NODES_SELECTION';
|
||||
export const SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS';
|
||||
export const REMOVE_NODES = 'REMOVE_NODES';
|
||||
|
||||
export const initialState = {
|
||||
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: {}
|
||||
};
|
||||
|
||||
export const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_NODE_DATA: {
|
||||
return {
|
||||
...state,
|
||||
nodes: state.nodes.map((n) => {
|
||||
if (n.id === action.payload.id) {
|
||||
n.__rg = {
|
||||
...n.__rg,
|
||||
...action.payload.data
|
||||
};
|
||||
}
|
||||
return n;
|
||||
})
|
||||
};
|
||||
}
|
||||
case UPDATE_NODE_POS: {
|
||||
return {
|
||||
...state,
|
||||
nodes: state.nodes.map((n) => {
|
||||
if (n.id === action.payload.id) {
|
||||
n.__rg = {
|
||||
...n.__rg,
|
||||
position: action.payload.pos
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
})
|
||||
};
|
||||
}
|
||||
case FIT_VIEW: {
|
||||
const bounds = getBoundingBox(state.nodes);
|
||||
const maxBoundsSize = Math.max(bounds.width, bounds.height);
|
||||
const k = Math.min(state.width, state.height) / (maxBoundsSize + (maxBoundsSize * action.payload.padding));
|
||||
const boundsCenterX = bounds.x + (bounds.width / 2);
|
||||
const boundsCenterY = bounds.y + (bounds.height / 2);
|
||||
const transform = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)];
|
||||
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
|
||||
|
||||
state.d3Selection.call(state.d3Zoom.transform, fittedTransform);
|
||||
|
||||
return state;
|
||||
}
|
||||
case ZOOM_IN: {
|
||||
const { transform } = state;
|
||||
state.d3Zoom.scaleTo(state.d3Selection, transform[2] + 0.2);
|
||||
|
||||
return state;
|
||||
}
|
||||
case ZOOM_OUT: {
|
||||
const { transform } = state;
|
||||
state.d3Zoom.scaleTo(state.d3Selection, transform[2] - 0.2);
|
||||
|
||||
return state;
|
||||
}
|
||||
case UPDATE_SELECTION: {
|
||||
const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform);
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, state.edges);
|
||||
|
||||
const nextSelectedElements = [...selectedNodes, ...selectedEdges];
|
||||
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
|
||||
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
selectedElements: selectedElementsUpdated ? nextSelectedElements: state.selectedElements
|
||||
};
|
||||
}
|
||||
case SET_NODES_SELECTION: {
|
||||
if (!action.payload.nodesSelectionActive) {
|
||||
return { ...state, nodesSelectionActive: false, selectedElements: [] };
|
||||
}
|
||||
const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform);
|
||||
const selectedNodesBbox = getBoundingBox(selectedNodes);
|
||||
|
||||
return { ...state, ...action.payload, selectedNodesBbox };
|
||||
}
|
||||
case SET_SELECTED_ELEMENTS: {
|
||||
const selectedElementsUpdated = !isEqual(action.payload.selectedElements, state.selectedElements);
|
||||
const selectedElements = selectedElementsUpdated ? action.payload.selectedElements : state.selectedElements;
|
||||
|
||||
return { ...state, selectedElements, odesSelectionActive: false };
|
||||
}
|
||||
// unused
|
||||
case REMOVE_NODES: {
|
||||
const { ids } = action.payload;
|
||||
const nextEdges = state.edges.filter(e => !ids.includes(e.target) && !ids.includes(e.source));
|
||||
const nextNodes = state.nodes.filter(n => !ids.includes(n.id));
|
||||
|
||||
return { ...state, nodes: nextNodes, edges: nextEdges };
|
||||
}
|
||||
case SET_NODES:
|
||||
case SET_EDGES:
|
||||
case UPDATE_TRANSFORM:
|
||||
case INIT_D3:
|
||||
case UPDATE_SIZE:
|
||||
case SET_SELECTION:
|
||||
return { ...state, ...action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import {
|
||||
UPDATE_TRANSFORM, UPDATE_SIZE, INIT_D3,
|
||||
FIT_VIEW, ZOOM_IN, ZOOM_OUT
|
||||
} from './index';
|
||||
|
||||
export const updateTransform = transform => ({
|
||||
type: UPDATE_TRANSFORM,
|
||||
payload: { transform: [transform.x, transform.y, transform.k] }
|
||||
});
|
||||
|
||||
export const updateSize = size => ({
|
||||
type: UPDATE_SIZE,
|
||||
payload: size
|
||||
});
|
||||
|
||||
export const initD3 = ({ zoom, selection }) => ({
|
||||
type: INIT_D3,
|
||||
payload: { d3Zoom: zoom, d3Selection: selection, d3Initialised: true }
|
||||
});
|
||||
|
||||
export const fitView = ({ padding = 0 } = {}) => ({
|
||||
type: FIT_VIEW,
|
||||
payload: { padding }
|
||||
});
|
||||
|
||||
export const zoomIn = () => ({
|
||||
type: ZOOM_IN
|
||||
});
|
||||
|
||||
export const zoomOut = () => ({
|
||||
type: ZOOM_OUT
|
||||
});
|
||||
30
src/store.js
Normal file
30
src/store.js
Normal file
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user