feat(wrapper): add onSelectionChange handler
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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];
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user