diff --git a/packages/core/src/components/UserSelection/SelectionBox.tsx b/packages/core/src/components/UserSelection/SelectionBox.tsx new file mode 100644 index 00000000..bfd63ea1 --- /dev/null +++ b/packages/core/src/components/UserSelection/SelectionBox.tsx @@ -0,0 +1,30 @@ +import shallow from 'zustand/shallow'; +import { useStore } from '../../hooks/useStore'; +import { ReactFlowState } from '../../types'; + +const selector = (s: ReactFlowState) => ({ + userSelectionActive: s.userSelectionActive, + userSelectionRect: s.userSelectionRect, +}); + +function SelectionBox() { + const { userSelectionActive, userSelectionRect } = useStore(selector, shallow); + const showSelectionBox = userSelectionActive && userSelectionRect; + + if (!showSelectionBox) { + return null; + } + + return ( +
+ ); +} + +export default SelectionBox; diff --git a/packages/core/src/components/UserSelection/index.tsx b/packages/core/src/components/UserSelection/index.tsx index 9b380967..21d07a2c 100644 --- a/packages/core/src/components/UserSelection/index.tsx +++ b/packages/core/src/components/UserSelection/index.tsx @@ -2,23 +2,17 @@ * The user selection rectangle gets displayed when a user drags the mouse while pressing shift */ -import { memo, useState, useRef, MouseEvent as ReactMouseEvent } from 'react'; +import { memo, useRef, MouseEvent as ReactMouseEvent } from 'react'; import shallow from 'zustand/shallow'; import cc from 'classcat'; +import SelectionBox from './SelectionBox'; import { containerStyle } from '../../styles'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { getSelectionChanges } from '../../utils/changes'; import { getConnectedEdges, getNodesInside } from '../../utils/graph'; import { SelectionMode } from '../../types'; -import type { XYPosition, ReactFlowState, NodeChange, EdgeChange, Rect } from '../../types'; - -type SelectionRect = Rect & { - startX: number; - startY: number; -}; - -type EventHandlers = { [key: string]: React.MouseEventHandler | React.WheelEventHandler | undefined }; +import type { XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; type UserSelectionProps = { isSelectionMode: boolean; @@ -54,12 +48,6 @@ const wrapHandler = ( }; }; -const wrapHandlers = ( - handlers: EventHandlers, - containerRef: React.MutableRefObject -): EventHandlers => - Object.keys(handlers).reduce((hls, key) => ({ ...hls, [key]: wrapHandler(handlers[key], containerRef) }), {}); - const selector = (s: ReactFlowState) => ({ userSelectionActive: s.userSelectionActive, elementsSelectable: s.elementsSelectable, @@ -86,13 +74,10 @@ const UserSelection = memo( const prevSelectedNodesCount = useRef(0); const prevSelectedEdgesCount = useRef(0); const containerBounds = useRef(); - const [userSelectionRect, setUserSelectionRect] = useState(null); const { userSelectionActive, elementsSelectable, paneDragging } = useStore(selector, shallow); const resetUserSelection = () => { - setUserSelectionRect(null); - - store.setState({ userSelectionActive: false }); + store.setState({ userSelectionActive: false, userSelectionRect: null }); prevSelectedNodesCount.current = 0; prevSelectedEdgesCount.current = 0; @@ -116,30 +101,39 @@ const UserSelection = memo( const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; const onMouseDown = (event: ReactMouseEvent): void => { - if (!elementsSelectable || !isSelectionMode || event.button !== 0 || event.target !== container.current) { + const { resetSelectedElements, domNode } = store.getState(); + if ( + !elementsSelectable || + !isSelectionMode || + event.button !== 0 || + event.target !== container.current || + !domNode + ) { return; } - store.getState().resetSelectedElements(); + containerBounds.current = domNode.getBoundingClientRect(); + const { x, y } = getMousePosition(event, containerBounds.current!); - const reactFlowNode = (event.target as Element).closest('.react-flow')!; - containerBounds.current = reactFlowNode.getBoundingClientRect(); + resetSelectedElements(); - const mousePos = getMousePosition(event, containerBounds.current!); - - setUserSelectionRect({ - width: 0, - height: 0, - startX: mousePos.x, - startY: mousePos.y, - x: mousePos.x, - y: mousePos.y, + store.setState({ + userSelectionRect: { + width: 0, + height: 0, + startX: x, + startY: y, + x, + y, + }, }); onSelectionStart?.(event); }; const onMouseMove = (event: ReactMouseEvent): void => { + const { userSelectionRect, nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin, getNodes } = + store.getState(); if (!isSelectionMode || !containerBounds.current || !userSelectionRect) { return; } @@ -158,7 +152,6 @@ const UserSelection = memo( height: Math.abs(mousePos.y - startY), }; - const { nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin, getNodes } = store.getState(); const nodes = getNodes(); const selectedNodes = getNodesInside( nodeInternals, @@ -187,10 +180,13 @@ const UserSelection = memo( } } - setUserSelectionRect(nextUserSelectRect); + store.setState({ + userSelectionRect: nextUserSelectRect, + }); }; const onMouseUp = (event: ReactMouseEvent) => { + const { userSelectionRect } = store.getState(); // We only want to trigger click functions when in selection mode if // the user did not move the mouse. if (!userSelectionActive && userSelectionRect && event.target === container.current) { @@ -200,7 +196,6 @@ const UserSelection = memo( store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); resetUserSelection(); - onSelectionEnd?.(event); }; @@ -209,29 +204,11 @@ const UserSelection = memo( store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); onSelectionEnd?.(event); } + resetUserSelection(); }; - const eventHandlers = - elementsSelectable && (isSelectionMode || userSelectionActive) - ? { - ...wrapHandlers({ onContextMenu, onWheel }, container), - onMouseDown, - onMouseMove, - onMouseUp, - onMouseLeave, - } - : wrapHandlers( - { - onClick, - onContextMenu, - onWheel, - onMouseEnter: onPaneMouseEnter, - onMouseMove: onPaneMouseMove, - onMouseLeave: onPaneMouseLeave, - }, - container - ); + const hasActiveSelection = elementsSelectable && (isSelectionMode || userSelectionActive); return (
{children} - {userSelectionActive && userSelectionRect && ( -
- )} +
); } diff --git a/packages/core/src/store/initialState.ts b/packages/core/src/store/initialState.ts index ab918e65..6a92b777 100644 --- a/packages/core/src/store/initialState.ts +++ b/packages/core/src/store/initialState.ts @@ -26,6 +26,7 @@ const initialState: ReactFlowStore = { nodeExtent: infiniteExtent, nodesSelectionActive: false, userSelectionActive: false, + userSelectionRect: null, connectionNodeId: null, connectionHandleId: null, connectionHandleType: 'source', diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 52ded366..b083964f 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -155,6 +155,7 @@ export type ReactFlowStore = { nodesSelectionActive: boolean; userSelectionActive: boolean; + userSelectionRect: SelectionRect | null; connectionNodeId: string | null; connectionHandleId: string | null; @@ -254,3 +255,8 @@ export enum SelectionMode { Overlap = 'overlap', Contained = 'contained', } + +export type SelectionRect = Rect & { + startX: number; + startY: number; +};