refactor(selection): only fire selection changes that changed
This commit is contained in:
@@ -65,6 +65,7 @@ const UpdatableEdge = () => {
|
|||||||
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
const onConnect = (connection: Connection) => setEdges((els) => addEdge(connection, els));
|
||||||
|
|
||||||
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
const onNodesChange = useCallback((changes: NodeChange[]) => {
|
||||||
|
console.log(changes);
|
||||||
setNodes((ns) => applyNodeChanges(changes, ns));
|
setNodes((ns) => applyNodeChanges(changes, ns));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { getMarkerId } from '../../utils/graph';
|
|||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
addSelectedEdges: s.addSelectedEdges,
|
addSelectedEdges: s.addSelectedEdges,
|
||||||
setConnectionNodeId: s.setConnectionNodeId,
|
setConnectionNodeId: s.setConnectionNodeId,
|
||||||
unsetNodesSelection: s.unsetNodesSelection,
|
setNodesSelectionActive: s.setNodesSelectionActive,
|
||||||
setPosition: s.setConnectionPosition,
|
setPosition: s.setConnectionPosition,
|
||||||
connectionMode: s.connectionMode,
|
connectionMode: s.connectionMode,
|
||||||
});
|
});
|
||||||
@@ -57,7 +57,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
markerStart,
|
markerStart,
|
||||||
}: WrapEdgeProps): JSX.Element | null => {
|
}: WrapEdgeProps): JSX.Element | null => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { addSelectedEdges, setConnectionNodeId, unsetNodesSelection, setPosition, connectionMode } = useStore(
|
const { addSelectedEdges, setConnectionNodeId, setNodesSelectionActive, setPosition, connectionMode } = useStore(
|
||||||
selector,
|
selector,
|
||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
@@ -99,7 +99,7 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
|||||||
const onEdgeClick = useCallback(
|
const onEdgeClick = useCallback(
|
||||||
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
(event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
|
||||||
if (elementsSelectable) {
|
if (elementsSelectable) {
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
addSelectedEdges([edgeElement.id]);
|
addSelectedEdges([edgeElement.id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
|
|||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
addSelectedNodes: s.addSelectedNodes,
|
addSelectedNodes: s.addSelectedNodes,
|
||||||
unsetNodesSelection: s.unsetNodesSelection,
|
setNodesSelectionActive: s.setNodesSelectionActive,
|
||||||
updateNodePosition: s.updateNodePosition,
|
updateNodePosition: s.updateNodePosition,
|
||||||
updateNodeDimensions: s.updateNodeDimensions,
|
updateNodeDimensions: s.updateNodeDimensions,
|
||||||
unselectNodesAndEdges: s.unselectNodesAndEdges,
|
unselectNodesAndEdges: s.unselectNodesAndEdges,
|
||||||
@@ -56,7 +56,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
const {
|
const {
|
||||||
addSelectedNodes,
|
addSelectedNodes,
|
||||||
unselectNodesAndEdges,
|
unselectNodesAndEdges,
|
||||||
unsetNodesSelection,
|
setNodesSelectionActive,
|
||||||
updateNodePosition,
|
updateNodePosition,
|
||||||
// updateNodeDimensions,
|
// updateNodeDimensions,
|
||||||
} = useStore(selector, shallow);
|
} = useStore(selector, shallow);
|
||||||
@@ -127,7 +127,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
if (!isDraggable) {
|
if (!isDraggable) {
|
||||||
if (isSelectable) {
|
if (isSelectable) {
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
addSelectedNodes([node.id]);
|
addSelectedNodes([node.id]);
|
||||||
@@ -145,14 +145,14 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
onNodeDragStart?.(event as MouseEvent, node);
|
onNodeDragStart?.(event as MouseEvent, node);
|
||||||
|
|
||||||
if (selectNodesOnDrag && isSelectable) {
|
if (selectNodesOnDrag && isSelectable) {
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
addSelectedNodes([node.id]);
|
addSelectedNodes([node.id]);
|
||||||
}
|
}
|
||||||
} else if (!selectNodesOnDrag && !selected && isSelectable) {
|
} else if (!selectNodesOnDrag && !selected && isSelectable) {
|
||||||
unselectNodesAndEdges();
|
unselectNodesAndEdges();
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
[node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
|
||||||
@@ -163,9 +163,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
node.position.x += draggableData.deltaX;
|
node.position.x += draggableData.deltaX;
|
||||||
node.position.y += draggableData.deltaY;
|
node.position.y += draggableData.deltaY;
|
||||||
|
|
||||||
if (onNodeDrag) {
|
onNodeDrag?.(event as MouseEvent, node);
|
||||||
onNodeDrag(event as MouseEvent, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
|
updateNodePosition({ id, dragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
* made a selectio with on or several nodes
|
* made a selectio with on or several nodes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useCallback, useRef, MouseEvent } from 'react';
|
import React, { memo, useMemo, useCallback, useRef, MouseEvent } from 'react';
|
||||||
import ReactDraggable, { DraggableData } from 'react-draggable';
|
import { DraggableCore, DraggableData } from 'react-draggable';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
|
|
||||||
import { useStore } from '../../store';
|
import { useStore } from '../../store';
|
||||||
import { Node, ReactFlowState } from '../../types';
|
import { Node, ReactFlowState } from '../../types';
|
||||||
|
import { getRectOfNodes } from '../..';
|
||||||
|
|
||||||
export interface NodesSelectionProps {
|
export interface NodesSelectionProps {
|
||||||
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
|
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
|
||||||
@@ -21,7 +22,7 @@ export interface NodesSelectionProps {
|
|||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
transform: s.transform,
|
transform: s.transform,
|
||||||
selectedNodesBbox: s.selectedNodesBbox,
|
selectedNodesBbox: s.selectedNodesBbox,
|
||||||
selectionActive: s.selectionActive,
|
userSelectionActive: s.userSelectionActive,
|
||||||
selectedNodes: Array.from(s.nodeInternals)
|
selectedNodes: Array.from(s.nodeInternals)
|
||||||
.filter(([_, n]) => n.selected)
|
.filter(([_, n]) => n.selected)
|
||||||
.map(([_, n]) => n),
|
.map(([_, n]) => n),
|
||||||
@@ -30,14 +31,14 @@ const selector = (s: ReactFlowState) => ({
|
|||||||
updateNodePosition: s.updateNodePosition,
|
updateNodePosition: s.updateNodePosition,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ({
|
function NodesSelection({
|
||||||
onSelectionDragStart,
|
onSelectionDragStart,
|
||||||
onSelectionDrag,
|
onSelectionDrag,
|
||||||
onSelectionDragStop,
|
onSelectionDragStop,
|
||||||
onSelectionContextMenu,
|
onSelectionContextMenu,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
}: NodesSelectionProps) => {
|
}: NodesSelectionProps) {
|
||||||
const { transform, selectedNodesBbox, selectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
|
const { transform, userSelectionActive, selectedNodes, snapToGrid, snapGrid, updateNodePosition } =
|
||||||
useStore(selector);
|
useStore(selector);
|
||||||
const [tX, tY, tScale] = transform;
|
const [tX, tY, tScale] = transform;
|
||||||
const nodeRef = useRef(null);
|
const nodeRef = useRef(null);
|
||||||
@@ -51,6 +52,8 @@ export default ({
|
|||||||
[tX, tY, tScale]
|
[tX, tY, tScale]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
|
||||||
|
|
||||||
const innerStyle = useMemo(
|
const innerStyle = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
width: selectedNodesBbox.width,
|
width: selectedNodesBbox.width,
|
||||||
@@ -70,10 +73,6 @@ export default ({
|
|||||||
|
|
||||||
const onDrag = useCallback(
|
const onDrag = useCallback(
|
||||||
(event: MouseEvent, data: DraggableData) => {
|
(event: MouseEvent, data: DraggableData) => {
|
||||||
if (onSelectionDrag) {
|
|
||||||
onSelectionDrag(event, selectedNodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNodePosition({
|
updateNodePosition({
|
||||||
diff: {
|
diff: {
|
||||||
x: data.deltaX,
|
x: data.deltaX,
|
||||||
@@ -81,6 +80,8 @@ export default ({
|
|||||||
},
|
},
|
||||||
dragging: true,
|
dragging: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onSelectionDrag?.(event, selectedNodes);
|
||||||
},
|
},
|
||||||
[onSelectionDrag, selectedNodes, updateNodePosition]
|
[onSelectionDrag, selectedNodes, updateNodePosition]
|
||||||
);
|
);
|
||||||
@@ -103,13 +104,13 @@ export default ({
|
|||||||
[onSelectionContextMenu, selectedNodes]
|
[onSelectionContextMenu, selectedNodes]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedNodes || selectionActive) {
|
if (!selectedNodes?.length || userSelectionActive) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
|
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
|
||||||
<ReactDraggable
|
<DraggableCore
|
||||||
scale={tScale}
|
scale={tScale}
|
||||||
grid={grid}
|
grid={grid}
|
||||||
onStart={(event) => onStart(event as MouseEvent)}
|
onStart={(event) => onStart(event as MouseEvent)}
|
||||||
@@ -124,7 +125,9 @@ export default ({
|
|||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
style={innerStyle}
|
style={innerStyle}
|
||||||
/>
|
/>
|
||||||
</ReactDraggable>
|
</DraggableCore>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default memo(NodesSelection);
|
||||||
|
|||||||
@@ -2,104 +2,139 @@
|
|||||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { memo } from 'react';
|
import React, { memo, useState, useRef, useCallback } from 'react';
|
||||||
|
import shallow from 'zustand/shallow';
|
||||||
|
|
||||||
import { useStore } from '../../store';
|
import { useStore, useStoreApi } from '../../store';
|
||||||
import { XYPosition, ReactFlowState } from '../../types';
|
import { getSelectionChanges } from '../../utils/changes';
|
||||||
|
import { XYPosition, ReactFlowState, SelectionRect, NodeChange, EdgeChange } from '../../types';
|
||||||
|
import { getConnectedEdges, getNodesInside } from '../../utils/graph';
|
||||||
|
|
||||||
type UserSelectionProps = {
|
type UserSelectionProps = {
|
||||||
selectionKeyPressed: boolean;
|
selectionKeyPressed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMousePosition(event: React.MouseEvent): XYPosition | void {
|
function getMousePosition(event: React.MouseEvent, containerBounds: DOMRect): XYPosition {
|
||||||
const reactFlowNode = (event.target as Element).closest('.react-flow');
|
|
||||||
if (!reactFlowNode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const containerBounds = reactFlowNode.getBoundingClientRect();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: event.clientX - containerBounds.left,
|
x: event.clientX - containerBounds.left,
|
||||||
y: event.clientY - containerBounds.top,
|
y: event.clientY - containerBounds.top,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const userSelectionRectSelector = (state: ReactFlowState) => state.userSelectionRect;
|
|
||||||
|
|
||||||
const SelectionRect = () => {
|
|
||||||
const userSelectionRect = useStore(userSelectionRectSelector);
|
|
||||||
|
|
||||||
if (!userSelectionRect.draw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="react-flow__selection react-flow__container"
|
|
||||||
style={{
|
|
||||||
width: userSelectionRect.width,
|
|
||||||
height: userSelectionRect.height,
|
|
||||||
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
selectionActive: s.selectionActive,
|
userSelectionActive: s.userSelectionActive,
|
||||||
elementsSelectable: s.elementsSelectable,
|
elementsSelectable: s.elementsSelectable,
|
||||||
setUserSelection: s.setUserSelection,
|
setUserSelectionActive: s.setUserSelectionActive,
|
||||||
updateUserSelection: s.updateUserSelection,
|
setNodesSelectionActive: s.setNodesSelectionActive,
|
||||||
unsetUserSelection: s.unsetUserSelection,
|
|
||||||
unsetNodesSelection: s.unsetNodesSelection,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
|
const initialRect: SelectionRect = {
|
||||||
const {
|
startX: 0,
|
||||||
selectionActive,
|
startY: 0,
|
||||||
elementsSelectable,
|
x: 0,
|
||||||
setUserSelection,
|
y: 0,
|
||||||
updateUserSelection,
|
width: 0,
|
||||||
unsetUserSelection,
|
height: 0,
|
||||||
unsetNodesSelection,
|
draw: false,
|
||||||
} = useStore(selector);
|
};
|
||||||
|
|
||||||
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
|
export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
|
||||||
|
const store = useStoreApi();
|
||||||
|
const prevSelectedNodesCount = useRef<number>(0);
|
||||||
|
const prevSelectedEdgesCount = useRef<number>(0);
|
||||||
|
const containerBounds = useRef<DOMRect>();
|
||||||
|
const [userSelectionRect, setUserSelectionRect] = useState<SelectionRect>(initialRect);
|
||||||
|
const { userSelectionActive, elementsSelectable, setUserSelectionActive, setNodesSelectionActive } = useStore(
|
||||||
|
selector,
|
||||||
|
shallow
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderUserSelectionPane = userSelectionActive || selectionKeyPressed;
|
||||||
|
|
||||||
|
const resetUserSelection = useCallback(() => {
|
||||||
|
setUserSelectionRect(initialRect);
|
||||||
|
setUserSelectionActive(false);
|
||||||
|
|
||||||
|
prevSelectedNodesCount.current = 0;
|
||||||
|
prevSelectedEdgesCount.current = 0;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseDown = useCallback((event: React.MouseEvent): void => {
|
||||||
|
const reactFlowNode = (event.target as Element).closest('.react-flow')!;
|
||||||
|
containerBounds.current = reactFlowNode.getBoundingClientRect();
|
||||||
|
|
||||||
|
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||||
|
|
||||||
|
setUserSelectionRect({
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
startX: mousePos.x,
|
||||||
|
startY: mousePos.y,
|
||||||
|
x: mousePos.x,
|
||||||
|
y: mousePos.y,
|
||||||
|
draw: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
setUserSelectionActive(true);
|
||||||
|
setNodesSelectionActive(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseMove = (event: React.MouseEvent): void => {
|
||||||
|
if (!selectionKeyPressed || !userSelectionRect.draw || !containerBounds.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mousePos = getMousePosition(event, containerBounds.current!);
|
||||||
|
const startX = userSelectionRect.startX ?? 0;
|
||||||
|
const startY = userSelectionRect.startY ?? 0;
|
||||||
|
|
||||||
|
const nextUserSelectRect = {
|
||||||
|
...userSelectionRect,
|
||||||
|
x: mousePos.x < startX ? mousePos.x : userSelectionRect.x,
|
||||||
|
y: mousePos.y < startY ? mousePos.y : userSelectionRect.y,
|
||||||
|
width: Math.abs(mousePos.x - startX),
|
||||||
|
height: Math.abs(mousePos.y - startY),
|
||||||
|
};
|
||||||
|
|
||||||
|
const { nodeInternals, edges, transform, onNodesChange, onEdgesChange } = store.getState();
|
||||||
|
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
||||||
|
const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true);
|
||||||
|
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||||
|
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||||
|
|
||||||
|
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||||
|
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||||
|
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||||
|
if (changes.length) {
|
||||||
|
onNodesChange?.(changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||||
|
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||||
|
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||||
|
if (changes.length) {
|
||||||
|
onEdgesChange?.(changes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserSelectionRect(nextUserSelectRect);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = useCallback(() => {
|
||||||
|
setNodesSelectionActive(prevSelectedNodesCount.current > 0);
|
||||||
|
resetUserSelection();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onMouseLeave = useCallback(() => {
|
||||||
|
setNodesSelectionActive(false);
|
||||||
|
resetUserSelection();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!elementsSelectable || !renderUserSelectionPane) {
|
if (!elementsSelectable || !renderUserSelectionPane) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseDown = (event: React.MouseEvent): void => {
|
|
||||||
const mousePos = getMousePosition(event);
|
|
||||||
if (!mousePos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setUserSelection(mousePos);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseMove = (event: React.MouseEvent): void => {
|
|
||||||
if (!selectionKeyPressed || !selectionActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const mousePos = getMousePosition(event);
|
|
||||||
|
|
||||||
if (!mousePos) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUserSelection(mousePos);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMouseUp = () => unsetUserSelection();
|
|
||||||
|
|
||||||
const onMouseLeave = () => {
|
|
||||||
unsetUserSelection();
|
|
||||||
unsetNodesSelection();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="react-flow__selectionpane react-flow__container"
|
className="react-flow__selectionpane react-flow__container"
|
||||||
@@ -108,7 +143,16 @@ export default memo(({ selectionKeyPressed }: UserSelectionProps) => {
|
|||||||
onMouseUp={onMouseUp}
|
onMouseUp={onMouseUp}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<SelectionRect />
|
{userSelectionRect.draw && (
|
||||||
|
<div
|
||||||
|
className="react-flow__selection react-flow__container"
|
||||||
|
style={{
|
||||||
|
width: userSelectionRect.width,
|
||||||
|
height: userSelectionRect.height,
|
||||||
|
transform: `translate(${userSelectionRect.x}px, ${userSelectionRect.y}px)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ interface FlowRendererProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
unsetNodesSelection: s.unsetNodesSelection,
|
setNodesSelectionActive: s.setNodesSelectionActive,
|
||||||
resetSelectedElements: s.resetSelectedElements,
|
resetSelectedElements: s.resetSelectedElements,
|
||||||
nodesSelectionActive: s.nodesSelectionActive,
|
nodesSelectionActive: s.nodesSelectionActive,
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ const FlowRenderer = ({
|
|||||||
noWheelClassName,
|
noWheelClassName,
|
||||||
noPanClassName,
|
noPanClassName,
|
||||||
}: FlowRendererProps) => {
|
}: FlowRendererProps) => {
|
||||||
const { unsetNodesSelection, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
|
const { setNodesSelectionActive, resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
|
||||||
|
|
||||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ const FlowRenderer = ({
|
|||||||
const onClick = useCallback(
|
const onClick = useCallback(
|
||||||
(event: MouseEvent) => {
|
(event: MouseEvent) => {
|
||||||
onPaneClick?.(event);
|
onPaneClick?.(event);
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
resetSelectedElements();
|
resetSelectedElements();
|
||||||
},
|
},
|
||||||
[onPaneClick]
|
[onPaneClick]
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ interface HookParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => ({
|
const selector = (s: ReactFlowState) => ({
|
||||||
unsetNodesSelection: s.unsetNodesSelection,
|
setNodesSelectionActive: s.setNodesSelectionActive,
|
||||||
setMultiSelectionActive: s.setMultiSelectionActive,
|
setMultiSelectionActive: s.setMultiSelectionActive,
|
||||||
resetSelectedElements: s.resetSelectedElements,
|
resetSelectedElements: s.resetSelectedElements,
|
||||||
onNodesChange: s.onNodesChange,
|
onNodesChange: s.onNodesChange,
|
||||||
@@ -21,7 +21,7 @@ const selector = (s: ReactFlowState) => ({
|
|||||||
|
|
||||||
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
||||||
const store = useStoreApi();
|
const store = useStoreApi();
|
||||||
const { unsetNodesSelection, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
|
const { setNodesSelectionActive, setMultiSelectionActive, resetSelectedElements, onNodesChange, onEdgesChange } =
|
||||||
useStore(selector, shallow);
|
useStore(selector, shallow);
|
||||||
|
|
||||||
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
const deleteKeyPressed = useKeyPress(deleteKeyCode);
|
||||||
@@ -46,7 +46,7 @@ export default ({ deleteKeyCode, multiSelectionKeyCode }: HookParams): void => {
|
|||||||
onNodesChange?.(nodeChanges);
|
onNodesChange?.(nodeChanges);
|
||||||
onEdgesChange?.(edgeChanges);
|
onEdgesChange?.(edgeChanges);
|
||||||
|
|
||||||
unsetNodesSelection();
|
setNodesSelectionActive(false);
|
||||||
resetSelectedElements();
|
resetSelectedElements();
|
||||||
}
|
}
|
||||||
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
|
}, [deleteKeyPressed, onNodesChange, onEdgesChange]);
|
||||||
|
|||||||
+13
-84
@@ -27,9 +27,9 @@ import {
|
|||||||
XYPosition,
|
XYPosition,
|
||||||
ReactFlowStore,
|
ReactFlowStore,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getRectOfNodes, getNodesInside, getConnectedEdges } from '../utils/graph';
|
|
||||||
import { getHandleBounds } from '../components/Nodes/utils';
|
import { getHandleBounds } from '../components/Nodes/utils';
|
||||||
import { createNodeInternals, createNodeOrEdgeSelectionChange } from './utils';
|
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||||
|
import { createNodeInternals } from './utils';
|
||||||
|
|
||||||
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
|
const { Provider, useStore, useStoreApi } = createContext<ReactFlowState>();
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ const initialState: ReactFlowStore = {
|
|||||||
translateExtent: infiniteExtent,
|
translateExtent: infiniteExtent,
|
||||||
nodeExtent: infiniteExtent,
|
nodeExtent: infiniteExtent,
|
||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
selectionActive: false,
|
userSelectionActive: false,
|
||||||
userSelectionRect: {
|
userSelectionRect: {
|
||||||
startX: 0,
|
startX: 0,
|
||||||
startY: 0,
|
startY: 0,
|
||||||
@@ -181,74 +181,6 @@ const createStore = () =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setUserSelection: (mousePos: XYPosition) => {
|
|
||||||
set({
|
|
||||||
selectionActive: true,
|
|
||||||
userSelectionRect: {
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
startX: mousePos.x,
|
|
||||||
startY: mousePos.y,
|
|
||||||
x: mousePos.x,
|
|
||||||
y: mousePos.y,
|
|
||||||
draw: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateUserSelection: (mousePos: XYPosition) => {
|
|
||||||
const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange } = get();
|
|
||||||
const startX = userSelectionRect.startX ?? 0;
|
|
||||||
const startY = userSelectionRect.startY ?? 0;
|
|
||||||
|
|
||||||
const nextUserSelectRect = {
|
|
||||||
...userSelectionRect,
|
|
||||||
x: mousePos.x < startX ? mousePos.x : userSelectionRect.x,
|
|
||||||
y: mousePos.y < startY ? mousePos.y : userSelectionRect.y,
|
|
||||||
width: Math.abs(mousePos.x - startX),
|
|
||||||
height: Math.abs(mousePos.y - startY),
|
|
||||||
};
|
|
||||||
|
|
||||||
// @TODO: work with nodeInternals instead of converting it to an array
|
|
||||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
|
||||||
const selectedNodes = getNodesInside(nodes, nextUserSelectRect, transform, false, true);
|
|
||||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
|
||||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
|
||||||
|
|
||||||
onNodesChange?.(
|
|
||||||
nodes.map((n) => createNodeOrEdgeSelectionChange(selectedNodeIds.includes(n.id))(n)) as NodeChange[]
|
|
||||||
);
|
|
||||||
onEdgesChange?.(
|
|
||||||
edges.map((e) => createNodeOrEdgeSelectionChange(selectedEdgeIds.includes(e.id))(e)) as EdgeChange[]
|
|
||||||
);
|
|
||||||
|
|
||||||
set({
|
|
||||||
userSelectionRect: nextUserSelectRect,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
unsetUserSelection: () => {
|
|
||||||
const { userSelectionRect, nodeInternals } = get();
|
|
||||||
// @TODO: work with nodeInternals instead of converting it to an array
|
|
||||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
|
||||||
const selectedNodes = nodes.filter((node) => node.selected);
|
|
||||||
|
|
||||||
const stateUpdate = {
|
|
||||||
selectionActive: false,
|
|
||||||
userSelectionRect: {
|
|
||||||
...userSelectionRect,
|
|
||||||
draw: false,
|
|
||||||
},
|
|
||||||
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
|
|
||||||
nodesSelectionActive: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (selectedNodes && selectedNodes.length > 0) {
|
|
||||||
const selectedNodesBbox = getRectOfNodes(selectedNodes);
|
|
||||||
stateUpdate.selectedNodesBbox = selectedNodesBbox;
|
|
||||||
stateUpdate.nodesSelectionActive = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
set(stateUpdate);
|
|
||||||
},
|
|
||||||
addSelectedNodes: (selectedNodeIds: string[]) => {
|
addSelectedNodes: (selectedNodeIds: string[]) => {
|
||||||
const { multiSelectionActive, onNodesChange, nodeInternals } = get();
|
const { multiSelectionActive, onNodesChange, nodeInternals } = get();
|
||||||
// @TODO: work with nodeInternals instead of converting it to an array
|
// @TODO: work with nodeInternals instead of converting it to an array
|
||||||
@@ -256,11 +188,9 @@ const createStore = () =>
|
|||||||
let changedNodes;
|
let changedNodes;
|
||||||
|
|
||||||
if (multiSelectionActive) {
|
if (multiSelectionActive) {
|
||||||
changedNodes = selectedNodeIds.map(createNodeOrEdgeSelectionChange(true));
|
changedNodes = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
|
||||||
} else {
|
} else {
|
||||||
changedNodes = nodes.map((node) =>
|
changedNodes = getSelectionChanges(nodes, selectedNodeIds);
|
||||||
createNodeOrEdgeSelectionChange(selectedNodeIds.some((nodeId) => nodeId === node.id))(node.id)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedNodes.length) {
|
if (changedNodes.length) {
|
||||||
@@ -273,11 +203,9 @@ const createStore = () =>
|
|||||||
let changedEdges;
|
let changedEdges;
|
||||||
|
|
||||||
if (multiSelectionActive) {
|
if (multiSelectionActive) {
|
||||||
changedEdges = selectedEdgeIds.map(createNodeOrEdgeSelectionChange(true));
|
changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
|
||||||
} else {
|
} else {
|
||||||
changedEdges = edges.map((edge) =>
|
changedEdges = getSelectionChanges(edges, selectedEdgeIds);
|
||||||
createNodeOrEdgeSelectionChange(selectedEdgeIds.some((edgeId) => edgeId === edge.id))(edge.id)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedEdges.length) {
|
if (changedEdges.length) {
|
||||||
@@ -291,9 +219,9 @@ const createStore = () =>
|
|||||||
|
|
||||||
const nodesToUnselect = nodes.map((n) => {
|
const nodesToUnselect = nodes.map((n) => {
|
||||||
n.selected = false;
|
n.selected = false;
|
||||||
return createNodeOrEdgeSelectionChange(false)(n);
|
return createSelectionChange(n.id, false);
|
||||||
}) as NodeChange[];
|
}) as NodeChange[];
|
||||||
const edgesToUnselect = edges.map(createNodeOrEdgeSelectionChange(false)) as EdgeChange[];
|
const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeChange[];
|
||||||
|
|
||||||
if (nodesToUnselect.length) {
|
if (nodesToUnselect.length) {
|
||||||
onNodesChange?.(nodesToUnselect);
|
onNodesChange?.(nodesToUnselect);
|
||||||
@@ -332,8 +260,8 @@ const createStore = () =>
|
|||||||
const { nodeInternals, edges, onNodesChange, onEdgesChange } = get();
|
const { nodeInternals, edges, onNodesChange, onEdgesChange } = get();
|
||||||
// @TODO: work with nodeInternals instead of converting it to an array
|
// @TODO: work with nodeInternals instead of converting it to an array
|
||||||
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
const nodes = Array.from(nodeInternals).map(([_, node]) => node);
|
||||||
const nodesToUnselect = nodes.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false));
|
const nodesToUnselect = nodes.filter((e) => e.selected).map((n) => createSelectionChange(n.id, false));
|
||||||
const edgesToUnselect = edges.filter((e) => e.selected).map(createNodeOrEdgeSelectionChange(false));
|
const edgesToUnselect = edges.filter((e) => e.selected).map((e) => createSelectionChange(e.id, false));
|
||||||
|
|
||||||
if (nodesToUnselect.length) {
|
if (nodesToUnselect.length) {
|
||||||
onNodesChange?.(nodesToUnselect as NodeChange[]);
|
onNodesChange?.(nodesToUnselect as NodeChange[]);
|
||||||
@@ -354,7 +282,8 @@ const createStore = () =>
|
|||||||
nodeInternals: new Map(nodeInternals),
|
nodeInternals: new Map(nodeInternals),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
unsetNodesSelection: () => set({ nodesSelectionActive: false }),
|
setUserSelectionActive: (userSelectionActive: boolean) => set({ userSelectionActive }),
|
||||||
|
setNodesSelectionActive: (nodesSelectionActive: boolean) => set({ nodesSelectionActive }),
|
||||||
updateTransform: (transform: Transform) => set({ transform }),
|
updateTransform: (transform: Transform) => set({ transform }),
|
||||||
updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }),
|
updateSize: (size: Dimensions) => set({ width: size.width || 500, height: size.height || 500 }),
|
||||||
setOnConnect: (onConnect: OnConnect) => set({ onConnect }),
|
setOnConnect: (onConnect: OnConnect) => set({ onConnect }),
|
||||||
|
|||||||
+1
-7
@@ -1,4 +1,4 @@
|
|||||||
import { Node, Edge, NodeInternals, NodeInternalsItem, XYZPosition } from '../types';
|
import { Node, NodeInternals, NodeInternalsItem, XYZPosition } from '../types';
|
||||||
import { isNumeric } from '../utils';
|
import { isNumeric } from '../utils';
|
||||||
|
|
||||||
type ParentNodes = Record<string, boolean>;
|
type ParentNodes = Record<string, boolean>;
|
||||||
@@ -104,9 +104,3 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
|
|||||||
|
|
||||||
return nextNodeInternals;
|
return nextNodeInternals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createNodeOrEdgeSelectionChange = (selected: boolean) => (item: Node | Edge | string) => ({
|
|
||||||
id: typeof item === 'string' ? item : item.id,
|
|
||||||
type: 'select',
|
|
||||||
selected,
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export type ReactFlowStore = {
|
|||||||
nodeExtent: CoordinateExtent;
|
nodeExtent: CoordinateExtent;
|
||||||
|
|
||||||
nodesSelectionActive: boolean;
|
nodesSelectionActive: boolean;
|
||||||
selectionActive: boolean;
|
userSelectionActive: boolean;
|
||||||
|
|
||||||
userSelectionRect: SelectionRect;
|
userSelectionRect: SelectionRect;
|
||||||
|
|
||||||
@@ -176,10 +176,8 @@ export type ReactFlowActions = {
|
|||||||
setEdges: (edges: Edge[]) => void;
|
setEdges: (edges: Edge[]) => void;
|
||||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||||
updateNodePosition: (update: NodeDiffUpdate) => void;
|
updateNodePosition: (update: NodeDiffUpdate) => void;
|
||||||
setUserSelection: (mousePos: XYPosition) => void;
|
setNodesSelectionActive: (isActive: boolean) => void;
|
||||||
updateUserSelection: (mousePos: XYPosition) => void;
|
setUserSelectionActive: (isActive: boolean) => void;
|
||||||
unsetUserSelection: () => void;
|
|
||||||
unsetNodesSelection: () => void;
|
|
||||||
resetSelectedElements: () => void;
|
resetSelectedElements: () => void;
|
||||||
unselectNodesAndEdges: () => void;
|
unselectNodesAndEdges: () => void;
|
||||||
addSelectedNodes: (nodeIds: string[]) => void;
|
addSelectedNodes: (nodeIds: string[]) => void;
|
||||||
|
|||||||
@@ -49,3 +49,23 @@ export function applyNodeChanges(changes: NodeChange[], nodes: Node[]): Node[] {
|
|||||||
export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] {
|
export function applyEdgeChanges(changes: EdgeChange[], edges: Edge[]): Edge[] {
|
||||||
return applyChanges(changes, edges) as Edge[];
|
return applyChanges(changes, edges) as Edge[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const createSelectionChange = (id: string, selected: boolean) => ({
|
||||||
|
id,
|
||||||
|
type: 'select',
|
||||||
|
selected,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getSelectionChanges(items: any[], selectedIds: string[]) {
|
||||||
|
return items.reduce((res, item) => {
|
||||||
|
const willBeSelected = selectedIds.includes(item.id);
|
||||||
|
|
||||||
|
if (!item.selected && willBeSelected) {
|
||||||
|
res.push(createSelectionChange(item.id, true));
|
||||||
|
} else if (item.selected && !willBeSelected) {
|
||||||
|
res.push(createSelectionChange(item.id, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user