From 0e179ffe1f13130f4f47817e2630b6d120cf776d Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 16 Jul 2019 18:55:12 +0200 Subject: [PATCH] feat(nodes): add selection --- dist/ReactGraph.js | 176 ++++++++++++++++++++---- example/SimpleGraph.js | 21 ++- package.json | 1 + src/NodeRenderer/NodeTypes/InputNode.js | 5 +- src/NodeRenderer/NodeTypes/wrapNode.js | 19 ++- src/Selection/index.js | 61 +++++--- src/graph-utils.js | 21 ++- src/state/actions.js | 16 ++- src/state/index.js | 15 +- src/style.css | 16 ++- 10 files changed, 290 insertions(+), 61 deletions(-) diff --git a/dist/ReactGraph.js b/dist/ReactGraph.js index 2282d0bf..ea45bd47 100644 --- a/dist/ReactGraph.js +++ b/dist/ReactGraph.js @@ -228,6 +228,18 @@ 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() {}}; @@ -29767,15 +29779,20 @@ var UPDATE_SIZE = 'UPDATE_SIZE'; var INIT_D3 = 'INIT_D3'; var FIT_VIEW = 'FIT_VIEW'; + var UPDATE_SELECTION = 'UPDATE_SELECTION'; + var SET_SELECTION = 'SET_SELECTION'; var initialState = { width: 0, height: 0, transform: [0, 0, 1], nodes: [], edges: [], + selectedNodes: [], d3Zoom: null, d3Selection: null, - d3Initialised: false + d3Initialised: false, + selectionActive: false, + selection: {} }; var reducer = function reducer(state, action) { switch (action.type) { @@ -29817,11 +29834,22 @@ 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_EDGES: case UPDATE_TRANSFORM: case INIT_D3: case UPDATE_SIZE: + case SET_SELECTION: return _objectSpread2({}, state, {}, action.payload); default: @@ -29894,6 +29922,22 @@ 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 Provider = function Provider(props) { @@ -30075,55 +30119,73 @@ return EdgeRenderer; }(React.PureComponent); + var initialRect = { + startX: 0, + startY: 0, + x: 0, + y: 0, + width: 0, + height: 0, + draw: false + }; var Selection$2 = (function () { var selectionPane = React.useRef(null); - var _useState = React.useState({ - x: 0, - y: 0, - width: 0, - height: 0, - draw: false - }), + var _useState = React.useState(initialRect), _useState2 = _slicedToArray(_useState, 2), rect = _useState2[0], setRect = _useState2[1]; + var _useContext = React.useContext(GraphContext), + dispatch = _useContext.dispatch; + React.useEffect(function () { function onMouseDown(evt) { setRect(function (r) { return _objectSpread2({}, r, { + startX: evt.clientX, + startY: evt.clientY, x: evt.clientX, y: evt.clientY, draw: true }); }); + dispatch(setSelection(true)); } function onMouseMove(evt) { setRect(function (r) { - return _objectSpread2({}, r, { - width: evt.clientX, - height: evt.clientY + var negativeX = evt.clientX < r.startX; + var negativeY = evt.clientY < r.startY; + + 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() { - 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); selectionPane.current.addEventListener('mousemove', onMouseMove); selectionPane.current.addEventListener('mouseup', onMouseUp); return function () { - removeAll(); + selectionPane.current.removeEventListener('mousedown', onMouseDown); + selectionPane.current.removeEventListener('mousemove', onMouseMove); + selectionPane.current.removeEventListener('mouseup', onMouseUp); }; }, []); return React__default.createElement("div", { @@ -30280,7 +30342,8 @@ var data = _ref.data, style = _ref.style; 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, { style: { 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) { return function (props) { var position = props.position, @@ -32533,6 +32649,10 @@ y = _graphContext$state$t[1], k = _graphContext$state$t[2]; + var selected = graphContext.state.selectedNodes.includes(id); + var nodeClasses = classnames('react-graph__node', { + selected: selected + }); React.useEffect(function () { var bounds = nodeElement.current.getBoundingClientRect(); var unscaledWith = Math.round(bounds.width * (1 / k)); @@ -32545,6 +32665,10 @@ return React__default.createElement(reactDraggable.DraggableCore, { grid: [1, 1], onStart: function onStart(e) { + if (isInputTarget(e)) { + return false; + } + var unscaledPos = { x: e.clientX * (1 / k), y: e.clientY * (1 / k) @@ -32573,13 +32697,17 @@ }, scale: k }, React__default.createElement("div", { - className: "react-graph__nodewrap", + className: nodeClasses, ref: nodeElement, style: { transform: "translate(".concat(position.x, "px,").concat(position.y, "px)") }, - onClick: function onClick() { - return onNodeClick({ + onClick: function onClick(e) { + if (isInputTarget(e)) { + return false; + } + + onNodeClick({ data: data, 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); var ReactGraph = diff --git a/example/SimpleGraph.js b/example/SimpleGraph.js index 605783f3..1bdf9fdd 100644 --- a/example/SimpleGraph.js +++ b/example/SimpleGraph.js @@ -8,6 +8,14 @@ const SpecialNode = ({ data, styles }) => ( style={{ background: '#FFCC00', padding: 10, borderRadius: 30, ...styles }} > I am special!
{data.label} + + ); @@ -17,12 +25,12 @@ class App extends PureComponent { this.state = { 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: '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: '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: '2', target: '3' } }, { data: { source: '3', target: '4' } }, @@ -34,8 +42,9 @@ class App extends PureComponent { onLoad(graphInstance) { this.graphInstance = graphInstance; - console.log('graph loaded:', this.graphInstance); + + this.graphInstance.fitView(); } onChange(elements) { @@ -57,7 +66,7 @@ class App extends PureComponent { ...prevState, elements: prevState.elements.concat({ 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 { >