+
{data.label}
diff --git a/src/NodeRenderer/NodeTypes/wrapNode.js b/src/NodeRenderer/NodeTypes/wrapNode.js
index f8c39b15..c65cfadb 100644
--- a/src/NodeRenderer/NodeTypes/wrapNode.js
+++ b/src/NodeRenderer/NodeTypes/wrapNode.js
@@ -1,9 +1,11 @@
import React, { useEffect, useRef, useContext } from 'react';
import ReactDraggable from 'react-draggable';
+import cx from 'classnames';
import { GraphContext } from '../../GraphContext';
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) => {
const { position, data, onNodeClick } = props;
@@ -11,6 +13,8 @@ export default NodeComponent => (props) => {
const nodeElement = useRef(null);
const graphContext = useContext(GraphContext);
const [ x, y, k ] = graphContext.state.transform;
+ const selected = graphContext.state.selectedNodes.includes(id);
+ const nodeClasses = cx('react-graph__node', { selected });
useEffect(() => {
const bounds = nodeElement.current.getBoundingClientRect();
@@ -24,6 +28,10 @@ export default NodeComponent => (props) => {
{
+ if (isInputTarget(e)) {
+ return false;
+ }
+
const unscaledPos = {
x: e.clientX * (1 / k),
y: e.clientY * (1 / k)
@@ -48,10 +56,15 @@ export default NodeComponent => (props) => {
scale={k}
>
onNodeClick({ data, position })}
+ onClick={(e) => {
+ if (isInputTarget(e)) {
+ return false;
+ }
+ onNodeClick({ data, position });
+ }}
>
diff --git a/src/Selection/index.js b/src/Selection/index.js
index 6d3100ed..e6cf73f3 100644
--- a/src/Selection/index.js
+++ b/src/Selection/index.js
@@ -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 () => {
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(() => {
function onMouseDown(evt) {
setRect((r) => ({
...r,
+ startX: evt.clientX,
+ startY: evt.clientY,
x: evt.clientX,
y: evt.clientY,
draw: true
}));
+
+ dispatch(setSelection(true));
}
function onMouseMove(evt) {
- setRect((r) => ({
- ...r,
- width: evt.clientX,
- height: evt.clientY
- }));
+ setRect((r) => {
+ const negativeX = evt.clientX < r.startX;
+ const negativeY = evt.clientY < r.startY;
+
+ 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() {
- console.log('selection mouse up');
- }
-
- function removeAll() {
- selectionPane.current.removeEventListener('mousedown', onMouseDown);
- selectionPane.current.removeEventListener('mousemove', onMouseMove);
- selectionPane.current.removeEventListener('mouseup', onMouseUp);
+ setRect(initialRect);
+ dispatch(setSelection(false));
}
selectionPane.current.addEventListener('mousedown', onMouseDown);
@@ -38,7 +65,9 @@ export default () => {
selectionPane.current.addEventListener('mouseup', onMouseUp);
return () => {
- removeAll();
+ selectionPane.current.removeEventListener('mousedown', onMouseDown);
+ selectionPane.current.removeEventListener('mousemove', onMouseMove);
+ selectionPane.current.removeEventListener('mouseup', onMouseUp);
};
}, []);
diff --git a/src/graph-utils.js b/src/graph-utils.js
index 6b0ca742..516d3e19 100644
--- a/src/graph-utils.js
+++ b/src/graph-utils.js
@@ -42,16 +42,33 @@ export const getBoundingBox = (nodes) => {
};
};
-export const projectPosition = (pos, transform) => {
+export const graphPosToZoomedPos = (pos, transform) => {
return {
x: (pos.x * transform[2]) + transform[0],
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 {
isEdge,
separateElements,
getBoundingBox,
- projectPosition
+ graphPosToZoomedPos
};
diff --git a/src/state/actions.js b/src/state/actions.js
index 4cf79f74..b1441f48 100644
--- a/src/state/actions.js
+++ b/src/state/actions.js
@@ -1,6 +1,7 @@
import {
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';
export const updateTransform = (transform) => {
@@ -55,3 +56,16 @@ export const initD3 = ({ zoom, selection }) => {
export const fitView = () => {
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
+ }
+ };
+};
diff --git a/src/state/index.js b/src/state/index.js
index 8a19427a..ba0b0bf7 100644
--- a/src/state/index.js
+++ b/src/state/index.js
@@ -1,6 +1,6 @@
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_NODES = 'SET_NODES';
@@ -10,6 +10,8 @@ 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 UPDATE_SELECTION = 'UPDATE_SELECTION';
+export const SET_SELECTION = 'SET_SELECTION';
export const initialState = {
width: 0,
@@ -17,10 +19,14 @@ export const initialState = {
transform: [0, 0, 1],
nodes: [],
edges: [],
+ selectedNodes: [],
d3Zoom: null,
d3Selection: null,
- d3Initialised: false
+ d3Initialised: false,
+
+ selectionActive: false,
+ selection: {}
};
export const reducer = (state, action) => {
@@ -63,11 +69,16 @@ export const reducer = (state, action) => {
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_EDGES:
case UPDATE_TRANSFORM:
case INIT_D3:
case UPDATE_SIZE:
+ case SET_SELECTION:
return { ...state, ...action.payload };
default:
return state;
diff --git a/src/style.css b/src/style.css
index 541c3fc5..b2428be8 100644
--- a/src/style.css
+++ b/src/style.css
@@ -17,7 +17,7 @@
position: absolute;
top: 0;
left: 0;
- z-index: 2;
+ z-index: 1;
}
.react-graph__selectionpane {
@@ -26,15 +26,15 @@
position: absolute;
top: 0;
left: 0;
- z-index: 3;
+ z-index: 2;
}
.react-graph__selection {
position: absolute;
top: 0;
left: 0;
- background: rgba(0,0,0,0.1);
- border: 1px solid #222;
+ background: rgba(0, 89, 220, 0.08);
+ border: 1px dotted rgba(0, 89, 220, 0.8);
}
.react-graph__edges {
@@ -56,7 +56,7 @@
transform-origin: 0 0;
}
-.react-graph__nodewrap {
+.react-graph__node {
position: absolute;
width: 150px;
color: #222;
@@ -69,10 +69,14 @@
transform-origin: 0 0;
}
-.react-graph__nodewrap:hover > * {
+.react-graph__node:hover > * {
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 {
position: absolute;
width: 12px;