feat(wrapper): add onSelectionChange handler

This commit is contained in:
moklick
2020-05-25 14:00:46 +02:00
parent 3d80fb6586
commit f154f59e74
9 changed files with 62 additions and 43 deletions
+1
View File
@@ -72,6 +72,7 @@ const BasicFlow = () => (
- `onConnect`: connect handler - `onConnect`: connect handler
- `onLoad`: editor load handler - `onLoad`: editor load handler
- `onMove`: move handler - `onMove`: move handler
- `onSelectionChange`: fired when element selection changes
- `nodeTypes`: object with [node types](#node-types--custom-nodes) - `nodeTypes`: object with [node types](#node-types--custom-nodes)
- `edgeTypes`: object with [edge types](#edge-types--custom-edges) - `edgeTypes`: object with [edge types](#edge-types--custom-edges)
- `style`: css style passed to the wrapper - `style`: css style passed to the wrapper
+2
View File
@@ -5,6 +5,7 @@ import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flo
const onNodeDragStart = node => console.log('drag start', node); const onNodeDragStart = node => console.log('drag start', node);
const onNodeDragStop = node => console.log('drag stop', node); const onNodeDragStop = node => console.log('drag stop', node);
const onElementClick = element => console.log('click', element); const onElementClick = element => console.log('click', element);
const onSelectionChange = elements => console.log('selection change', elements);
const onLoad = (graph) => { const onLoad = (graph) => {
console.log('graph loaded:', graph); console.log('graph loaded:', graph);
graph.fitView(); graph.fitView();
@@ -53,6 +54,7 @@ const OverviewFlow = () => {
onConnect={onConnect} onConnect={onConnect}
onNodeDragStart={onNodeDragStart} onNodeDragStart={onNodeDragStart}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onSelectionChange={onSelectionChange}
style={{ width: '100%', height: '100%' }} style={{ width: '100%', height: '100%' }}
onLoad={onLoad} onLoad={onLoad}
connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }} connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }}
+14 -10
View File
@@ -50,9 +50,11 @@ export default memo(() => {
}; };
const offsetX: number = scaledClient.x - position.x - tx; const offsetX: number = scaledClient.x - position.x - tx;
const offsetY: number = scaledClient.y - position.y - ty; const offsetY: number = scaledClient.y - position.y - ty;
const selectedNodes = (state.selectedElements.filter(isNode) as Node[]).map( const selectedNodes = state.selectedElements
(selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node ? (state.selectedElements.filter(isNode) as Node[]).map(
); (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node
)
: [];
const nextStartPositions = getStartPositions(selectedNodes); const nextStartPositions = getStartPositions(selectedNodes);
@@ -68,14 +70,16 @@ export default memo(() => {
y: evt.clientY / tScale, y: evt.clientY / tScale,
}; };
(state.selectedElements.filter(isNode) as Node[]).forEach((node) => { if (state.selectedElements) {
const pos: XYPosition = { (state.selectedElements.filter(isNode) as Node[]).forEach((node) => {
x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, const pos: XYPosition = {
y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, 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 ( return (
+22
View File
@@ -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;
};
+4 -4
View File
@@ -117,7 +117,7 @@ function renderEdge(
edge: Edge, edge: Edge,
props: EdgeRendererProps, props: EdgeRendererProps,
nodes: Node[], nodes: Node[],
selectedElements: Elements, selectedElements: Elements | null,
isInteractive: boolean isInteractive: boolean
) { ) {
const [sourceId, sourceHandleId] = edge.source.split('__'); const [sourceId, sourceHandleId] = edge.source.split('__');
@@ -154,9 +154,9 @@ function renderEdge(
targetPosition targetPosition
); );
const isSelected = (selectedElements as Edge[]).some( const isSelected = selectedElements
(elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId ? (selectedElements as Edge[]).some((elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId)
); : false;
return ( return (
<EdgeComponent <EdgeComponent
+2 -2
View File
@@ -16,7 +16,7 @@ function renderNode(
node: Node, node: Node,
props: NodeRendererProps, props: NodeRendererProps,
transform: Transform, transform: Transform,
selectedElements: Elements, selectedElements: Elements | null,
isInteractive: boolean isInteractive: boolean
) { ) {
const nodeType = node.type || 'default'; const nodeType = node.type || 'default';
@@ -25,7 +25,7 @@ function renderNode(
console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`); console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`);
} }
const isSelected = selectedElements.some(({ id }) => id === node.id); const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false;
return ( return (
<NodeComponent <NodeComponent
+4 -1
View File
@@ -14,6 +14,7 @@ import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode'; import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode'; import OutputNode from '../../components/Nodes/OutputNode';
import { createNodeTypes } from '../NodeRenderer/utils'; import { createNodeTypes } from '../NodeRenderer/utils';
import SelectionListener from '../../components/SelectionListener';
import BezierEdge from '../../components/Edges/BezierEdge'; import BezierEdge from '../../components/Edges/BezierEdge';
import StraightEdge from '../../components/Edges/StraightEdge'; import StraightEdge from '../../components/Edges/StraightEdge';
import StepEdge from '../../components/Edges/StepEdge'; import StepEdge from '../../components/Edges/StepEdge';
@@ -32,6 +33,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
onConnect: (connection: Edge | Connection) => void; onConnect: (connection: Edge | Connection) => void;
onLoad: OnLoadFunc; onLoad: OnLoadFunc;
onMove: () => void; onMove: () => void;
onSelectionChange: (elements: Elements | null) => void;
nodeTypes: NodeTypesType; nodeTypes: NodeTypesType;
edgeTypes: EdgeTypesType; edgeTypes: EdgeTypesType;
connectionLineType: string; connectionLineType: string;
@@ -62,6 +64,7 @@ const ReactFlow = ({
onConnect, onConnect,
onNodeDragStart, onNodeDragStart,
onNodeDragStop, onNodeDragStop,
onSelectionChange,
connectionLineType, connectionLineType,
connectionLineStyle, connectionLineStyle,
deleteKeyCode, deleteKeyCode,
@@ -105,6 +108,7 @@ const ReactFlow = ({
onlyRenderVisibleNodes={onlyRenderVisibleNodes} onlyRenderVisibleNodes={onlyRenderVisibleNodes}
isInteractive={isInteractive} isInteractive={isInteractive}
/> />
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children} {children}
</StoreProvider> </StoreProvider>
</div> </div>
@@ -143,7 +147,6 @@ ReactFlow.defaultProps = {
snapGrid: [16, 16], snapGrid: [16, 16],
onlyRenderVisibleNodes: true, onlyRenderVisibleNodes: true,
isInteractive: true, isInteractive: true,
className: '',
}; };
export default ReactFlow; export default ReactFlow;
+4 -7
View File
@@ -11,22 +11,19 @@ interface HookParams {
} }
export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => {
const state = useStoreState(s => ({ const state = useStoreState((s) => ({
selectedElements: s.selectedElements, selectedElements: s.selectedElements,
edges: s.edges, edges: s.edges,
})); }));
const setNodesSelection = useStoreActions(a => a.setNodesSelection); const setNodesSelection = useStoreActions((a) => a.setNodesSelection);
const deleteKeyPressed = useKeyPress(deleteKeyCode); const deleteKeyPressed = useKeyPress(deleteKeyCode);
useEffect(() => { useEffect(() => {
if (deleteKeyPressed && state.selectedElements.length) { if (deleteKeyPressed && state.selectedElements) {
let elementsToRemove = state.selectedElements; let elementsToRemove = state.selectedElements;
// we also want to remove the edges if only one node is selected // we also want to remove the edges if only one node is selected
if ( if (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) {
state.selectedElements.length === 1 &&
!isEdge(state.selectedElements[0])
) {
const node = (state.selectedElements[0] as unknown) as Node; const node = (state.selectedElements[0] as unknown) as Node;
const connectedEdges = getConnectedEdges([node], state.edges); const connectedEdges = getConnectedEdges([node], state.edges);
elementsToRemove = [...state.selectedElements, ...connectedEdges]; elementsToRemove = [...state.selectedElements, ...connectedEdges];
+9 -19
View File
@@ -56,7 +56,7 @@ export interface StoreModel {
transform: Transform; transform: Transform;
nodes: Node[]; nodes: Node[];
edges: Edge[]; edges: Edge[];
selectedElements: Elements; selectedElements: Elements | null;
selectedNodesBbox: Rect; selectedNodesBbox: Rect;
d3Zoom: ZoomBehavior<Element, unknown> | null; d3Zoom: ZoomBehavior<Element, unknown> | null;
@@ -95,8 +95,6 @@ export interface StoreModel {
setSelectedElements: Action<StoreModel, Elements | Node | Edge>; setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
updateSelection: Action<StoreModel, SelectionRect>;
updateTransform: Action<StoreModel, TransformXYK>; updateTransform: Action<StoreModel, TransformXYK>;
updateSize: Action<StoreModel, Dimensions>; updateSize: Action<StoreModel, Dimensions>;
@@ -122,7 +120,7 @@ const storeModel: StoreModel = {
transform: [0, 0, 1], transform: [0, 0, 1],
nodes: [], nodes: [],
edges: [], edges: [],
selectedElements: [], selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null, d3Zoom: null,
@@ -250,8 +248,11 @@ const storeModel: StoreModel = {
const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements);
state.selection = nextRect; state.selection = nextRect;
state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements;
state.userSelectionRect = nextRect; state.userSelectionRect = nextRect;
if (selectedElementsUpdated) {
state.selectedElements = nextSelectedElements.length > 0 ? nextSelectedElements : null;
}
}), }),
unsetUserSelection: action((state) => { unsetUserSelection: action((state) => {
@@ -261,7 +262,7 @@ const storeModel: StoreModel = {
state.selectionActive = false; state.selectionActive = false;
state.userSelectionRect = { ...state.userSelectionRect, draw: false }; state.userSelectionRect = { ...state.userSelectionRect, draw: false };
state.nodesSelectionActive = false; state.nodesSelectionActive = false;
state.selectedElements = []; state.selectedElements = null;
return; return;
} }
@@ -283,7 +284,7 @@ const storeModel: StoreModel = {
setNodesSelection: action((state, { isActive, selection }) => { setNodesSelection: action((state, { isActive, selection }) => {
if (!isActive || typeof selection === 'undefined') { if (!isActive || typeof selection === 'undefined') {
state.nodesSelectionActive = false; state.nodesSelectionActive = false;
state.selectedElements = []; state.selectedElements = null;
return; return;
} }
@@ -291,7 +292,7 @@ const storeModel: StoreModel = {
if (!selectedNodes.length) { if (!selectedNodes.length) {
state.nodesSelectionActive = false; state.nodesSelectionActive = false;
state.selectedElements = []; state.selectedElements = null;
return; return;
} }
@@ -311,17 +312,6 @@ const storeModel: StoreModel = {
state.selectedElements = selectedElements; 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) => { updateTransform: action((state, transform) => {
state.transform = [transform.x, transform.y, transform.k]; state.transform = [transform.x, transform.y, transform.k];
}), }),