From 43a16e61e6da422d67b5f14fbf89c10d1c144d10 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 19 Dec 2022 12:22:42 +0100 Subject: [PATCH] chore(selection): cleanup --- .../vite-app/src/examples/Subflow/index.tsx | 6 +- .../core/src/components/Nodes/wrapNode.tsx | 13 +- .../src/components/NodesSelection/index.tsx | 10 +- .../components/UserSelection/SelectionBox.tsx | 30 --- .../src/components/UserSelection/index.tsx | 252 ++---------------- .../core/src/container/FlowRenderer/index.tsx | 6 +- packages/core/src/container/Pane/index.tsx | 241 +++++++++++++++++ .../core/src/hooks/useUpdateNodePositions.ts | 15 +- 8 files changed, 288 insertions(+), 285 deletions(-) delete mode 100644 packages/core/src/components/UserSelection/SelectionBox.tsx create mode 100644 packages/core/src/container/Pane/index.tsx diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx index 55aea018..db021f52 100644 --- a/examples/vite-app/src/examples/Subflow/index.tsx +++ b/examples/vite-app/src/examples/Subflow/index.tsx @@ -11,7 +11,7 @@ import ReactFlow, { Controls, MiniMap, Background, - NodeOrigin, + Panel, } from 'reactflow'; import DebugNode from './DebugNode'; @@ -211,7 +211,7 @@ const Subflow = () => { -
+ @@ -225,7 +225,7 @@ const Subflow = () => { toggleChildNodes -
+ ); }; diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx index 70b36ddb..f56f117b 100644 --- a/packages/core/src/components/Nodes/wrapNode.tsx +++ b/packages/core/src/components/Nodes/wrapNode.tsx @@ -88,8 +88,6 @@ export default (NodeComponent: ComponentType) => { return; } - const { snapGrid, snapToGrid } = store.getState(); - if (elementSelectionKeys.includes(event.key) && isSelectable) { const unselect = event.key === 'Escape'; if (unselect) { @@ -112,15 +110,10 @@ export default (NodeComponent: ComponentType) => { .toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`, }); - // by default a node moves 5px on each key press, or 20px if shift is pressed - // if snap grid is enabled, we use that for the velocity. - const xVelo = snapToGrid ? snapGrid[0] : 5; - const yVelo = snapToGrid ? snapGrid[1] : 5; - const factor = event.shiftKey ? 4 : 1; - updatePositions({ - x: arrowKeyDiffs[event.key].x * xVelo * factor, - y: arrowKeyDiffs[event.key].y * yVelo * factor, + x: arrowKeyDiffs[event.key].x, + y: arrowKeyDiffs[event.key].y, + isShiftPressed: event.shiftKey, }); } }; diff --git a/packages/core/src/components/NodesSelection/index.tsx b/packages/core/src/components/NodesSelection/index.tsx index cff39b5d..745a0550 100644 --- a/packages/core/src/components/NodesSelection/index.tsx +++ b/packages/core/src/components/NodesSelection/index.tsx @@ -41,7 +41,9 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboar useEffect(() => { if (!disableKeyboardA11y) { - nodeRef.current?.focus(); + nodeRef.current?.focus({ + preventScroll: true, + }); } }, [disableKeyboardA11y]); @@ -65,7 +67,11 @@ function NodesSelection({ onSelectionContextMenu, noPanClassName, disableKeyboar const onKeyDown = (event: KeyboardEvent) => { if (Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) { - updatePositions(arrowKeyDiffs[event.key]); + updatePositions({ + x: arrowKeyDiffs[event.key].x, + y: arrowKeyDiffs[event.key].y, + isShiftPressed: event.shiftKey, + }); } }; diff --git a/packages/core/src/components/UserSelection/SelectionBox.tsx b/packages/core/src/components/UserSelection/SelectionBox.tsx deleted file mode 100644 index bfd63ea1..00000000 --- a/packages/core/src/components/UserSelection/SelectionBox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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 26ba621c..34d16a36 100644 --- a/packages/core/src/components/UserSelection/index.tsx +++ b/packages/core/src/components/UserSelection/index.tsx @@ -1,245 +1,31 @@ -/** - * The user selection rectangle gets displayed when a user drags the mouse while pressing shift - */ - -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 { ReactFlowProps, SelectionMode } from '../../types'; -import type { XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; - -type UserSelectionProps = { - isSelecting: boolean; - children: React.ReactNode; -} & Partial< - Pick< - ReactFlowProps, - | 'selectionMode' - | 'panOnDrag' - | 'onSelectionStart' - | 'onSelectionEnd' - | 'onPaneClick' - | 'onPaneContextMenu' - | 'onPaneScroll' - | 'onPaneMouseEnter' - | 'onPaneMouseMove' - | 'onPaneMouseLeave' - > ->; - -function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition { - return { - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top, - }; -} - -const wrapHandler = ( - handler: React.MouseEventHandler | undefined, - containerRef: React.MutableRefObject -): React.MouseEventHandler => { - return (event: ReactMouseEvent) => { - if (event.target !== containerRef.current) { - return; - } - handler?.(event); - }; -}; +import { useStore } from '../../hooks/useStore'; +import type { ReactFlowState } from '../../types'; const selector = (s: ReactFlowState) => ({ userSelectionActive: s.userSelectionActive, - elementsSelectable: s.elementsSelectable, - paneDragging: s.paneDragging, + userSelectionRect: s.userSelectionRect, }); -const UserSelection = memo( - ({ - isSelecting, - selectionMode = SelectionMode.Full, - panOnDrag, - onSelectionStart, - onSelectionEnd, - onPaneClick, - onPaneContextMenu, - onPaneScroll, - onPaneMouseEnter, - onPaneMouseMove, - onPaneMouseLeave, - children, - }: UserSelectionProps) => { - const container = useRef(null); - const store = useStoreApi(); - const prevSelectedNodesCount = useRef(0); - const prevSelectedEdgesCount = useRef(0); - const containerBounds = useRef(); - const { userSelectionActive, elementsSelectable, paneDragging } = useStore(selector, shallow); +function UserSelection() { + const { userSelectionActive, userSelectionRect } = useStore(selector, shallow); + const showSelectionBox = userSelectionActive && userSelectionRect; - const resetUserSelection = () => { - store.setState({ userSelectionActive: false, userSelectionRect: null }); - - prevSelectedNodesCount.current = 0; - prevSelectedEdgesCount.current = 0; - }; - - const onClick = (event: ReactMouseEvent) => { - onPaneClick?.(event); - store.getState().resetSelectedElements(); - store.setState({ nodesSelectionActive: false }); - }; - - const onContextMenu = (event: ReactMouseEvent) => { - if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) { - event.preventDefault(); - return; - } - - onPaneContextMenu?.(event); - }; - - const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; - - const onMouseDown = (event: ReactMouseEvent): void => { - const { resetSelectedElements, domNode } = store.getState(); - containerBounds.current = domNode?.getBoundingClientRect(); - - if ( - !elementsSelectable || - !isSelecting || - event.button !== 0 || - event.target !== container.current || - !containerBounds.current - ) { - return; - } - - const { x, y } = getMousePosition(event, containerBounds.current); - - resetSelectedElements(); - - 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 (!isSelecting || !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), - }; - - const nodes = getNodes(); - const selectedNodes = getNodesInside( - nodeInternals, - nextUserSelectRect, - transform, - selectionMode === SelectionMode.Partial, - 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); - } - } - - if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { - prevSelectedEdgesCount.current = selectedEdgeIds.length; - const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; - if (changes.length) { - onEdgesChange?.(changes); - } - } - - 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) { - onClick?.(event); - } - - store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); - - resetUserSelection(); - onSelectionEnd?.(event); - }; - - const onMouseLeave = (event: ReactMouseEvent) => { - if (userSelectionActive) { - store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); - onSelectionEnd?.(event); - } - - resetUserSelection(); - }; - - const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive); - - return ( -
- {children} - -
- ); + if (!showSelectionBox) { + return null; } -); -UserSelection.displayName = 'UserSelection'; + return ( +
+ ); +} export default UserSelection; diff --git a/packages/core/src/container/FlowRenderer/index.tsx b/packages/core/src/container/FlowRenderer/index.tsx index 37bd44d9..a8e9dc69 100644 --- a/packages/core/src/container/FlowRenderer/index.tsx +++ b/packages/core/src/container/FlowRenderer/index.tsx @@ -6,7 +6,7 @@ import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useKeyPress from '../../hooks/useKeyPress'; import { GraphViewProps } from '../GraphView'; import ZoomPane from '../ZoomPane'; -import UserSelection from '../../components/UserSelection'; +import Pane from '../Pane'; import NodesSelection from '../../components/NodesSelection'; import type { ReactFlowState } from '../../types'; @@ -100,7 +100,7 @@ const FlowRenderer = ({ noWheelClassName={noWheelClassName} noPanClassName={noPanClassName} > - )} - + ); }; diff --git a/packages/core/src/container/Pane/index.tsx b/packages/core/src/container/Pane/index.tsx new file mode 100644 index 00000000..755e0c04 --- /dev/null +++ b/packages/core/src/container/Pane/index.tsx @@ -0,0 +1,241 @@ +/** + * The user selection rectangle gets displayed when a user drags the mouse while pressing shift + */ + +import { memo, useRef, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; +import shallow from 'zustand/shallow'; +import cc from 'classcat'; + +import UserSelection from '../../components/UserSelection'; +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 { ReactFlowProps, XYPosition, ReactFlowState, NodeChange, EdgeChange } from '../../types'; + +type PaneProps = { + isSelecting: boolean; + children: ReactNode; +} & Partial< + Pick< + ReactFlowProps, + | 'selectionMode' + | 'panOnDrag' + | 'onSelectionStart' + | 'onSelectionEnd' + | 'onPaneClick' + | 'onPaneContextMenu' + | 'onPaneScroll' + | 'onPaneMouseEnter' + | 'onPaneMouseMove' + | 'onPaneMouseLeave' + > +>; + +function getMousePosition(event: ReactMouseEvent, containerBounds: DOMRect): XYPosition { + return { + x: event.clientX - containerBounds.left, + y: event.clientY - containerBounds.top, + }; +} + +const wrapHandler = ( + handler: React.MouseEventHandler | undefined, + containerRef: React.MutableRefObject +): React.MouseEventHandler => { + return (event: ReactMouseEvent) => { + if (event.target !== containerRef.current) { + return; + } + handler?.(event); + }; +}; + +const selector = (s: ReactFlowState) => ({ + userSelectionActive: s.userSelectionActive, + elementsSelectable: s.elementsSelectable, + dragging: s.paneDragging, +}); + +const Pane = memo( + ({ + isSelecting, + selectionMode = SelectionMode.Full, + panOnDrag, + onSelectionStart, + onSelectionEnd, + onPaneClick, + onPaneContextMenu, + onPaneScroll, + onPaneMouseEnter, + onPaneMouseMove, + onPaneMouseLeave, + children, + }: PaneProps) => { + const container = useRef(null); + const store = useStoreApi(); + const prevSelectedNodesCount = useRef(0); + const prevSelectedEdgesCount = useRef(0); + const containerBounds = useRef(); + const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow); + + const resetUserSelection = () => { + store.setState({ userSelectionActive: false, userSelectionRect: null }); + + prevSelectedNodesCount.current = 0; + prevSelectedEdgesCount.current = 0; + }; + + const onClick = (event: ReactMouseEvent) => { + onPaneClick?.(event); + store.getState().resetSelectedElements(); + store.setState({ nodesSelectionActive: false }); + }; + + const onContextMenu = (event: ReactMouseEvent) => { + if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) { + event.preventDefault(); + return; + } + + onPaneContextMenu?.(event); + }; + + const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined; + + const onMouseDown = (event: ReactMouseEvent): void => { + const { resetSelectedElements, domNode } = store.getState(); + containerBounds.current = domNode?.getBoundingClientRect(); + + if ( + !elementsSelectable || + !isSelecting || + event.button !== 0 || + event.target !== container.current || + !containerBounds.current + ) { + return; + } + + const { x, y } = getMousePosition(event, containerBounds.current); + + resetSelectedElements(); + + 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 (!isSelecting || !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), + }; + + const nodes = getNodes(); + const selectedNodes = getNodesInside( + nodeInternals, + nextUserSelectRect, + transform, + selectionMode === SelectionMode.Partial, + 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); + } + } + + if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) { + prevSelectedEdgesCount.current = selectedEdgeIds.length; + const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[]; + if (changes.length) { + onEdgesChange?.(changes); + } + } + + 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) { + onClick?.(event); + } + + store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); + + resetUserSelection(); + onSelectionEnd?.(event); + }; + + const onMouseLeave = (event: ReactMouseEvent) => { + if (userSelectionActive) { + store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 }); + onSelectionEnd?.(event); + } + + resetUserSelection(); + }; + + const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive); + + return ( +
+ {children} + +
+ ); + } +); + +Pane.displayName = 'Pane'; + +export default Pane; diff --git a/packages/core/src/hooks/useUpdateNodePositions.ts b/packages/core/src/hooks/useUpdateNodePositions.ts index 56e55925..b20ea4d1 100644 --- a/packages/core/src/hooks/useUpdateNodePositions.ts +++ b/packages/core/src/hooks/useUpdateNodePositions.ts @@ -2,18 +2,25 @@ import { useCallback } from 'react'; import { useStoreApi } from '../hooks/useStore'; import { calcNextPosition } from './useDrag/utils'; -import type { XYPosition } from '../types'; function useUpdateNodePositions() { const store = useStoreApi(); - const updatePositions = useCallback((positionDiff: XYPosition) => { + const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { const { nodeInternals, nodeExtent, updateNodePositions, getNodes, snapToGrid, snapGrid } = store.getState(); const selectedNodes = getNodes().filter((n) => n.selected); + // by default a node moves 5px on each key press, or 20px if shift is pressed + // if snap grid is enabled, we use that for the velocity. + const xVelo = snapToGrid ? snapGrid[0] : 5; + const yVelo = snapToGrid ? snapGrid[1] : 5; + const factor = params.isShiftPressed ? 4 : 1; + + const positionDiffX = params.x * xVelo * factor; + const positionDiffY = params.y * yVelo * factor; const nodeUpdates = selectedNodes.map((n) => { if (n.positionAbsolute) { - const nextPosition = { x: n.positionAbsolute.x + positionDiff.x, y: n.positionAbsolute.y + positionDiff.y }; + const nextPosition = { x: n.positionAbsolute.x + positionDiffX, y: n.positionAbsolute.y + positionDiffY }; if (snapToGrid) { nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]); @@ -29,7 +36,7 @@ function useUpdateNodePositions() { return n; }); - updateNodePositions(nodeUpdates, true, true); + updateNodePositions(nodeUpdates, true, false); }, []); return updatePositions;