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