feat(nodes): add selection

This commit is contained in:
moklick
2019-07-16 18:55:12 +02:00
parent a66f707f11
commit 0e179ffe1f
10 changed files with 290 additions and 61 deletions
+152 -24
View File
@@ -228,6 +228,18 @@
height: bbox.maxY - bbox.minY height: bbox.maxY - bbox.minY
}; };
}; };
var getNodesInside = function getNodesInside(nodes, bbox) {
var transform = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : [0, 0, 1];
return nodes.filter(function (n) {
var bboxPos = {
x: (bbox.x - transform[0]) * (1 / transform[2]),
y: (bbox.y - transform[1]) * (1 / transform[2])
};
var bboxWidth = bbox.width * (1 / transform[2]);
var bboxHeight = bbox.height * (1 / transform[2]);
return n.position.x > bboxPos.x && n.position.x + n.data.__width < bboxPos.x + bboxWidth && n.position.y > bboxPos.y && n.position.y + n.data.__height < bboxPos.y + bboxHeight;
});
};
var noop = {value: function() {}}; var noop = {value: function() {}};
@@ -29767,15 +29779,20 @@
var UPDATE_SIZE = 'UPDATE_SIZE'; var UPDATE_SIZE = 'UPDATE_SIZE';
var INIT_D3 = 'INIT_D3'; var INIT_D3 = 'INIT_D3';
var FIT_VIEW = 'FIT_VIEW'; var FIT_VIEW = 'FIT_VIEW';
var UPDATE_SELECTION = 'UPDATE_SELECTION';
var SET_SELECTION = 'SET_SELECTION';
var initialState = { var initialState = {
width: 0, width: 0,
height: 0, height: 0,
transform: [0, 0, 1], transform: [0, 0, 1],
nodes: [], nodes: [],
edges: [], edges: [],
selectedNodes: [],
d3Zoom: null, d3Zoom: null,
d3Selection: null, d3Selection: null,
d3Initialised: false d3Initialised: false,
selectionActive: false,
selection: {}
}; };
var reducer = function reducer(state, action) { var reducer = function reducer(state, action) {
switch (action.type) { switch (action.type) {
@@ -29817,11 +29834,22 @@
return state; return state;
} }
case UPDATE_SELECTION:
{
var selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform).map(function (n) {
return n.data.id;
});
return _objectSpread2({}, state, {}, action.payload, {
selectedNodes: selectedNodes
});
}
case SET_NODES: case SET_NODES:
case SET_EDGES: case SET_EDGES:
case UPDATE_TRANSFORM: case UPDATE_TRANSFORM:
case INIT_D3: case INIT_D3:
case UPDATE_SIZE: case UPDATE_SIZE:
case SET_SELECTION:
return _objectSpread2({}, state, {}, action.payload); return _objectSpread2({}, state, {}, action.payload);
default: default:
@@ -29894,6 +29922,22 @@
type: FIT_VIEW type: FIT_VIEW
}; };
}; };
var setSelection = function setSelection(isActive) {
return {
type: SET_SELECTION,
payload: {
selectionActive: isActive
}
};
};
var updateSelection = function updateSelection(selection) {
return {
type: UPDATE_SELECTION,
payload: {
selection: selection
}
};
};
var GraphContext = React.createContext({}); var GraphContext = React.createContext({});
var Provider = function Provider(props) { var Provider = function Provider(props) {
@@ -30075,55 +30119,73 @@
return EdgeRenderer; return EdgeRenderer;
}(React.PureComponent); }(React.PureComponent);
var initialRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false
};
var Selection$2 = (function () { var Selection$2 = (function () {
var selectionPane = React.useRef(null); var selectionPane = React.useRef(null);
var _useState = React.useState({ var _useState = React.useState(initialRect),
x: 0,
y: 0,
width: 0,
height: 0,
draw: false
}),
_useState2 = _slicedToArray(_useState, 2), _useState2 = _slicedToArray(_useState, 2),
rect = _useState2[0], rect = _useState2[0],
setRect = _useState2[1]; setRect = _useState2[1];
var _useContext = React.useContext(GraphContext),
dispatch = _useContext.dispatch;
React.useEffect(function () { React.useEffect(function () {
function onMouseDown(evt) { function onMouseDown(evt) {
setRect(function (r) { setRect(function (r) {
return _objectSpread2({}, r, { return _objectSpread2({}, r, {
startX: evt.clientX,
startY: evt.clientY,
x: evt.clientX, x: evt.clientX,
y: evt.clientY, y: evt.clientY,
draw: true draw: true
}); });
}); });
dispatch(setSelection(true));
} }
function onMouseMove(evt) { function onMouseMove(evt) {
setRect(function (r) { setRect(function (r) {
return _objectSpread2({}, r, { var negativeX = evt.clientX < r.startX;
width: evt.clientX, var negativeY = evt.clientY < r.startY;
height: evt.clientY
if (!r.draw) {
return r;
}
var nextRect = _objectSpread2({}, r, {
x: negativeX ? evt.clientX : r.x,
y: negativeY ? evt.clientY : r.y,
width: negativeX ? r.startX - evt.clientX : evt.clientX - r.startX,
height: negativeY ? r.startY - evt.clientY : evt.clientY - r.startY
}); });
dispatch(updateSelection(nextRect));
return nextRect;
}); });
} }
function onMouseUp() { function onMouseUp() {
console.log('selection mouse up'); setRect(initialRect);
} dispatch(setSelection(false));
function removeAll() {
selectionPane.current.removeEventListener('mousedown', onMouseDown);
selectionPane.current.removeEventListener('mousemove', onMouseMove);
selectionPane.current.removeEventListener('mouseup', onMouseUp);
} }
selectionPane.current.addEventListener('mousedown', onMouseDown); selectionPane.current.addEventListener('mousedown', onMouseDown);
selectionPane.current.addEventListener('mousemove', onMouseMove); selectionPane.current.addEventListener('mousemove', onMouseMove);
selectionPane.current.addEventListener('mouseup', onMouseUp); selectionPane.current.addEventListener('mouseup', onMouseUp);
return function () { return function () {
removeAll(); selectionPane.current.removeEventListener('mousedown', onMouseDown);
selectionPane.current.removeEventListener('mousemove', onMouseMove);
selectionPane.current.removeEventListener('mouseup', onMouseUp);
}; };
}, []); }, []);
return React__default.createElement("div", { return React__default.createElement("div", {
@@ -30280,7 +30342,8 @@
var data = _ref.data, var data = _ref.data,
style = _ref.style; style = _ref.style;
return React__default.createElement("div", { return React__default.createElement("div", {
style: _objectSpread2({}, nodeStyles$1, {}, style) style: _objectSpread2({}, nodeStyles$1, {}, style),
className: "react-graph__node-inner"
}, data.label, React__default.createElement(Handle, { }, data.label, React__default.createElement(Handle, {
style: { style: {
bottom: 0, bottom: 0,
@@ -32519,6 +32582,59 @@
}); });
var classnames = createCommonjsModule(function (module) {
/*!
Copyright (c) 2017 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/* global define */
(function () {
var hasOwn = {}.hasOwnProperty;
function classNames () {
var classes = [];
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
if (!arg) continue;
var argType = typeof arg;
if (argType === 'string' || argType === 'number') {
classes.push(arg);
} else if (Array.isArray(arg) && arg.length) {
var inner = classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
} else if (argType === 'object') {
for (var key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
return classes.join(' ');
}
if ( module.exports) {
classNames.default = classNames;
module.exports = classNames;
} else {
window.classNames = classNames;
}
}());
});
var isInputTarget = function isInputTarget(e) {
return ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
};
var wrapNode = (function (NodeComponent) { var wrapNode = (function (NodeComponent) {
return function (props) { return function (props) {
var position = props.position, var position = props.position,
@@ -32533,6 +32649,10 @@
y = _graphContext$state$t[1], y = _graphContext$state$t[1],
k = _graphContext$state$t[2]; k = _graphContext$state$t[2];
var selected = graphContext.state.selectedNodes.includes(id);
var nodeClasses = classnames('react-graph__node', {
selected: selected
});
React.useEffect(function () { React.useEffect(function () {
var bounds = nodeElement.current.getBoundingClientRect(); var bounds = nodeElement.current.getBoundingClientRect();
var unscaledWith = Math.round(bounds.width * (1 / k)); var unscaledWith = Math.round(bounds.width * (1 / k));
@@ -32545,6 +32665,10 @@
return React__default.createElement(reactDraggable.DraggableCore, { return React__default.createElement(reactDraggable.DraggableCore, {
grid: [1, 1], grid: [1, 1],
onStart: function onStart(e) { onStart: function onStart(e) {
if (isInputTarget(e)) {
return false;
}
var unscaledPos = { var unscaledPos = {
x: e.clientX * (1 / k), x: e.clientX * (1 / k),
y: e.clientY * (1 / k) y: e.clientY * (1 / k)
@@ -32573,13 +32697,17 @@
}, },
scale: k scale: k
}, React__default.createElement("div", { }, React__default.createElement("div", {
className: "react-graph__nodewrap", className: nodeClasses,
ref: nodeElement, ref: nodeElement,
style: { style: {
transform: "translate(".concat(position.x, "px,").concat(position.y, "px)") transform: "translate(".concat(position.x, "px,").concat(position.y, "px)")
}, },
onClick: function onClick() { onClick: function onClick(e) {
return onNodeClick({ if (isInputTarget(e)) {
return false;
}
onNodeClick({
data: data, data: data,
position: position position: position
}); });
@@ -32630,7 +32758,7 @@
} }
} }
var css = ".react-graph {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.react-graph__renderer {\n width: 100%;\n height: 100%;\n position: absolute;\n}\n\n.react-graph__zoompane {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2;\n}\n\n.react-graph__selectionpane {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 3;\n}\n\n.react-graph__selection {\n position: absolute;\n top: 0;\n left: 0;\n background: rgba(0,0,0,0.1);\n border: 1px solid #222;\n}\n\n.react-graph__edges {\n pointer-events: none;\n}\n\n.react-graph__edge {\n fill: none;\n stroke: #333;\n stroke-width: 2;\n}\n\n.react-graph__nodes {\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: 2;\n pointer-events: none;\n transform-origin: 0 0;\n}\n\n.react-graph__nodewrap {\n position: absolute;\n width: 150px;\n color: #222;\n font-family: sans-serif;\n font-size: 12px;\n text-align: center;\n cursor: -webkit-grab;\n cursor: grab;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n pointer-events: all;\n transform-origin: 0 0;\n}\n\n.react-graph__nodewrap:hover > * {\n box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);\n}\n\n.react-graph__handle {\n position: absolute;\n width: 12px;\n height: 12px;\n transform: translate(-50%, -50%);\n background: #222;\n left: 50%;\n border-radius: 50%;\n}"; var css = ".react-graph {\n width: 100%;\n height: 100%;\n position: relative;\n overflow: hidden;\n}\n\n.react-graph__renderer {\n width: 100%;\n height: 100%;\n position: absolute;\n}\n\n.react-graph__zoompane {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1;\n}\n\n.react-graph__selectionpane {\n width: 100%;\n height: 100%;\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2;\n}\n\n.react-graph__selection {\n position: absolute;\n top: 0;\n left: 0;\n background: rgba(0, 89, 220, 0.08);\n border: 1px dotted rgba(0, 89, 220, 0.8);\n}\n\n.react-graph__edges {\n pointer-events: none;\n}\n\n.react-graph__edge {\n fill: none;\n stroke: #333;\n stroke-width: 2;\n}\n\n.react-graph__nodes {\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: 2;\n pointer-events: none;\n transform-origin: 0 0;\n}\n\n.react-graph__node {\n position: absolute;\n width: 150px;\n color: #222;\n font-family: sans-serif;\n font-size: 12px;\n text-align: center;\n cursor: -webkit-grab;\n cursor: grab;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n pointer-events: all;\n transform-origin: 0 0;\n}\n\n.react-graph__node:hover > * {\n box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);\n}\n\n.react-graph__node.selected > * {\n box-shadow: 0 0 0 2px #000;\n}\n\n.react-graph__handle {\n position: absolute;\n width: 12px;\n height: 12px;\n transform: translate(-50%, -50%);\n background: #222;\n left: 50%;\n border-radius: 50%;\n}";
styleInject(css); styleInject(css);
var ReactGraph = var ReactGraph =
+15 -6
View File
@@ -8,6 +8,14 @@ const SpecialNode = ({ data, styles }) => (
style={{ background: '#FFCC00', padding: 10, borderRadius: 30, ...styles }} style={{ background: '#FFCC00', padding: 10, borderRadius: 30, ...styles }}
> >
I am <strong>special</strong>!<br />{data.label} I am <strong>special</strong>!<br />{data.label}
<input
style={{ margin: '10px 0' }}
/>
<select>
<option>1</option>
<option>2</option>
<option>3</option>
</select>
</div> </div>
); );
@@ -17,12 +25,12 @@ class App extends PureComponent {
this.state = { this.state = {
elements: [ elements: [
{ data: { id: '1', label: 'Tests', type: 'input' }, position: { x: 0, y: 0 } }, { data: { id: '1', label: 'Tests', type: 'input' }, position: { x: 50, y: 50 } },
{ data: { id: '2', label: 'This is a node This is a node This is a node This is a node' }, position: { x: 100, y: 100 } }, { data: { id: '2', label: 'This is a node This is a node This is a node This is a node' }, position: { x: 100, y: 100 } },
{ data: { id: '3', label: 'This is a node' }, position: { x: 100, y: 200 }, style: { background: '#222', color: '#fff' } }, { data: { id: '3', label: 'This is a node' }, position: { x: 100, y: 200 }, style: { background: '#222', color: '#fff' } },
{ data: { id: '4', label: 'nody nodes', type: 'output' }, position: { x: 50, y: 300 } }, { data: { id: '4', label: 'nody nodes', type: 'output' }, position: { x: 50, y: 300 } },
{ data: { id: '5', label: 'Another node', type: 'default' }, position: { x: 400, y: 300 } }, { data: { id: '5', label: 'Another node', type: 'default' }, position: { x: 400, y: 300 } },
{ data: { id: '6', label: 'I am label', type: 'special' }, position: { x: 400, y: 400 } }, { data: { id: '6', label: 'I have inputs', type: 'special' }, position: { x: 400, y: 400 } },
{ data: { source: '1', target: '2' } }, { data: { source: '1', target: '2' } },
{ data: { source: '2', target: '3' } }, { data: { source: '2', target: '3' } },
{ data: { source: '3', target: '4' } }, { data: { source: '3', target: '4' } },
@@ -34,8 +42,9 @@ class App extends PureComponent {
onLoad(graphInstance) { onLoad(graphInstance) {
this.graphInstance = graphInstance; this.graphInstance = graphInstance;
console.log('graph loaded:', this.graphInstance); console.log('graph loaded:', this.graphInstance);
this.graphInstance.fitView();
} }
onChange(elements) { onChange(elements) {
@@ -57,7 +66,7 @@ class App extends PureComponent {
...prevState, ...prevState,
elements: prevState.elements.concat({ elements: prevState.elements.concat({
data: { id: prevState.elements.length + 1, label: 'Added node' }, data: { id: prevState.elements.length + 1, label: 'Added node' },
position: { x: 50, y: 50 } position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight }
}) })
})); }));
} }
@@ -76,14 +85,14 @@ class App extends PureComponent {
> >
<button <button
type="button" type="button"
style={{ position: 'absolute', right: '10px', bottom: '10px' }} style={{ position: 'absolute', right: '10px', bottom: '10px', zIndex: 4 }}
onClick={() => this.onFitView()} onClick={() => this.onFitView()}
> >
fit fit
</button> </button>
<button <button
type="button" type="button"
style={{ position: 'absolute', bottom: '10px', left: '10px' }} style={{ position: 'absolute', bottom: '10px', left: '10px', zIndex: 4 }}
onClick={() => this.onAdd()} onClick={() => this.onAdd()}
> >
add add
+1
View File
@@ -6,6 +6,7 @@
"main": "dist/ReactGraph.js", "main": "dist/ReactGraph.js",
"private": true, "private": true,
"dependencies": { "dependencies": {
"classnames": "^2.2.6",
"d3-selection": "^1.4.0", "d3-selection": "^1.4.0",
"d3-zoom": "^1.7.3", "d3-zoom": "^1.7.3",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
+4 -1
View File
@@ -9,7 +9,10 @@ const nodeStyles = {
}; };
export default ({ data, style }) => ( export default ({ data, style }) => (
<div style={{ ...nodeStyles, ...style }}> <div
style={{ ...nodeStyles, ...style }}
className="react-graph__node-inner"
>
{data.label} {data.label}
<Handle style={{ bottom: 0, top: 'auto', transform: 'translate(-50%, 50%)' }} /> <Handle style={{ bottom: 0, top: 'auto', transform: 'translate(-50%, 50%)' }} />
</div> </div>
+16 -3
View File
@@ -1,9 +1,11 @@
import React, { useEffect, useRef, useContext } from 'react'; import React, { useEffect, useRef, useContext } from 'react';
import ReactDraggable from 'react-draggable'; import ReactDraggable from 'react-draggable';
import cx from 'classnames';
import { GraphContext } from '../../GraphContext'; import { GraphContext } from '../../GraphContext';
import { updateNodeData, updateNodePos } from '../../state/actions'; import { updateNodeData, updateNodePos } from '../../state/actions';
import { projectPosition } from '../../graph-utils';
const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName);
export default NodeComponent => (props) => { export default NodeComponent => (props) => {
const { position, data, onNodeClick } = props; const { position, data, onNodeClick } = props;
@@ -11,6 +13,8 @@ export default NodeComponent => (props) => {
const nodeElement = useRef(null); const nodeElement = useRef(null);
const graphContext = useContext(GraphContext); const graphContext = useContext(GraphContext);
const [ x, y, k ] = graphContext.state.transform; const [ x, y, k ] = graphContext.state.transform;
const selected = graphContext.state.selectedNodes.includes(id);
const nodeClasses = cx('react-graph__node', { selected });
useEffect(() => { useEffect(() => {
const bounds = nodeElement.current.getBoundingClientRect(); const bounds = nodeElement.current.getBoundingClientRect();
@@ -24,6 +28,10 @@ export default NodeComponent => (props) => {
<ReactDraggable.DraggableCore <ReactDraggable.DraggableCore
grid={[1, 1]} grid={[1, 1]}
onStart={(e) => { onStart={(e) => {
if (isInputTarget(e)) {
return false;
}
const unscaledPos = { const unscaledPos = {
x: e.clientX * (1 / k), x: e.clientX * (1 / k),
y: e.clientY * (1 / k) y: e.clientY * (1 / k)
@@ -48,10 +56,15 @@ export default NodeComponent => (props) => {
scale={k} scale={k}
> >
<div <div
className="react-graph__nodewrap" className={nodeClasses}
ref={nodeElement} ref={nodeElement}
style={{ transform: `translate(${position.x}px,${position.y}px)` }} style={{ transform: `translate(${position.x}px,${position.y}px)` }}
onClick={() => onNodeClick({ data, position })} onClick={(e) => {
if (isInputTarget(e)) {
return false;
}
onNodeClick({ data, position });
}}
> >
<NodeComponent {...props} /> <NodeComponent {...props} />
</div> </div>
+45 -16
View File
@@ -1,36 +1,63 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState, useContext } from 'react';
import { GraphContext } from '../GraphContext';
import { updateSelection, setSelection } from '../state/actions';
const initialRect = {
startX: 0,
startY: 0,
x: 0,
y: 0,
width: 0,
height: 0,
draw: false
};
export default () => { export default () => {
const selectionPane = useRef(null); const selectionPane = useRef(null);
const [rect, setRect] = useState({ x: 0, y: 0, width: 0, height: 0, draw: false }); const [rect, setRect] = useState(initialRect);
const { dispatch } = useContext(GraphContext);
useEffect(() => { useEffect(() => {
function onMouseDown(evt) { function onMouseDown(evt) {
setRect((r) => ({ setRect((r) => ({
...r, ...r,
startX: evt.clientX,
startY: evt.clientY,
x: evt.clientX, x: evt.clientX,
y: evt.clientY, y: evt.clientY,
draw: true draw: true
})); }));
dispatch(setSelection(true));
} }
function onMouseMove(evt) { function onMouseMove(evt) {
setRect((r) => ({ setRect((r) => {
...r, const negativeX = evt.clientX < r.startX;
width: evt.clientX, const negativeY = evt.clientY < r.startY;
height: evt.clientY
})); if (!r.draw) {
return r;
}
const nextRect = {
...r,
x: negativeX ? evt.clientX : r.x,
y: negativeY ? evt.clientY : r.y,
width: negativeX ? r.startX - evt.clientX : evt.clientX - r.startX,
height: negativeY ? r.startY - evt.clientY : evt.clientY - r.startY,
};
dispatch(updateSelection(nextRect));
return nextRect;
});
} }
function onMouseUp() { function onMouseUp() {
console.log('selection mouse up'); setRect(initialRect);
} dispatch(setSelection(false));
function removeAll() {
selectionPane.current.removeEventListener('mousedown', onMouseDown);
selectionPane.current.removeEventListener('mousemove', onMouseMove);
selectionPane.current.removeEventListener('mouseup', onMouseUp);
} }
selectionPane.current.addEventListener('mousedown', onMouseDown); selectionPane.current.addEventListener('mousedown', onMouseDown);
@@ -38,7 +65,9 @@ export default () => {
selectionPane.current.addEventListener('mouseup', onMouseUp); selectionPane.current.addEventListener('mouseup', onMouseUp);
return () => { return () => {
removeAll(); selectionPane.current.removeEventListener('mousedown', onMouseDown);
selectionPane.current.removeEventListener('mousemove', onMouseMove);
selectionPane.current.removeEventListener('mouseup', onMouseUp);
}; };
}, []); }, []);
+19 -2
View File
@@ -42,16 +42,33 @@ export const getBoundingBox = (nodes) => {
}; };
}; };
export const projectPosition = (pos, transform) => { export const graphPosToZoomedPos = (pos, transform) => {
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]) => {
return nodes.
filter(n => {
const bboxPos = {
x: (bbox.x - transform[0]) * (1 / transform[2]),
y: (bbox.y - transform[1]) * (1 / transform[2])
};
const bboxWidth = bbox.width * (1 / transform[2]);
const bboxHeight = bbox.height * (1 / transform[2]);
return (
(n.position.x > bboxPos.x && (n.position.x + n.data.__width) < (bboxPos.x + bboxWidth)) &&
(n.position.y > bboxPos.y && (n.position.y + n.data.__height) < (bboxPos.y + bboxHeight))
);
});
};
export default { export default {
isEdge, isEdge,
separateElements, separateElements,
getBoundingBox, getBoundingBox,
projectPosition graphPosToZoomedPos
}; };
+15 -1
View File
@@ -1,6 +1,7 @@
import { import {
UPDATE_TRANSFORM, UPDATE_SIZE, SET_NODES, SET_EDGES, UPDATE_TRANSFORM, UPDATE_SIZE, SET_NODES, SET_EDGES,
UPDATE_NODE_DATA, UPDATE_NODE_POS, INIT_D3, FIT_VIEW UPDATE_NODE_DATA, UPDATE_NODE_POS, INIT_D3, FIT_VIEW,
UPDATE_SELECTION, SET_SELECTION
} from './index'; } from './index';
export const updateTransform = (transform) => { export const updateTransform = (transform) => {
@@ -55,3 +56,16 @@ export const initD3 = ({ zoom, selection }) => {
export const fitView = () => { export const fitView = () => {
return { type: FIT_VIEW }; return { type: FIT_VIEW };
}; };
export const setSelection = (isActive) => {
return { type: SET_SELECTION, payload: { selectionActive: isActive } };
};
export const updateSelection = (selection) => {
return {
type: UPDATE_SELECTION,
payload: {
selection
}
};
};
+13 -2
View File
@@ -1,6 +1,6 @@
import { zoomIdentity } from 'd3-zoom'; import { zoomIdentity } from 'd3-zoom';
import { getBoundingBox } from '../graph-utils'; import { getBoundingBox, getNodesInside } from '../graph-utils';
export const SET_EDGES = 'SET_EDGES'; export const SET_EDGES = 'SET_EDGES';
export const SET_NODES = 'SET_NODES'; export const SET_NODES = 'SET_NODES';
@@ -10,6 +10,8 @@ export const UPDATE_TRANSFORM = 'UPDATE_TRANSFORM';
export const UPDATE_SIZE = 'UPDATE_SIZE'; export const UPDATE_SIZE = 'UPDATE_SIZE';
export const INIT_D3 = 'INIT_D3'; export const INIT_D3 = 'INIT_D3';
export const FIT_VIEW = 'FIT_VIEW'; export const FIT_VIEW = 'FIT_VIEW';
export const UPDATE_SELECTION = 'UPDATE_SELECTION';
export const SET_SELECTION = 'SET_SELECTION';
export const initialState = { export const initialState = {
width: 0, width: 0,
@@ -17,10 +19,14 @@ export const initialState = {
transform: [0, 0, 1], transform: [0, 0, 1],
nodes: [], nodes: [],
edges: [], edges: [],
selectedNodes: [],
d3Zoom: null, d3Zoom: null,
d3Selection: null, d3Selection: null,
d3Initialised: false d3Initialised: false,
selectionActive: false,
selection: {}
}; };
export const reducer = (state, action) => { export const reducer = (state, action) => {
@@ -63,11 +69,16 @@ export const reducer = (state, action) => {
return state; return state;
} }
case UPDATE_SELECTION: {
const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform).map(n => n.data.id);
return { ...state, ...action.payload, selectedNodes };
}
case SET_NODES: case SET_NODES:
case SET_EDGES: case SET_EDGES:
case UPDATE_TRANSFORM: case UPDATE_TRANSFORM:
case INIT_D3: case INIT_D3:
case UPDATE_SIZE: case UPDATE_SIZE:
case SET_SELECTION:
return { ...state, ...action.payload }; return { ...state, ...action.payload };
default: default:
return state; return state;
+10 -6
View File
@@ -17,7 +17,7 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 2; z-index: 1;
} }
.react-graph__selectionpane { .react-graph__selectionpane {
@@ -26,15 +26,15 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 3; z-index: 2;
} }
.react-graph__selection { .react-graph__selection {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
background: rgba(0,0,0,0.1); background: rgba(0, 89, 220, 0.08);
border: 1px solid #222; border: 1px dotted rgba(0, 89, 220, 0.8);
} }
.react-graph__edges { .react-graph__edges {
@@ -56,7 +56,7 @@
transform-origin: 0 0; transform-origin: 0 0;
} }
.react-graph__nodewrap { .react-graph__node {
position: absolute; position: absolute;
width: 150px; width: 150px;
color: #222; color: #222;
@@ -69,10 +69,14 @@
transform-origin: 0 0; transform-origin: 0 0;
} }
.react-graph__nodewrap:hover > * { .react-graph__node:hover > * {
box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08); box-shadow: 0 1px 5px 2px rgba(0, 0, 0, 0.08);
} }
.react-graph__node.selected > * {
box-shadow: 0 0 0 2px #000;
}
.react-graph__handle { .react-graph__handle {
position: absolute; position: absolute;
width: 12px; width: 12px;