diff --git a/src/components/Edges/wrapEdge.tsx b/src/components/Edges/wrapEdge.tsx index 2d682b73..d1e881b2 100644 --- a/src/components/Edges/wrapEdge.tsx +++ b/src/components/Edges/wrapEdge.tsx @@ -33,7 +33,7 @@ export default (EdgeComponent: ComponentType) => { markerEndId, isHidden, }: WrapEdgeProps) => { - const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); + const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const inactive = !elementsSelectable && !onClick; const edgeClasses = cc([ @@ -46,7 +46,7 @@ export default (EdgeComponent: ComponentType) => { const onEdgeClick = useCallback( (event: React.MouseEvent): void => { if (elementsSelectable) { - setSelectedElements({ id, source, target }); + addSelectedElements({ id, source, target }); } if (onClick) { diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 7c22aa36..9a9c89d4 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -38,7 +38,7 @@ export default (NodeComponent: ComponentType) => { isDragging, }: WrapNodeProps) => { const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); - const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); + const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); @@ -94,7 +94,7 @@ export default (NodeComponent: ComponentType) => { if (!isDraggable) { if (isSelectable) { unsetNodesSelection(); - setSelectedElements({ id: node.id, type: node.type } as Node); + addSelectedElements({ id: node.id, type: node.type } as Node); } onClick?.(event, node); @@ -109,7 +109,7 @@ export default (NodeComponent: ComponentType) => { if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); - setSelectedElements({ id: node.id, type: node.type } as Node); + addSelectedElements({ id: node.id, type: node.type } as Node); } }, [node, selectNodesOnDrag, isSelectable, onNodeDragStart] @@ -134,7 +134,7 @@ export default (NodeComponent: ComponentType) => { // Because of that we set dragging to true inside the onDrag handler and handle the click here if (!isDragging) { if (isSelectable && !selectNodesOnDrag) { - setSelectedElements({ id: node.id, type: node.type } as Node); + addSelectedElements({ id: node.id, type: node.type } as Node); } onClick?.(event as MouseEvent, node); diff --git a/src/container/FlowRenderer/index.tsx b/src/container/FlowRenderer/index.tsx index da3f8e1b..0db73284 100644 --- a/src/container/FlowRenderer/index.tsx +++ b/src/container/FlowRenderer/index.tsx @@ -36,6 +36,7 @@ const FlowRenderer = ({ onMoveStart, onMoveEnd, selectionKeyCode, + multiSelectionKeyCode, zoomOnScroll, zoomOnDoubleClick, paneMoveable, @@ -48,12 +49,15 @@ const FlowRenderer = ({ onSelectionContextMenu, }: FlowRendererProps) => { const zoomPane = useRef(null); + const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); + const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); const nodesSelectionActive = useStoreState((state) => state.nodesSelectionActive); + const selectionKeyPressed = useKeyPress(selectionKeyCode); useResizeHandler(zoomPane); - useGlobalKeyHandler({ onElementsRemove, deleteKeyCode }); + useGlobalKeyHandler({ onElementsRemove, deleteKeyCode, multiSelectionKeyCode }); useD3Zoom({ zoomPane, @@ -73,6 +77,7 @@ const FlowRenderer = ({ (event: MouseEvent) => { onPaneClick?.(event); unsetNodesSelection(); + resetSelectedElements(); }, [onPaneClick] ); diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 0dcafc0f..85a4b266 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -55,6 +55,7 @@ export interface GraphViewProps { connectionLineStyle?: CSSProperties; connectionLineComponent?: ConnectionLineComponent; deleteKeyCode: number; + multiSelectionKeyCode: number; snapToGrid: boolean; snapGrid: [number, number]; onlyRenderVisibleNodes: boolean; @@ -96,6 +97,7 @@ const GraphView = ({ connectionLineStyle, connectionLineComponent, selectionKeyCode, + multiSelectionKeyCode, onElementsRemove, deleteKeyCode, elements, @@ -245,6 +247,7 @@ const GraphView = ({ onElementsRemove={onElementsRemove} deleteKeyCode={deleteKeyCode} selectionKeyCode={selectionKeyCode} + multiSelectionKeyCode={multiSelectionKeyCode} onMove={onMove} onMoveStart={onMoveStart} onMoveEnd={onMoveEnd} diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx index 0593fa8f..8de2008c 100644 --- a/src/container/ReactFlow/index.tsx +++ b/src/container/ReactFlow/index.tsx @@ -82,6 +82,7 @@ export interface ReactFlowProps extends Omit, 'on connectionLineComponent?: ConnectionLineComponent; deleteKeyCode?: number; selectionKeyCode?: number; + multiSelectionKeyCode?: number; snapToGrid?: boolean; snapGrid?: [number, number]; onlyRenderVisibleNodes?: boolean; @@ -132,6 +133,7 @@ const ReactFlow = ({ connectionLineComponent, deleteKeyCode = 8, selectionKeyCode = 16, + multiSelectionKeyCode = 91, snapToGrid = false, snapGrid = [15, 15], onlyRenderVisibleNodes = true, @@ -182,6 +184,7 @@ const ReactFlow = ({ selectionKeyCode={selectionKeyCode} onElementsRemove={onElementsRemove} deleteKeyCode={deleteKeyCode} + multiSelectionKeyCode={multiSelectionKeyCode} elements={elements} onConnect={onConnect} onConnectStart={onConnectStart} diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index 0fd256c8..c229b09d 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -7,15 +7,20 @@ import { Elements } from '../types'; interface HookParams { deleteKeyCode: number; + multiSelectionKeyCode: number; onElementsRemove?: (elements: Elements) => void; } -export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { +export default ({ deleteKeyCode, multiSelectionKeyCode, onElementsRemove }: HookParams): void => { const selectedElements = useStoreState((state) => state.selectedElements); const edges = useStoreState((state) => state.edges); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); + const setMultiSelectionActive = useStoreActions((actions) => actions.setMultiSelectionActive); + const resetSelectedElements = useStoreActions((actions) => actions.resetSelectedElements); + const deleteKeyPressed = useKeyPress(deleteKeyCode); + const multiSelectionKeyPressed = useKeyPress(multiSelectionKeyCode); useEffect(() => { if (onElementsRemove && deleteKeyPressed && selectedElements) { @@ -30,6 +35,11 @@ export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { onElementsRemove(elementsToRemove); unsetNodesSelection(); + resetSelectedElements(); } }, [deleteKeyPressed]); + + useEffect(() => { + setMultiSelectionActive(multiSelectionKeyPressed); + }, [multiSelectionKeyPressed]) }; diff --git a/src/store/index.ts b/src/store/index.ts index 39d64b3b..eeae8f79 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -82,6 +82,8 @@ export interface StoreModel { nodesConnectable: boolean; elementsSelectable: boolean; + multiSelectionActive: boolean; + reactFlowVersion: string; onConnect?: OnConnectFunc; @@ -104,8 +106,10 @@ export interface StoreModel { setSelection: Action; unsetNodesSelection: Action; + resetSelectedElements: Action; setSelectedElements: Action; + addSelectedElements: Thunk; updateTransform: Action; @@ -141,6 +145,8 @@ export interface StoreModel { zoom: Thunk; zoomIn: Thunk; zoomOut: Thunk; + + setMultiSelectionActive: Action; } export const storeModel: StoreModel = { @@ -187,6 +193,8 @@ export const storeModel: StoreModel = { nodesConnectable: true, elementsSelectable: true, + multiSelectionActive: false, + reactFlowVersion: typeof __REACT_FLOW_VERSION__ !== 'undefined' ? __REACT_FLOW_VERSION__ : '-', setOnConnect: action((state, onConnect) => { @@ -332,6 +340,9 @@ export const storeModel: StoreModel = { unsetNodesSelection: action((state) => { state.nodesSelectionActive = false; + }), + + resetSelectedElements: action((state) => { state.selectedElements = null; }), @@ -343,6 +354,20 @@ export const storeModel: StoreModel = { state.selectedElements = selectedElements; }), + addSelectedElements: thunk((actions, elements, helpers) => { + const { multiSelectionActive, selectedElements } = helpers.getState(); + const selectedElementsArr = Array.isArray(elements) ? elements : [elements]; + + if (multiSelectionActive) { + const nextElements = selectedElements ? [...selectedElements, ...selectedElementsArr] : selectedElementsArr; + actions.setSelectedElements(nextElements); + + return; + } + + actions.setSelectedElements(elements); + }), + updateTransform: action((state, transform) => { state.transform[0] = transform.x; state.transform[1] = transform.y; @@ -509,6 +534,10 @@ export const storeModel: StoreModel = { zoomOut: thunk((actions) => { actions.zoom(-0.2); }), + + setMultiSelectionActive: action((state, isActive) => { + state.multiSelectionActive = isActive; + }) }; const nodeEnv: string = process.env.NODE_ENV as string;