diff --git a/example/SimpleGraph.js b/example/SimpleGraph.js index aead6e0d..4d35069b 100644 --- a/example/SimpleGraph.js +++ b/example/SimpleGraph.js @@ -86,7 +86,7 @@ class App extends PureComponent { console.log('clicked', node)} - onNodeRemove={nodeIds => console.log('remove', nodeIds)} + onElementsRemove={elements => console.log('remove', elements)} style={{ width: '100%', height: '100%' }} onLoad={graphInstance => this.onLoad(graphInstance)} onChange={(elements) => this.onChange(elements)} diff --git a/src/EdgeRenderer/EdgeTypes/wrapEdge.js b/src/EdgeRenderer/EdgeTypes/wrapEdge.js index fc87957f..6440c9a3 100644 --- a/src/EdgeRenderer/EdgeTypes/wrapEdge.js +++ b/src/EdgeRenderer/EdgeTypes/wrapEdge.js @@ -3,16 +3,17 @@ import ReactDraggable from 'react-draggable'; import cx from 'classnames'; import { GraphContext } from '../../GraphContext'; -import { updateNodePos, setSelectedNodesIds } from '../../state/actions'; +import { updateNodePos, setSelectedElements } from '../../state/actions'; +import { isEdge } from '../../graph-utils'; const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName); export default EdgeComponent => (props) => { const { state, dispatch } = useContext(GraphContext); const { data, onClick } = props; - const { id } = data; - const [ x, y, k ] = state.transform; - const selected = state.selectedNodeIds.includes(id); + const selected = state.selectedElements + .filter(e => isEdge(e)) + .find(e => e.data.source === data.source && e.data.target === data.target); const edgeClasses = cx('react-graph__edge', { selected }); return ( @@ -23,7 +24,7 @@ export default EdgeComponent => (props) => { return false; } - // dispatch(setSelectedNodesIds(id)); + dispatch(setSelectedElements({ data })); onClick({ data }); }} > diff --git a/src/GlobalKeyHandler/index.js b/src/GlobalKeyHandler/index.js index 0b5411b0..a2d9664a 100644 --- a/src/GlobalKeyHandler/index.js +++ b/src/GlobalKeyHandler/index.js @@ -9,8 +9,8 @@ export default (props) => { const removePressed = useKeyPress('Backspace'); useEffect(() => { - if (removePressed && state.selectedNodeIds.length) { - props.onNodeRemove(state.selectedNodeIds); + if (removePressed && state.selectedElements.length) { + props.onElementsRemove(state.selectedElements); dispatch(setNodesSelection({ isActive: false })); } }, [removePressed]) diff --git a/src/NodeRenderer/NodeTypes/wrapNode.js b/src/NodeRenderer/NodeTypes/wrapNode.js index e54af39a..65507006 100644 --- a/src/NodeRenderer/NodeTypes/wrapNode.js +++ b/src/NodeRenderer/NodeTypes/wrapNode.js @@ -3,7 +3,8 @@ import ReactDraggable from 'react-draggable'; import cx from 'classnames'; import { GraphContext } from '../../GraphContext'; -import { updateNodeData, updateNodePos, setSelectedNodesIds } from '../../state/actions'; +import { updateNodeData, updateNodePos, setSelectedElements } from '../../state/actions'; +import { isEdge } from '../../graph-utils'; const isInputTarget = (e) => ['INPUT', 'SELECT', 'TEXTAREA'].includes(e.target.nodeName); @@ -16,7 +17,7 @@ export default NodeComponent => (props) => { const { position } = __rg; const { id } = data; const [ x, y, k ] = state.transform; - const selected = state.selectedNodeIds.includes(id); + const selected = state.selectedElements.filter(e => !isEdge(e)).map(e => e.data.id).includes(id); const nodeClasses = cx('react-graph__node', { selected }); useEffect(() => { @@ -69,7 +70,7 @@ export default NodeComponent => (props) => { return false; } - dispatch(setSelectedNodesIds(id)); + dispatch(setSelectedElements({ data })); onClick({ data, position }); }} > diff --git a/src/graph-utils.js b/src/graph-utils.js index 4b871b2e..40d6f321 100644 --- a/src/graph-utils.js +++ b/src/graph-utils.js @@ -1,4 +1,4 @@ -export const isEdge = element => element.data.source && element.data.target; +export const isEdge = element => element.data && element.data.source && element.data.target; export const parseElements = e => ({ ...e, diff --git a/src/index.js b/src/index.js index 5558b9d0..e8fc030f 100644 --- a/src/index.js +++ b/src/index.js @@ -25,7 +25,7 @@ class ReactGraph extends PureComponent { render() { const { - style, onElementClick, children, onLoad, onMove, onChange, elements, onNodeRemove + style, onElementClick, children, onLoad, onMove, onChange, elements, onElementsRemove } = this.props; const { nodes, edges } = elements @@ -43,7 +43,7 @@ class ReactGraph extends PureComponent { edgeTypes={this.edgeTypes} /> {children} @@ -54,7 +54,7 @@ class ReactGraph extends PureComponent { ReactGraph.defaultProps = { onElementClick: () => {}, - onNodeRemove: () => {}, + onElementsRemove: () => {}, onLoad: () => {}, onMove: () => {}, onChange: () => {}, diff --git a/src/state/actions.js b/src/state/actions.js index 9f6ffa88..542fddc2 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_NODES_IDS, REMOVE_NODES + SET_SELECTED_ELEMENTS, REMOVE_NODES } from './index'; export const updateTransform = (transform) => { @@ -66,10 +66,16 @@ 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]; +export const setSelectedElements = (elements) => { + const elementsArray = Array.isArray(elements) ? elements : [elements]; - return { type: SET_SELECTED_NODES_IDS, payload: { selectedNodeIds: idArray, nodesSelectionActive: false } }; + return { + type: SET_SELECTED_ELEMENTS, + payload: { + selectedElements: elementsArray, + nodesSelectionActive: false + } + }; }; export const removeNodes = (ids) => { diff --git a/src/state/index.js b/src/state/index.js index 19911f72..63fb1300 100644 --- a/src/state/index.js +++ b/src/state/index.js @@ -13,7 +13,7 @@ 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 SET_SELECTED_ELEMENTS = 'SET_SELECTED_ELEMENTS'; export const REMOVE_NODES = 'REMOVE_NODES'; export const initialState = { @@ -22,7 +22,7 @@ export const initialState = { transform: [0, 0, 1], nodes: [], edges: [], - selectedNodeIds: [], + selectedElements: [], selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, @@ -79,13 +79,14 @@ export const reducer = (state, action) => { } case UPDATE_SELECTION: { const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform); - const selectedNodeIds = selectedNodes.map(n => n.data.id); + const selectedNodesIds = selectedNodes.map(n => n.data.id); + const selectedEdges = state.edges.filter(e => selectedNodesIds.includes(e.data.source) || selectedNodesIds.includes(e.data.target)) - return { ...state, ...action.payload, selectedNodeIds }; + return { ...state, ...action.payload, selectedElements: [...selectedNodes, ...selectedEdges] }; } case SET_NODES_SELECTION: { if (!action.payload.nodesSelectionActive) { - return { ...state, nodesSelectionActive: false, selectedNodeIds: [] }; + return { ...state, nodesSelectionActive: false, selectedElements: [] }; } const selectedNodes = getNodesInside(state.nodes, action.payload.selection, state.transform); const selectedNodesBbox = getBoundingBox(selectedNodes); @@ -105,7 +106,7 @@ export const reducer = (state, action) => { case INIT_D3: case UPDATE_SIZE: case SET_SELECTION: - case SET_SELECTED_NODES_IDS: + case SET_SELECTED_ELEMENTS: return { ...state, ...action.payload }; default: return state; diff --git a/src/style.css b/src/style.css index 7657312f..7923b298 100644 --- a/src/style.css +++ b/src/style.css @@ -52,6 +52,10 @@ pointer-events: all; } +.react-graph__edge.selected { + stroke: #ff5050; +} + .react-graph__nodes { width: 100%; height: 100%;