From 4be80dd9853a872e2a76710ffc5fde0135c30281 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 24 Jul 2019 16:44:26 +0200 Subject: [PATCH] feat(keyhandler): add onNodeRemove callback --- dist/ReactGraph.js | 125 ++++++++++++++++++------- example/SimpleGraph.js | 1 + src/GlobalKeyHandler/index.js | 19 ++++ src/GraphContext/index.js | 12 ++- src/NodeRenderer/NodeTypes/wrapNode.js | 14 +-- src/index.js | 9 +- src/state/actions.js | 15 ++- src/state/index.js | 14 +++ 8 files changed, 166 insertions(+), 43 deletions(-) create mode 100644 src/GlobalKeyHandler/index.js diff --git a/dist/ReactGraph.js b/dist/ReactGraph.js index 8f786d42..e7a1466e 100644 --- a/dist/ReactGraph.js +++ b/dist/ReactGraph.js @@ -29798,6 +29798,8 @@ var UPDATE_SELECTION = 'UPDATE_SELECTION'; var SET_SELECTION = 'SET_SELECTION'; var SET_NODES_SELECTION = 'SET_NODES_SELECTION'; + var SET_SELECTED_NODES_IDS = 'SET_SELECTED_NODES_IDS'; + var REMOVE_NODES = 'REMOVE_NODES'; var initialState = { width: 0, height: 0, @@ -29894,12 +29896,31 @@ }); } + case REMOVE_NODES: + { + var ids = action.payload.ids; + var nextEdges = state.edges.filter(function (e) { + return !ids.includes(e.data.target) && !ids.includes(e.data.source); + }); + var nextNodes = state.nodes.filter(function (n) { + return !ids.includes(n.data.id); + }); + console.log(ids); + console.log(state.edges, nextEdges); + console.log(state.nodes, nextNodes); + return _objectSpread2({}, state, { + nodes: nextNodes, + edges: nextEdges + }); + } + case SET_NODES: case SET_EDGES: case UPDATE_TRANSFORM: case INIT_D3: case UPDATE_SIZE: case SET_SELECTION: + case SET_SELECTED_NODES_IDS: return _objectSpread2({}, state, {}, action.payload); default: @@ -29991,6 +30012,15 @@ } }; }; + var setSelectedNodesIds = function setSelectedNodesIds(ids) { + var idArray = Array.isArray(ids) ? ids : [ids]; + return { + type: SET_SELECTED_NODES_IDS, + payload: { + selectedNodeIds: idArray + } + }; + }; var updateSelection = function updateSelection(selection) { return { type: UPDATE_SELECTION, @@ -30001,6 +30031,7 @@ }; var GraphContext = React.createContext({}); + var Provider = function Provider(props) { var onNodeClick = props.onNodeClick, children = props.children; @@ -30284,17 +30315,11 @@ var NodesSelection = (function () { var graphContext = React.useContext(GraphContext); - var state = graphContext.state, - dispatch = graphContext.dispatch; + var state = graphContext.state; return React__default.createElement("div", { className: "react-graph__nodesselection", style: { transform: "translate(".concat(state.transform[0], "px,").concat(state.transform[1], "px) scale(").concat(state.transform[2], ")") - }, - onClick: function onClick() { - return dispatch(setNodesSelection({ - isActive: false - })); } }, React__default.createElement("div", { className: "react-graph__nodesselection-rect", @@ -30344,11 +30369,15 @@ var GraphView = function GraphView(props) { var zoomPane = React.useRef(null); - var graphContext = React.useContext(GraphContext); + + var _useContext = React.useContext(GraphContext), + state = _useContext.state, + dispatch = _useContext.dispatch; + var shiftPressed = useKeyPress('Shift'); React.useEffect(function () { var selection = select(zoomPane.current).call(d3ZoomInstance); - graphContext.dispatch(initD3({ + dispatch(initD3({ zoom: d3ZoomInstance, selection: selection })); @@ -30362,35 +30391,35 @@ return false; } - graphContext.dispatch(updateTransform(event.transform)); + dispatch(updateTransform(event.transform)); props.onMove(); }); - if (graphContext.state.d3Selection) { + if (state.d3Selection) { // we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced - var graphTransform = identity$1.translate(graphContext.state.transform[0], graphContext.state.transform[1]).scale(graphContext.state.transform[2]); - graphContext.state.d3Selection.call(graphContext.state.d3Zoom.transform, graphTransform); + var graphTransform = identity$1.translate(state.transform[0], state.transform[1]).scale(state.transform[2]); + state.d3Selection.call(state.d3Zoom.transform, graphTransform); } } }, [shiftPressed]); React.useEffect(function () { - return graphContext.dispatch(updateSize(props.size)); + return dispatch(updateSize(props.size)); }, [props.size.width, props.size.height]); React.useEffect(function () { - if (graphContext.state.d3Initialised) { + if (state.d3Initialised) { props.onLoad({ - nodes: graphContext.state.nodes, - edges: graphContext.state.edges, + nodes: state.nodes, + edges: state.edges, fitView: function fitView$1() { - return graphContext.dispatch(fitView()); + return dispatch(fitView()); } }); } - }, [graphContext.state.d3Initialised]); + }, [state.d3Initialised]); React.useEffect(function () { props.onChange({ - nodes: graphContext.state.nodes, - edges: graphContext.state.edges + nodes: state.nodes, + edges: state.edges }); }); return React__default.createElement("div", { @@ -30398,10 +30427,15 @@ }, React__default.createElement(NodeRenderer, { nodeTypes: props.nodeTypes }), React__default.createElement(EdgeRenderer, { - width: graphContext.state.width, - height: graphContext.state.height - }), shiftPressed && React__default.createElement(UserSelection, null), graphContext.state.nodesSelectionActive && React__default.createElement(NodesSelection, null), React__default.createElement("div", { + width: state.width, + height: state.height + }), shiftPressed && React__default.createElement(UserSelection, null), state.nodesSelectionActive && React__default.createElement(NodesSelection, null), React__default.createElement("div", { className: "react-graph__zoompane", + onClick: function onClick() { + return dispatch(setNodesSelection({ + isActive: false + })); + }, ref: zoomPane })); }; @@ -30410,6 +30444,23 @@ monitorHeight: true })(GraphView); + var GlobalKeyHandler = (function (props) { + var _useContext = React.useContext(GraphContext), + state = _useContext.state, + dispatch = _useContext.dispatch; + + var removePressed = useKeyPress('Backspace'); + React.useEffect(function () { + if (removePressed && state.selectedNodeIds.length) { + props.onNodeRemove(state.selectedNodeIds); + dispatch(setNodesSelection({ + isActive: false + })); + } + }, [removePressed]); + return null; + }); + var Handle = (function (props) { return React__default.createElement("div", _extends({ className: "react-graph__handle" @@ -32747,7 +32798,10 @@ var wrapNode = (function (NodeComponent) { return function (props) { var nodeElement = React.useRef(null); - var graphContext = React.useContext(GraphContext); + + var _useContext = React.useContext(GraphContext), + state = _useContext.state, + dispatch = _useContext.dispatch; var _useState = React.useState({ x: 0, @@ -32763,12 +32817,12 @@ var position = __rg.position; var id = data.id; - var _graphContext$state$t = _slicedToArray(graphContext.state.transform, 3), - x = _graphContext$state$t[0], - y = _graphContext$state$t[1], - k = _graphContext$state$t[2]; + var _state$transform = _slicedToArray(state.transform, 3), + x = _state$transform[0], + y = _state$transform[1], + k = _state$transform[2]; - var selected = graphContext.state.selectedNodeIds.includes(id); + var selected = state.selectedNodeIds.includes(id); var nodeClasses = classnames('react-graph__node', { selected: selected }); @@ -32776,7 +32830,7 @@ var bounds = nodeElement.current.getBoundingClientRect(); var unscaledWith = Math.round(bounds.width * (1 / k)); var unscaledHeight = Math.round(bounds.height * (1 / k)); - graphContext.dispatch(updateNodeData(id, { + dispatch(updateNodeData(id, { width: unscaledWith, height: unscaledHeight })); @@ -32804,7 +32858,7 @@ x: e.clientX * (1 / k), y: e.clientY * (1 / k) }; - graphContext.dispatch(updateNodePos(id, { + dispatch(updateNodePos(id, { x: unscaledPos.x - x - offset.x, y: unscaledPos.y - y - offset.y })); @@ -32821,6 +32875,7 @@ return false; } + dispatch(setSelectedNodesIds(id)); onNodeClick({ data: data, position: position @@ -32900,7 +32955,8 @@ onLoad = _this$props.onLoad, onMove = _this$props.onMove, onChange = _this$props.onChange, - elements = _this$props.elements; + elements = _this$props.elements, + onNodeRemove = _this$props.onNodeRemove; var _elements$map$reduce = elements.map(parseElements).reduce(separateElements, {}), nodes = _elements$map$reduce.nodes, @@ -32918,6 +32974,8 @@ onMove: onMove, onChange: onChange, nodeTypes: this.nodeTypes + }), React__default.createElement(GlobalKeyHandler, { + onNodeRemove: onNodeRemove }), children)); } }]); @@ -32927,6 +32985,7 @@ ReactGraph.defaultProps = { onNodeClick: function onNodeClick() {}, + onNodeRemove: function onNodeRemove() {}, onLoad: function onLoad() {}, onMove: function onMove() {}, onChange: function onChange() {}, diff --git a/example/SimpleGraph.js b/example/SimpleGraph.js index d958764b..d8273bec 100644 --- a/example/SimpleGraph.js +++ b/example/SimpleGraph.js @@ -86,6 +86,7 @@ class App extends PureComponent { console.log(node)} + onNodeRemove={nodeIds => console.log('remove', nodeIds)} style={{ width: '100%', height: '100%' }} onLoad={graphInstance => this.onLoad(graphInstance)} onChange={(elements) => this.onChange(elements)} diff --git a/src/GlobalKeyHandler/index.js b/src/GlobalKeyHandler/index.js new file mode 100644 index 00000000..0b5411b0 --- /dev/null +++ b/src/GlobalKeyHandler/index.js @@ -0,0 +1,19 @@ +import { useEffect, useContext } from 'react'; + +import { useKeyPress } from '../hooks'; +import { setNodesSelection } from '../state/actions'; +import { GraphContext } from '../GraphContext'; + +export default (props) => { + const { state, dispatch } = useContext(GraphContext); + const removePressed = useKeyPress('Backspace'); + + useEffect(() => { + if (removePressed && state.selectedNodeIds.length) { + props.onNodeRemove(state.selectedNodeIds); + dispatch(setNodesSelection({ isActive: false })); + } + }, [removePressed]) + + return null; +} \ No newline at end of file diff --git a/src/GraphContext/index.js b/src/GraphContext/index.js index 8c4b5751..fec6e866 100644 --- a/src/GraphContext/index.js +++ b/src/GraphContext/index.js @@ -1,4 +1,4 @@ -import React, { createContext, useEffect, useReducer } from 'react'; +import React, { createContext, useEffect, useReducer, useRef } from 'react'; import PropTypes from 'prop-types'; import isEqual from 'lodash.isequal'; @@ -7,6 +7,16 @@ 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 { onNodeClick, diff --git a/src/NodeRenderer/NodeTypes/wrapNode.js b/src/NodeRenderer/NodeTypes/wrapNode.js index edf9d2ce..78659795 100644 --- a/src/NodeRenderer/NodeTypes/wrapNode.js +++ b/src/NodeRenderer/NodeTypes/wrapNode.js @@ -3,20 +3,20 @@ import ReactDraggable from 'react-draggable'; import cx from 'classnames'; import { GraphContext } from '../../GraphContext'; -import { updateNodeData, updateNodePos } from '../../state/actions'; +import { updateNodeData, updateNodePos, setSelectedNodesIds } from '../../state/actions'; const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName); export default NodeComponent => (props) => { const nodeElement = useRef(null); - const graphContext = useContext(GraphContext); + const { state, dispatch } = useContext(GraphContext); const [offset, setOffset] = useState({ x: 0, y: 0 }); const { data, onNodeClick, __rg } = props; const { position } = __rg; const { id } = data; - const [ x, y, k ] = graphContext.state.transform; - const selected = graphContext.state.selectedNodeIds.includes(id); + const [ x, y, k ] = state.transform; + const selected = state.selectedNodeIds.includes(id); const nodeClasses = cx('react-graph__node', { selected }); useEffect(() => { @@ -24,7 +24,7 @@ export default NodeComponent => (props) => { const unscaledWith = Math.round(bounds.width * (1 / k)); const unscaledHeight = Math.round(bounds.height * (1 / k)); - graphContext.dispatch(updateNodeData(id, { width: unscaledWith, height: unscaledHeight })); + dispatch(updateNodeData(id, { width: unscaledWith, height: unscaledHeight })); }, []); return ( @@ -50,7 +50,7 @@ export default NodeComponent => (props) => { y: e.clientY * (1 / k) } - graphContext.dispatch(updateNodePos(id, { + dispatch(updateNodePos(id, { x: unscaledPos.x - x - offset.x, y: unscaledPos.y - y - offset.y })); @@ -65,6 +65,8 @@ export default NodeComponent => (props) => { if (isInputTarget(e)) { return false; } + + dispatch(setSelectedNodesIds(id)); onNodeClick({ data, position }); }} > diff --git a/src/index.js b/src/index.js index 4228302a..b3207a8f 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react'; import { parseElements, separateElements } from './graph-utils'; import GraphView from './GraphView'; +import GlobalKeyHandler from './GlobalKeyHandler'; import { Provider } from './GraphContext'; import { createNodeTypes } from './NodeRenderer/utils'; @@ -20,7 +21,7 @@ class ReactGraph extends PureComponent { render() { const { - style, onNodeClick, children, onLoad, onMove, onChange, elements + style, onNodeClick, children, onLoad, onMove, onChange, elements, onNodeRemove } = this.props; const { nodes, edges } = elements @@ -36,6 +37,9 @@ class ReactGraph extends PureComponent { onChange={onChange} nodeTypes={this.nodeTypes} /> + {children} @@ -44,7 +48,8 @@ class ReactGraph extends PureComponent { } ReactGraph.defaultProps = { - onNodeClick: () => {}, + onNodeClick: () => {}, + onNodeRemove: () => {}, onLoad: () => {}, onMove: () => {}, onChange: () => {}, diff --git a/src/state/actions.js b/src/state/actions.js index 7b564eba..c8a18056 100644 --- a/src/state/actions.js +++ b/src/state/actions.js @@ -1,7 +1,8 @@ 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 + UPDATE_SELECTION, SET_SELECTION, SET_NODES_SELECTION, + SET_SELECTED_NODES_IDS, REMOVE_NODES } from './index'; export const updateTransform = (transform) => { @@ -65,6 +66,18 @@ export const setNodesSelection = ({ isActive, selection }) => { return { type: SET_NODES_SELECTION, payload: { nodesSelectionActive: isActive, selection } }; }; +export const setSelectedNodesIds = (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + + return { type: SET_SELECTED_NODES_IDS, payload: { selectedNodeIds: idArray } }; +}; + +export const removeNodes = (ids) => { + const idArray = Array.isArray(ids) ? ids : [ids]; + + return { type: REMOVE_NODES, payload: { ids: idArray } }; +} + export const updateSelection = (selection) => { return { type: UPDATE_SELECTION, diff --git a/src/state/index.js b/src/state/index.js index b6c561bb..3d17a147 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -13,6 +13,8 @@ export const FIT_VIEW = 'FIT_VIEW'; export const UPDATE_SELECTION = 'UPDATE_SELECTION'; export const SET_SELECTION = 'SET_SELECTION'; export const SET_NODES_SELECTION = 'SET_NODES_SELECTION'; +export const SET_SELECTED_NODES_IDS = 'SET_SELECTED_NODES_IDS'; +export const REMOVE_NODES = 'REMOVE_NODES'; export const initialState = { width: 0, @@ -98,12 +100,24 @@ export const reducer = (state, action) => { return { ...state, ...action.payload, selectedNodesBbox }; } + case REMOVE_NODES: { + const { ids } = action.payload; + const nextEdges = state.edges.filter(e => !ids.includes(e.data.target) && !ids.includes(e.data.source)); + const nextNodes = state.nodes.filter(n => !ids.includes(n.data.id)); + + console.log(ids,); + console.log( state.edges, nextEdges, ); + console.log(state.nodes, nextNodes); + + return { ...state, nodes: nextNodes, edges: nextEdges }; + } case SET_NODES: case SET_EDGES: case UPDATE_TRANSFORM: case INIT_D3: case UPDATE_SIZE: case SET_SELECTION: + case SET_SELECTED_NODES_IDS: return { ...state, ...action.payload }; default: return state;