refactor(nodes): simplify selection handling

This commit is contained in:
moklick
2022-05-22 14:09:40 +02:00
parent 21f56db09a
commit a88e53ddc5
9 changed files with 95 additions and 75 deletions
+1
View File
@@ -66,6 +66,7 @@ const BasicFlow = () => {
maxZoom={4} maxZoom={4}
fitView fitView
defaultEdgeOptions={defaultEdgeOptions} defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
> >
<Background variant={BackgroundVariant.Lines} /> <Background variant={BackgroundVariant.Lines} />
-3
View File
@@ -4,14 +4,11 @@ import Handle from '../../components/Handle';
import { NodeProps, Position } from '../../types'; import { NodeProps, Position } from '../../types';
const DefaultNode = ({ const DefaultNode = ({
id,
data, data,
isConnectable, isConnectable,
targetPosition = Position.Top, targetPosition = Position.Top,
sourcePosition = Position.Bottom, sourcePosition = Position.Bottom,
}: NodeProps) => { }: NodeProps) => {
console.log('render', id);
return ( return (
<> <>
<Handle type="target" position={targetPosition} isConnectable={isConnectable} /> <Handle type="target" position={targetPosition} isConnectable={isConnectable} />
+27 -1
View File
@@ -1,5 +1,5 @@
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { GetState } from 'zustand'; import { GetState, SetState } from 'zustand';
import { HandleElement, Node, Position, ReactFlowState } from '../../types'; import { HandleElement, Node, Position, ReactFlowState } from '../../types';
import { getDimensions } from '../../utils'; import { getDimensions } from '../../utils';
@@ -55,3 +55,29 @@ export function getMouseHandler(
handler(event, { ...node }); handler(event, { ...node });
}; };
} }
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
// 2. the on drag start handler when node is draggable and selectNodesOnDrag = true
export function handleNodeClick({
id,
store,
}: {
id: string;
store: {
getState: GetState<ReactFlowState>;
setState: SetState<ReactFlowState>;
};
}) {
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeInternals } = store.getState();
const node = nodeInternals.get(id)!;
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([id]);
} else if (node.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [node] });
}
}
+33 -39
View File
@@ -1,17 +1,13 @@
import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react'; import React, { useEffect, useRef, memo, ComponentType, MouseEvent } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store'; import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext'; import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types'; import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useDrag from '../../hooks/useDrag'; import useDrag from '../../hooks/useDrag';
import { getMouseHandler } from './utils'; import { getMouseHandler, handleNodeClick } from './utils';
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => s.updateNodeDimensions;
addSelectedNodes: s.addSelectedNodes,
updateNodeDimensions: s.updateNodeDimensions,
});
export default (NodeComponent: ComponentType<NodeProps>) => { export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({ const NodeWrapper = ({
@@ -27,9 +23,9 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
onMouseLeave, onMouseLeave,
onContextMenu, onContextMenu,
onNodeDoubleClick, onNodeDoubleClick,
onNodeDragStart, onDragStart,
onNodeDrag, onDrag,
onNodeDragStop, onDragStop,
style, style,
className, className,
isDraggable, isDraggable,
@@ -47,8 +43,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noDragClassName, noDragClassName,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedNodes, updateNodeDimensions } = useStore(selector, shallow); const updateNodeDimensions = useStore(selector);
const nodeElement = useRef<HTMLDivElement>(null); const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition); const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition); const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type); const prevType = useRef(type);
@@ -60,12 +56,12 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu); const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
const onNodeDoubleClickHandler = getMouseHandler(id, store.getState, onNodeDoubleClick); const onNodeDoubleClickHandler = getMouseHandler(id, store.getState, onNodeDoubleClick);
const onSelectNodeHandler = (event: MouseEvent) => { const onSelectNodeHandler = (event: MouseEvent) => {
if (isSelectable) { if (isSelectable && (!selectNodesOnDrag || !isDraggable)) {
store.setState({ nodesSelectionActive: false }); // this handler gets called within the drag start event when selectNodesOnDrag=true
handleNodeClick({
if (!selected) { id,
addSelectedNodes([id]); store,
} });
} }
if (onClick) { if (onClick) {
@@ -75,8 +71,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
}; };
useEffect(() => { useEffect(() => {
if (nodeElement.current && !hidden) { if (nodeRef.current && !hidden) {
const currNode = nodeElement.current; const currNode = nodeRef.current;
resizeObserver?.observe(currNode); resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode); return () => resizeObserver?.unobserve(currNode);
@@ -89,7 +85,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
const sourcePosChanged = prevSourcePosition.current !== sourcePosition; const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition; const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeElement.current && (typeChanged || sourcePosChanged || targetPosChanged)) { if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) { if (typeChanged) {
prevType.current = type; prevType.current = type;
} }
@@ -99,15 +95,15 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
if (targetPosChanged) { if (targetPosChanged) {
prevTargetPosition.current = targetPosition; prevTargetPosition.current = targetPosition;
} }
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
} }
}, [id, type, sourcePosition, targetPosition]); }, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({ const dragging = useDrag({
onStart: onNodeDragStart, onStart: onDragStart,
onDrag: onNodeDrag, onDrag: onDrag,
onStop: onNodeDragStop, onStop: onDragStop,
nodeRef: nodeElement, nodeRef,
disabled: !isDraggable, disabled: !isDraggable,
noDragClassName, noDragClassName,
handleSelector: dragHandle, handleSelector: dragHandle,
@@ -120,22 +116,20 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
return null; return null;
} }
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
]);
return ( return (
<div <div
className={nodeClasses} className={cc([
ref={nodeElement} 'react-flow__node',
`react-flow__node-${type}`,
noPanClassName,
className,
{
selected,
selectable: isSelectable,
parent: isParent,
},
])}
ref={nodeRef}
style={{ style={{
zIndex, zIndex,
transform: `translate(${xPos}px,${yPos}px)`, transform: `translate(${xPos}px,${yPos}px)`,
+13 -19
View File
@@ -12,6 +12,7 @@ import {
selectorExistsTargetToNode, selectorExistsTargetToNode,
updatePosition, updatePosition,
} from './utils'; } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>; export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number }; export type UseDragData = { dx: number; dy: number };
@@ -76,30 +77,23 @@ function useDrag({
} else { } else {
const dragHandler = drag() const dragHandler = drag()
.on('start', (event: UseDragEvent) => { .on('start', (event: UseDragEvent) => {
const { nodeInternals, addSelectedNodes, unselectNodesAndEdges, multiSelectionActive } = store.getState(); const { nodeInternals, multiSelectionActive, unselectNodesAndEdges } = store.getState();
parentPos.current = getParentNodePosition(nodeInternals, nodeId); parentPos.current = getParentNodePosition(nodeInternals, nodeId);
// this part is the regular drag handler for a single node if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {
// it selects the dragged node and deselects all other nodes if multiSelectionActive = false if (!nodeInternals.get(nodeId)?.selected) {
if (nodeId && isSelectable) { // we need to reset selected nodes when selectNodesOnDrag=false
const node = nodeInternals.get(nodeId)!; unselectNodesAndEdges();
if (selectNodesOnDrag) {
store.setState({ nodesSelectionActive: false });
if (!node.selected) {
addSelectedNodes([nodeId]);
}
} else if (!selectNodesOnDrag && !node.selected) {
if (multiSelectionActive) {
addSelectedNodes([nodeId]);
} else {
unselectNodesAndEdges();
store.setState({ nodesSelectionActive: false });
}
} }
} }
if (nodeId && isSelectable && selectNodesOnDrag) {
handleNodeClick({
id: nodeId,
store,
});
}
const mousePos = getMousePosition(event); const mousePos = getMousePosition(event);
dragItems.current = getDragItems(nodeInternals, mousePos, nodeId); dragItems.current = getDragItems(nodeInternals, mousePos, nodeId);
+11 -7
View File
@@ -14,6 +14,7 @@ import {
NodeSelectionChange, NodeSelectionChange,
NodePositionChange, NodePositionChange,
NodeDragItem, NodeDragItem,
UnselectNodesAndEdgesParams,
} from '../types'; } from '../types';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes'; import { createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -152,19 +153,22 @@ const createStore = () =>
set, set,
}); });
}, },
unselectNodesAndEdges: () => { unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
const { nodeInternals, edges } = get(); const { nodeInternals, edges: storeEdges } = get();
const nodes = Array.from(nodeInternals.values()); const nodesToUnselect = nodes ? nodes : Array.from(nodeInternals.values());
const edgesToUnselect = edges ? edges : storeEdges;
const nodesToUnselect = nodes.map((n) => { const changedNodes = nodesToUnselect.map((n) => {
n.selected = false; n.selected = false;
return createSelectionChange(n.id, false); return createSelectionChange(n.id, false);
}) as NodeSelectionChange[]; }) as NodeSelectionChange[];
const edgesToUnselect = edges.map((edge) => createSelectionChange(edge.id, false)) as EdgeSelectionChange[]; const changedEdges = edgesToUnselect.map((edge) =>
createSelectionChange(edge.id, false)
) as EdgeSelectionChange[];
updateNodesAndEdgesSelections({ updateNodesAndEdgesSelections({
changedNodes: nodesToUnselect, changedNodes,
changedEdges: edgesToUnselect, changedEdges,
get, get,
set, set,
}); });
+6 -1
View File
@@ -105,6 +105,11 @@ export type FitBoundsOptions = ViewportHelperFunctionOptions & {
padding?: number; padding?: number;
}; };
export type UnselectNodesAndEdgesParams = {
nodes?: Node[];
edges?: Edge[];
};
export interface ViewportHelperFunctions { export interface ViewportHelperFunctions {
zoomIn: ZoomInOut; zoomIn: ZoomInOut;
zoomOut: ZoomInOut; zoomOut: ZoomInOut;
@@ -187,7 +192,7 @@ export type ReactFlowActions = {
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePositions: (nodeDragItems: NodeDragItem[]) => void; updateNodePositions: (nodeDragItems: NodeDragItem[]) => void;
resetSelectedElements: () => void; resetSelectedElements: () => void;
unselectNodesAndEdges: () => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
addSelectedNodes: (nodeIds: string[]) => void; addSelectedNodes: (nodeIds: string[]) => void;
addSelectedEdges: (edgeIds: string[]) => void; addSelectedEdges: (edgeIds: string[]) => void;
setMinZoom: (minZoom: number) => void; setMinZoom: (minZoom: number) => void;
+3 -3
View File
@@ -71,9 +71,9 @@ export interface WrapNodeProps<T = any> {
onMouseMove?: NodeMouseHandler; onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler; onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler; onContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler; onDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler; onDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler; onDragStop?: NodeDragHandler;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourcePosition: Position; sourcePosition: Position;
+1 -2
View File
@@ -45,8 +45,7 @@ function handleParentExpand(res: any[], updateItem: any) {
} }
function applyChanges(changes: any[], elements: any[]): any[] { function applyChanges(changes: any[], elements: any[]): any[] {
// unfortunately we need this hack to handle the setNodes and setEdges function of the // we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
// useReactFlow hook.
if (changes.some((c) => c.type === 'reset')) { if (changes.some((c) => c.type === 'reset')) {
return changes.filter((c) => c.type === 'reset').map((c) => c.item); return changes.filter((c) => c.type === 'reset').map((c) => c.item);
} }