From f154f59e748c9c0e1a7d0ed4d138908c51755e68 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 25 May 2020 14:00:46 +0200 Subject: [PATCH] feat(wrapper): add onSelectionChange handler --- README.md | 1 + example/src/Overview/index.js | 2 ++ src/components/NodesSelection/index.tsx | 24 +++++++++++-------- src/components/SelectionListener/index.ts | 22 ++++++++++++++++++ src/container/EdgeRenderer/index.tsx | 8 +++---- src/container/NodeRenderer/index.tsx | 4 ++-- src/container/ReactFlow/index.tsx | 5 +++- src/hooks/useGlobalKeyHandler.ts | 11 ++++----- src/store/index.ts | 28 ++++++++--------------- 9 files changed, 62 insertions(+), 43 deletions(-) create mode 100644 src/components/SelectionListener/index.ts diff --git a/README.md b/README.md index c05652eb..bee31cf1 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ const BasicFlow = () => ( - `onConnect`: connect handler - `onLoad`: editor load handler - `onMove`: move handler +- `onSelectionChange`: fired when element selection changes - `nodeTypes`: object with [node types](#node-types--custom-nodes) - `edgeTypes`: object with [edge types](#edge-types--custom-edges) - `style`: css style passed to the wrapper diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js index a33817de..7df9ed28 100644 --- a/example/src/Overview/index.js +++ b/example/src/Overview/index.js @@ -5,6 +5,7 @@ import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flo const onNodeDragStart = node => console.log('drag start', node); const onNodeDragStop = node => console.log('drag stop', node); const onElementClick = element => console.log('click', element); +const onSelectionChange = elements => console.log('selection change', elements); const onLoad = (graph) => { console.log('graph loaded:', graph); graph.fitView(); @@ -53,6 +54,7 @@ const OverviewFlow = () => { onConnect={onConnect} onNodeDragStart={onNodeDragStart} onNodeDragStop={onNodeDragStop} + onSelectionChange={onSelectionChange} style={{ width: '100%', height: '100%' }} onLoad={onLoad} connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }} diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 9390aa01..6684b38b 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -50,9 +50,11 @@ export default memo(() => { }; const offsetX: number = scaledClient.x - position.x - tx; const offsetY: number = scaledClient.y - position.y - ty; - const selectedNodes = (state.selectedElements.filter(isNode) as Node[]).map( - (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node - ); + const selectedNodes = state.selectedElements + ? (state.selectedElements.filter(isNode) as Node[]).map( + (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node + ) + : []; const nextStartPositions = getStartPositions(selectedNodes); @@ -68,14 +70,16 @@ export default memo(() => { y: evt.clientY / tScale, }; - (state.selectedElements.filter(isNode) as Node[]).forEach((node) => { - const pos: XYPosition = { - x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, - y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, - }; + if (state.selectedElements) { + (state.selectedElements.filter(isNode) as Node[]).forEach((node) => { + const pos: XYPosition = { + x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, + y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, + }; - updateNodePos({ id: node.id, pos }); - }); + updateNodePos({ id: node.id, pos }); + }); + } }; return ( diff --git a/src/components/SelectionListener/index.ts b/src/components/SelectionListener/index.ts new file mode 100644 index 00000000..64c0e961 --- /dev/null +++ b/src/components/SelectionListener/index.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; + +import { Elements } from '../../types'; +import { useStoreState } from '../../store/hooks'; + +interface SelectionListenerProps { + onSelectionChange: (elements: Elements | null) => void; +} + +// This is just a helper component for calling the onSelectionChange listener. +// As soon as easy-peasy has implemented the effectOn hook, we can remove this compeonent +// and use the hook instead. https://github.com/ctrlplusb/easy-peasy/pull/459 + +export default ({ onSelectionChange }: SelectionListenerProps) => { + const selectedElements = useStoreState((s) => s.selectedElements); + + useEffect(() => { + onSelectionChange(selectedElements); + }, [selectedElements]); + + return null; +}; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 917182e0..351b3b34 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -117,7 +117,7 @@ function renderEdge( edge: Edge, props: EdgeRendererProps, nodes: Node[], - selectedElements: Elements, + selectedElements: Elements | null, isInteractive: boolean ) { const [sourceId, sourceHandleId] = edge.source.split('__'); @@ -154,9 +154,9 @@ function renderEdge( targetPosition ); - const isSelected = (selectedElements as Edge[]).some( - (elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId - ); + const isSelected = selectedElements + ? (selectedElements as Edge[]).some((elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId) + : false; return ( id === node.id); + const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false; return ( , 'on onConnect: (connection: Edge | Connection) => void; onLoad: OnLoadFunc; onMove: () => void; + onSelectionChange: (elements: Elements | null) => void; nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; connectionLineType: string; @@ -62,6 +64,7 @@ const ReactFlow = ({ onConnect, onNodeDragStart, onNodeDragStop, + onSelectionChange, connectionLineType, connectionLineStyle, deleteKeyCode, @@ -105,6 +108,7 @@ const ReactFlow = ({ onlyRenderVisibleNodes={onlyRenderVisibleNodes} isInteractive={isInteractive} /> + {onSelectionChange && } {children} @@ -143,7 +147,6 @@ ReactFlow.defaultProps = { snapGrid: [16, 16], onlyRenderVisibleNodes: true, isInteractive: true, - className: '', }; export default ReactFlow; diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index d03f3209..48ca0e8d 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -11,22 +11,19 @@ interface HookParams { } export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { - const state = useStoreState(s => ({ + const state = useStoreState((s) => ({ selectedElements: s.selectedElements, edges: s.edges, })); - const setNodesSelection = useStoreActions(a => a.setNodesSelection); + const setNodesSelection = useStoreActions((a) => a.setNodesSelection); const deleteKeyPressed = useKeyPress(deleteKeyCode); useEffect(() => { - if (deleteKeyPressed && state.selectedElements.length) { + if (deleteKeyPressed && state.selectedElements) { let elementsToRemove = state.selectedElements; // we also want to remove the edges if only one node is selected - if ( - state.selectedElements.length === 1 && - !isEdge(state.selectedElements[0]) - ) { + if (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) { const node = (state.selectedElements[0] as unknown) as Node; const connectedEdges = getConnectedEdges([node], state.edges); elementsToRemove = [...state.selectedElements, ...connectedEdges]; diff --git a/src/store/index.ts b/src/store/index.ts index 4ecf4bab..18e99bab 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -56,7 +56,7 @@ export interface StoreModel { transform: Transform; nodes: Node[]; edges: Edge[]; - selectedElements: Elements; + selectedElements: Elements | null; selectedNodesBbox: Rect; d3Zoom: ZoomBehavior | null; @@ -95,8 +95,6 @@ export interface StoreModel { setSelectedElements: Action; - updateSelection: Action; - updateTransform: Action; updateSize: Action; @@ -122,7 +120,7 @@ const storeModel: StoreModel = { transform: [0, 0, 1], nodes: [], edges: [], - selectedElements: [], + selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, @@ -250,8 +248,11 @@ const storeModel: StoreModel = { const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); state.selection = nextRect; - state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements; state.userSelectionRect = nextRect; + + if (selectedElementsUpdated) { + state.selectedElements = nextSelectedElements.length > 0 ? nextSelectedElements : null; + } }), unsetUserSelection: action((state) => { @@ -261,7 +262,7 @@ const storeModel: StoreModel = { state.selectionActive = false; state.userSelectionRect = { ...state.userSelectionRect, draw: false }; state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -283,7 +284,7 @@ const storeModel: StoreModel = { setNodesSelection: action((state, { isActive, selection }) => { if (!isActive || typeof selection === 'undefined') { state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -291,7 +292,7 @@ const storeModel: StoreModel = { if (!selectedNodes.length) { state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -311,17 +312,6 @@ const storeModel: StoreModel = { state.selectedElements = selectedElements; }), - updateSelection: action((state, selection) => { - const selectedNodes = getNodesInside(state.nodes, selection, state.transform); - const selectedEdges = getConnectedEdges(selectedNodes, state.edges); - - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); - - state.selection = selection; - state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements; - }), - updateTransform: action((state, transform) => { state.transform = [transform.x, transform.y, transform.k]; }),