diff --git a/packages/core/src/components/UserSelection/index.tsx b/packages/core/src/components/UserSelection/index.tsx index 8e691175..693ba368 100644 --- a/packages/core/src/components/UserSelection/index.tsx +++ b/packages/core/src/components/UserSelection/index.tsx @@ -19,6 +19,8 @@ type EventHandlers = { [key: string]: React.MouseEventHandler | React.WheelEvent type UserSelectionProps = { isSelectionMode: boolean; + onSelectionStart?: (e: React.MouseEvent) => void; + onSelectionEnd?: (e: React.MouseEvent) => void; onClick?: (e: React.MouseEvent) => void; onContextMenu?: (e: React.MouseEvent) => void; onWheel?: (e: React.WheelEvent) => void; @@ -55,144 +57,159 @@ const selector = (s: ReactFlowState) => ({ elementsSelectable: s.elementsSelectable, }); -const UserSelection = memo(({ isSelectionMode, onClick, onContextMenu, onWheel, children }: UserSelectionProps) => { - const container = useRef(null); - const store = useStoreApi(); - const prevSelectedNodesCount = useRef(0); - const prevSelectedEdgesCount = useRef(0); - const containerBounds = useRef(); - const [userSelectionRect, setUserSelectionRect] = useState(null); - const { userSelectionActive, elementsSelectable } = useStore(selector, shallow); +const UserSelection = memo( + ({ + isSelectionMode, + onSelectionStart, + onSelectionEnd, + onClick, + onContextMenu, + onWheel, + children, + }: UserSelectionProps) => { + const container = useRef(null); + const store = useStoreApi(); + const prevSelectedNodesCount = useRef(0); + const prevSelectedEdgesCount = useRef(0); + const containerBounds = useRef(); + const [userSelectionRect, setUserSelectionRect] = useState(null); + const { userSelectionActive, elementsSelectable } = useStore(selector, shallow); - const resetUserSelection = () => { - setUserSelectionRect(null); + const resetUserSelection = () => { + setUserSelectionRect(null); - store.setState({ userSelectionActive: false }); + store.setState({ userSelectionActive: false }); - prevSelectedNodesCount.current = 0; - prevSelectedEdgesCount.current = 0; - }; - - const onMouseDown = (event: React.MouseEvent): void => { - if (!elementsSelectable || !isSelectionMode || event.button !== 0 || event.target !== container.current) { - return; - } - - store.getState().resetSelectedElements(); - - 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, - }); - }; - - const onMouseMove = (event: React.MouseEvent): void => { - if (!isSelectionMode || !containerBounds.current || !userSelectionRect) { - return; - } - - store.setState({ userSelectionActive: true, nodesSelectionActive: false }); - - 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 : startX, - y: mousePos.y < startY ? mousePos.y : startY, - width: Math.abs(mousePos.x - startX), - height: Math.abs(mousePos.y - startY), - draw: true, + prevSelectedNodesCount.current = 0; + prevSelectedEdgesCount.current = 0; }; - const { nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin } = store.getState(); - const nodes = Array.from(nodeInternals.values()); - const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, true, true, nodeOrigin); - 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); + const onMouseDown = (event: React.MouseEvent): void => { + if (!elementsSelectable || !isSelectionMode || event.button !== 0 || event.target !== container.current) { + return; } - } - if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { - prevSelectedEdgesCount.current = selectedEdgeIds.length; - const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; - if (changes.length) { - onEdgesChange?.(changes); + store.getState().resetSelectedElements(); + + 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, + }); + + onSelectionStart?.(event); + }; + + const onMouseMove = (event: React.MouseEvent): void => { + if (!isSelectionMode || !containerBounds.current || !userSelectionRect) { + return; } - } - setUserSelectionRect(nextUserSelectRect); - }; + store.setState({ userSelectionActive: true, nodesSelectionActive: false }); - const onMouseUp = (event: React.MouseEvent) => { - // 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) { - onClick?.(event); - } + const mousePos = getMousePosition(event, containerBounds.current!); + const startX = userSelectionRect.startX ?? 0; + const startY = userSelectionRect.startY ?? 0; - store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); + const nextUserSelectRect = { + ...userSelectionRect, + x: mousePos.x < startX ? mousePos.x : startX, + y: mousePos.y < startY ? mousePos.y : startY, + width: Math.abs(mousePos.x - startX), + height: Math.abs(mousePos.y - startY), + draw: true, + }; - resetUserSelection(); - }; + const { nodeInternals, edges, transform, onNodesChange, onEdgesChange, nodeOrigin } = store.getState(); + const nodes = Array.from(nodeInternals.values()); + const selectedNodes = getNodesInside(nodeInternals, nextUserSelectRect, transform, true, true, nodeOrigin); + const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id); + const selectedNodeIds = selectedNodes.map((n) => n.id); - const onMouseLeave = () => { - if (userSelectionActive) { - store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); - } - resetUserSelection(); - }; - - const eventHandlers = - elementsSelectable && (isSelectionMode || userSelectionActive) - ? { - ...wrapHandlers({ onContextMenu, onWheel }, container), - onMouseDown, - onMouseMove, - onMouseUp, - onMouseLeave, + if (prevSelectedNodesCount.current !== selectedNodeIds.length) { + prevSelectedNodesCount.current = selectedNodeIds.length; + const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[]; + if (changes.length) { + onNodesChange?.(changes); } - : wrapHandlers( - { - onClick, - onContextMenu, - onWheel, - }, - container - ); + } - return ( -
- {children} - {userSelectionActive && userSelectionRect && ( -
- )} -
- ); -}); + 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 = (event: React.MouseEvent) => { + // 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) { + onClick?.(event); + } + + store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); + + resetUserSelection(); + + onSelectionEnd?.(event); + }; + + const onMouseLeave = (event: React.MouseEvent) => { + if (userSelectionActive) { + 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, + }, + container + ); + + return ( +
+ {children} + {userSelectionActive && userSelectionRect && ( +
+ )} +
+ ); + } +); UserSelection.displayName = 'UserSelection'; diff --git a/packages/core/src/container/FlowRenderer/index.tsx b/packages/core/src/container/FlowRenderer/index.tsx index d203033b..a5853a09 100644 --- a/packages/core/src/container/FlowRenderer/index.tsx +++ b/packages/core/src/container/FlowRenderer/index.tsx @@ -45,6 +45,8 @@ const FlowRenderer = ({ onMoveEnd, selectionKeyCode, selectBoxOnDrag, + onSelectionStart, + onSelectionEnd, multiSelectionKeyCode, zoomActivationKeyCode, elementsSelectable, @@ -112,6 +114,8 @@ const FlowRenderer = ({ noPanClassName={noPanClassName} > ( onSelectionDrag, onSelectionDragStop, onSelectionContextMenu, + onSelectionStart, + onSelectionEnd, connectionMode = ConnectionMode.Strict, connectionLineType = ConnectionLineType.Bezier, connectionLineStyle, @@ -219,6 +221,8 @@ const ReactFlow = forwardRef( onPaneScroll={onPaneScroll} onPaneContextMenu={onPaneContextMenu} onSelectionContextMenu={onSelectionContextMenu} + onSelectionStart={onSelectionStart} + onSelectionEnd={onSelectionEnd} onEdgeUpdate={onEdgeUpdate} onEdgeContextMenu={onEdgeContextMenu} onEdgeDoubleClick={onEdgeDoubleClick} diff --git a/packages/core/src/types/component-props.ts b/packages/core/src/types/component-props.ts index 9cf390a7..27498ba0 100644 --- a/packages/core/src/types/component-props.ts +++ b/packages/core/src/types/component-props.ts @@ -68,6 +68,8 @@ export type ReactFlowProps = HTMLAttributes & { onSelectionDragStart?: SelectionDragHandler; onSelectionDrag?: SelectionDragHandler; onSelectionDragStop?: SelectionDragHandler; + onSelectionStart?: (event: ReactMouseEvent) => void; + onSelectionEnd?: (event: ReactMouseEvent) => void; onSelectionContextMenu?: (event: ReactMouseEvent, nodes: Node[]) => void; onConnect?: OnConnect; onConnectStart?: OnConnectStart;