From 6dc9e5f79f2a36db3f927a72d0453c47f9cbf0eb Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 25 Jul 2019 17:39:07 +0200 Subject: [PATCH] feat(rg): add zoom functions --- dist/ReactGraph.js | 209 ++++++++++++++++++++----------- example/SimpleGraph.js | 51 ++++++-- example/index.html | 6 + src/GraphContext/index.js | 10 -- src/GraphView/index.js | 9 +- src/NodeRenderer/Handle/index.js | 16 +-- src/state/actions.js | 10 +- src/state/index.js | 18 ++- 8 files changed, 221 insertions(+), 108 deletions(-) diff --git a/dist/ReactGraph.js b/dist/ReactGraph.js index 6cb2e8f8..113ad13b 100644 --- a/dist/ReactGraph.js +++ b/dist/ReactGraph.js @@ -126,6 +126,42 @@ return _setPrototypeOf(o, p); } + function _objectWithoutPropertiesLoose(source, excluded) { + if (source == null) return {}; + var target = {}; + var sourceKeys = Object.keys(source); + var key, i; + + for (i = 0; i < sourceKeys.length; i++) { + key = sourceKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + target[key] = source[key]; + } + + return target; + } + + function _objectWithoutProperties(source, excluded) { + if (source == null) return {}; + + var target = _objectWithoutPropertiesLoose(source, excluded); + + var key, i; + + if (Object.getOwnPropertySymbols) { + var sourceSymbolKeys = Object.getOwnPropertySymbols(source); + + for (i = 0; i < sourceSymbolKeys.length; i++) { + key = sourceSymbolKeys[i]; + if (excluded.indexOf(key) >= 0) continue; + if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; + target[key] = source[key]; + } + } + + return target; + } + function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); @@ -29832,6 +29868,8 @@ var UPDATE_SIZE = 'UPDATE_SIZE'; var INIT_D3 = 'INIT_D3'; var FIT_VIEW = 'FIT_VIEW'; + var ZOOM_IN = 'ZOOM_IN'; + var ZOOM_OUT = 'ZOOM_OUT'; var UPDATE_SELECTION = 'UPDATE_SELECTION'; var SET_SELECTION = 'SET_SELECTION'; var SET_NODES_SELECTION = 'SET_NODES_SELECTION'; @@ -29893,12 +29931,26 @@ var k = Math.min(state.width, state.height) / Math.max(bounds.width, bounds.height); var boundsCenterX = bounds.x + bounds.width / 2; var boundsCenterY = bounds.y + bounds.height / 2; - var translate = [state.width / 2 - boundsCenterX * k, state.height / 2 - boundsCenterY * k]; - var fittedTransform = identity$1.translate(translate[0], translate[1]).scale(k); + var transform = [state.width / 2 - boundsCenterX * k, state.height / 2 - boundsCenterY * k]; + var fittedTransform = identity$1.translate(transform[0], transform[1]).scale(k); state.d3Selection.call(state.d3Zoom.transform, fittedTransform); return state; } + case ZOOM_IN: + { + var _transform = state.transform; + state.d3Zoom.scaleTo(state.d3Selection, _transform[2] + 0.2); + return state; + } + + case ZOOM_OUT: + { + var _transform2 = state.transform; + state.d3Zoom.scaleTo(state.d3Selection, _transform2[2] - 0.2); + return state; + } + case UPDATE_SELECTION: { var selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform); @@ -30020,6 +30072,16 @@ type: FIT_VIEW }; }; + var zoomIn = function zoomIn() { + return { + type: ZOOM_IN + }; + }; + var zoomOut = function zoomOut() { + return { + type: ZOOM_OUT + }; + }; var setSelection = function setSelection(isActive) { return { type: SET_SELECTION, @@ -30059,7 +30121,6 @@ }; var GraphContext = React.createContext({}); - var Provider = function Provider(props) { var onElementClick = props.onElementClick, children = props.children; @@ -32709,6 +32770,12 @@ edges: state.edges, fitView: function fitView$1() { return dispatch(fitView()); + }, + zoomIn: function zoomIn$1() { + return dispatch(zoomIn()); + }, + zoomOut: function zoomOut$1() { + return dispatch(zoomOut()); } }); } @@ -32765,75 +32832,6 @@ return null; }); - var Handle = (function (props) { - return React__default.createElement("div", _extends({ - className: "react-graph__handle" - }, props)); - }); - - var nodeStyles = { - background: '#ff6060', - padding: 10, - borderRadius: 5, - width: 150 - }; - var DefaultNode = (function (_ref) { - var data = _ref.data, - style = _ref.style; - return React__default.createElement("div", { - style: _objectSpread2({}, nodeStyles, {}, style) - }, React__default.createElement(Handle, { - style: { - top: 0 - } - }), data.label, React__default.createElement(Handle, { - style: { - bottom: 0, - top: 'auto', - transform: 'translate(-50%, 50%)' - } - })); - }); - - var nodeStyles$1 = { - background: '#9999ff', - padding: 10, - borderRadius: 5, - width: 150 - }; - var InputNode = (function (_ref) { - var data = _ref.data, - style = _ref.style; - return React__default.createElement("div", { - style: _objectSpread2({}, nodeStyles$1, {}, style), - className: "react-graph__node-inner" - }, data.label, React__default.createElement(Handle, { - style: { - bottom: 0, - top: 'auto', - transform: 'translate(-50%, 50%)' - } - })); - }); - - var nodeStyles$2 = { - background: '#55ff99', - padding: 10, - borderRadius: 5, - width: 150 - }; - var OutputNode = (function (_ref) { - var data = _ref.data, - style = _ref.style; - return React__default.createElement("div", { - style: _objectSpread2({}, nodeStyles$2, {}, style) - }, React__default.createElement(Handle, { - style: { - top: 0 - } - }), data.label); - }); - var classnames = createCommonjsModule(function (module) { /*! Copyright (c) 2017 Jed Watson. @@ -32883,6 +32881,71 @@ }()); }); + var Handle = React.memo(function (_ref) { + var input = _ref.input, + output = _ref.output, + rest = _objectWithoutProperties(_ref, ["input", "output"]); + + var handleClasses = classnames('react-graph__handle', { + input: input, + output: output + }); + return React__default.createElement("div", _extends({ + className: handleClasses + }, rest)); + }); + + var nodeStyles = { + background: '#ff6060', + padding: 10, + borderRadius: 5, + width: 150 + }; + var DefaultNode = (function (_ref) { + var data = _ref.data, + style = _ref.style; + return React__default.createElement("div", { + style: _objectSpread2({}, nodeStyles, {}, style) + }, React__default.createElement(Handle, { + input: true + }), data.label, React__default.createElement(Handle, { + output: true + })); + }); + + var nodeStyles$1 = { + background: '#9999ff', + padding: 10, + borderRadius: 5, + width: 150 + }; + var InputNode = (function (_ref) { + var data = _ref.data, + style = _ref.style; + return React__default.createElement("div", { + style: _objectSpread2({}, nodeStyles$1, {}, style), + className: "react-graph__node-inner" + }, data.label, React__default.createElement(Handle, { + output: true + })); + }); + + var nodeStyles$2 = { + background: '#55dd99', + padding: 10, + borderRadius: 5, + width: 150 + }; + var OutputNode = (function (_ref) { + var data = _ref.data, + style = _ref.style; + return React__default.createElement("div", { + style: _objectSpread2({}, nodeStyles$2, {}, style) + }, React__default.createElement(Handle, { + input: true + }), data.label); + }); + var isInputTarget = function isInputTarget(e) { return ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName); }; @@ -33124,7 +33187,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: 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 position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n z-index: 2;\n}\n\n.react-graph__edge {\n fill: none;\n stroke: #333;\n stroke-width: 2;\n pointer-events: all;\n}\n\n.react-graph__edge.selected {\n stroke: #ff5050;\n }\n\n.react-graph__edge.animated {\n stroke-dasharray: 5;\n -webkit-animation: dashdraw 0.5s linear infinite;\n animation: dashdraw 0.5s linear infinite;\n }\n\n@-webkit-keyframes dashdraw {\n from {stroke-dashoffset: 10}\n}\n\n@keyframes dashdraw {\n from {stroke-dashoffset: 10}\n}\n\n.react-graph__nodes {\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: 3;\n pointer-events: none;\n transform-origin: 0 0;\n}\n\n.react-graph__node {\n position: absolute;\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}\n\n.react-graph__nodesselection {\n z-index: 3;\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n transform-origin: left top;\n pointer-events: none;\n}\n\n.react-graph__nodesselection-rect {\n position: absolute;\n background: rgba(0, 89, 220, 0.08);\n border: 1px dotted rgba(0, 89, 220, 0.8);\n pointer-events: all;\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 position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n z-index: 2;\n}\n\n.react-graph__edge {\n fill: none;\n stroke: #bbb;\n stroke-width: 2;\n pointer-events: all;\n}\n\n.react-graph__edge.selected {\n stroke: #555;\n }\n\n.react-graph__edge.animated {\n stroke-dasharray: 5;\n -webkit-animation: dashdraw 0.5s linear infinite;\n animation: dashdraw 0.5s linear infinite;\n }\n\n@-webkit-keyframes dashdraw {\n from {stroke-dashoffset: 10}\n}\n\n@keyframes dashdraw {\n from {stroke-dashoffset: 10}\n}\n\n.react-graph__nodes {\n width: 100%;\n height: 100%;\n position: absolute;\n z-index: 3;\n pointer-events: none;\n transform-origin: 0 0;\n}\n\n.react-graph__node {\n position: absolute;\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 #555;\n }\n\n.react-graph__handle {\n position: absolute;\n width: 10px;\n height: 5px;\n transform: translate(-50%, 0);\n background: rgba(255, 255, 255, 0.4);\n left: 50%;\n top: 0;\n}\n\n.react-graph__handle.output {\n top: auto;\n bottom: 0;\n }\n\n.react-graph__nodesselection {\n z-index: 3;\n position: absolute;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n transform-origin: left top;\n pointer-events: none;\n}\n\n.react-graph__nodesselection-rect {\n position: absolute;\n background: rgba(0, 89, 220, 0.08);\n border: 1px dotted rgba(0, 89, 220, 0.8);\n pointer-events: all;\n }"; styleInject(css); var ReactGraph = diff --git a/example/SimpleGraph.js b/example/SimpleGraph.js index b6072fd1..00063a1f 100644 --- a/example/SimpleGraph.js +++ b/example/SimpleGraph.js @@ -39,6 +39,7 @@ class App extends PureComponent { } this.state = { + graphLoaded: false, elements: [ { id: '1', type: 'input', data: { label: 'Tests' }, position: { x: 250, y: 5 } }, { id: '2', data: { label: 'This is a node This is a node This is a node This is a node' }, position: { x: 100, y: 100 } }, @@ -59,7 +60,12 @@ class App extends PureComponent { this.graphInstance = graphInstance; console.log('graph loaded:', this.graphInstance); + window.rg = graphInstance; this.graphInstance.fitView(); + + this.setState({ + graphLoaded: true + }); } onChange(elements) { @@ -70,9 +76,6 @@ class App extends PureComponent { } onFitView() { - if (!this.graphInstance) { - return false; - } this.graphInstance.fitView(); } @@ -80,12 +83,21 @@ class App extends PureComponent { this.setState(prevState => ({ ...prevState, elements: prevState.elements.concat({ - data: { id: prevState.elements.length + 1, label: 'Added node' }, + id: prevState.elements.length + 1, + data: { label: 'Added node' }, position: { x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight } }) })); } + onZoomIn() { + this.graphInstance.zoomIn(); + } + + onZoomOut() { + this.graphInstance.zoomOut(); + } + render() { return ( - + {this.state.graphLoaded && ( +
+ + + +
+ )}
); } diff --git a/example/index.html b/example/index.html index 6742959e..e78c471f 100644 --- a/example/index.html +++ b/example/index.html @@ -13,6 +13,12 @@ html, body, #root { height: 100%; } + .controls { + position: absolute; + bottom: 10px; + left: 10px; + z-index: 4; + }  diff --git a/src/GraphContext/index.js b/src/GraphContext/index.js index 4f869789..cab08982 100644 --- a/src/GraphContext/index.js +++ b/src/GraphContext/index.js @@ -7,16 +7,6 @@ import { setNodes, setEdges } from '../state/actions'; export const GraphContext = createContext({}); -function usePrevious(value) { - const ref = useRef(); - - useEffect(() => { - ref.current = value; - }); - - return ref.current; -} - export const Provider = (props) => { const { onElementClick, diff --git a/src/GraphView/index.js b/src/GraphView/index.js index 18ae6c97..e5eec0f9 100644 --- a/src/GraphView/index.js +++ b/src/GraphView/index.js @@ -8,7 +8,10 @@ import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; import UserSelection from '../UserSelection'; import NodesSelection from '../NodesSelection'; -import { updateTransform, updateSize, initD3, fitView, setNodesSelection } from '../state/actions'; +import { + updateTransform, updateSize, initD3, fitView, + zoomIn, zoomOut, setNodesSelection +} from '../state/actions'; import { useKeyPress } from '../hooks'; const d3ZoomInstance = d3Zoom.zoom().scaleExtent([0.5, 2]); @@ -59,7 +62,9 @@ const GraphView = memo((props) => { props.onLoad({ nodes: state.nodes, edges: state.edges, - fitView: () => dispatch(fitView()) + fitView: () => dispatch(fitView()), + zoomIn: () => dispatch(zoomIn()), + zoomOut: () => dispatch(zoomOut()) }); } }, [state.d3Initialised]); diff --git a/src/NodeRenderer/Handle/index.js b/src/NodeRenderer/Handle/index.js index 882c863d..47f2412e 100644 --- a/src/NodeRenderer/Handle/index.js +++ b/src/NodeRenderer/Handle/index.js @@ -1,15 +1,15 @@ -import React from 'react'; +import React, { memo } from 'react'; import cx from 'classnames'; -export default (props) => { +export default memo(({ input, output, ...rest }) => { const handleClasses = cx( - 'react-graph__handle', { - input: props.input, - output: props.output - } + 'react-graph__handle', { input, output } ); return ( -
+
); -}; +}); diff --git a/src/state/actions.js b/src/state/actions.js index 542fddc2..fdf67ef7 100644 --- a/src/state/actions.js +++ b/src/state/actions.js @@ -2,7 +2,7 @@ import { UPDATE_TRANSFORM, UPDATE_SIZE, SET_NODES, SET_EDGES, UPDATE_NODE_DATA, UPDATE_NODE_POS, INIT_D3, FIT_VIEW, UPDATE_SELECTION, SET_SELECTION, SET_NODES_SELECTION, - SET_SELECTED_ELEMENTS, REMOVE_NODES + SET_SELECTED_ELEMENTS, REMOVE_NODES, ZOOM_IN, ZOOM_OUT } from './index'; export const updateTransform = (transform) => { @@ -58,6 +58,14 @@ export const fitView = () => { return { type: FIT_VIEW }; }; +export const zoomIn = () => { + return { type: ZOOM_IN }; +}; + +export const zoomOut = () => { + return { type: ZOOM_OUT }; +} + export const setSelection = (isActive) => { return { type: SET_SELECTION, payload: { selectionActive: isActive } }; }; diff --git a/src/state/index.js b/src/state/index.js index 0783b8e6..97f16aee 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -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 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'; @@ -70,13 +72,25 @@ export const reducer = (state, action) => { const k = Math.min(state.width, state.height) / Math.max(bounds.width, bounds.height); const boundsCenterX = bounds.x + (bounds.width / 2); const boundsCenterY = bounds.y + (bounds.height / 2); - const translate = [(state.width / 2) - (boundsCenterX * k), (state.height / 2) - (boundsCenterY * k)]; - const fittedTransform = zoomIdentity.translate(translate[0], translate[1]).scale(k); + 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);