refactor(useDragNode): cleanup, refactoring

This commit is contained in:
moklick
2022-05-16 18:56:26 +02:00
parent c4e98f84a4
commit 1eee0cd377
13 changed files with 425 additions and 379 deletions

View File

@@ -15,9 +15,9 @@ import ReactFlow, {
OnSelectionChangeParams,
} from 'react-flow-renderer';
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node);
const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node);
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag stop', node, nodes);
const onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
@@ -50,6 +50,7 @@ const initialNodes: Node[] = [
{
id: '1',
type: 'input',
draggable: false,
data: {
label: (
<>

View File

@@ -1,14 +1,4 @@
import React, {
useEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
useState,
} from 'react';
import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
@@ -16,12 +6,10 @@ import { useStore, useStoreApi } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeProps, WrapNodeProps, ReactFlowState } from '../../types';
import useMemoizedMouseHandler from './useMemoizedMouseHandler';
import useDrag, { UseDragEvent, UseDragData } from '../../hooks/useDrag';
import useDragNode from '../../hooks/useDragNode';
const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions,
});
@@ -58,12 +46,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noPanClassName,
noDragClassName,
}: WrapNodeProps) => {
const [dragging, setDragging] = useState(false);
const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore(
selector,
shallow
);
const { addSelectedNodes, updateNodeDimensions } = useStore(selector, shallow);
const nodeElement = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
@@ -103,67 +87,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
[isSelectable, selected, onClick, id]
);
const onDragStart = useCallback(
(event: UseDragEvent) => {
if (selectNodesOnDrag && isSelectable) {
store.setState({ nodesSelectionActive: false });
if (!selected) {
addSelectedNodes([id]);
}
} else if (!selectNodesOnDrag && !selected && isSelectable) {
const { multiSelectionActive } = store.getState();
if (multiSelectionActive) {
addSelectedNodes([id]);
} else {
unselectNodesAndEdges();
store.setState({ nodesSelectionActive: false });
}
}
if (onNodeDragStart) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStart(event.sourceEvent as MouseEvent, { ...node });
}
},
[id, selected, selectNodesOnDrag, isSelectable, onNodeDragStart]
);
const onDrag = useCallback(
(event: UseDragEvent, dragPos: UseDragData) => {
updateNodePosition({ id, diff: { x: dragPos.dx, y: dragPos.dy } });
setDragging(true);
if (onNodeDrag) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDrag(event.sourceEvent as MouseEvent, {
...node,
position: {
x: node.position.x + dragPos.dx,
y: node.position.y + dragPos.dy,
},
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + dragPos.dx,
y: (node.positionAbsolute?.y || 0) + dragPos.dy,
},
});
}
},
[id, onNodeDrag]
);
const onDragStop = useCallback(
(event: UseDragEvent) => {
setDragging(false);
if (onNodeDragStop) {
const node = store.getState().nodeInternals.get(id)!;
onNodeDragStop(event.sourceEvent as MouseEvent, { ...node });
}
},
[id, onNodeDragStop]
);
useEffect(() => {
if (nodeElement.current && !hidden) {
const currNode = nodeElement.current;
@@ -193,15 +116,17 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
}
}, [id, type, sourcePosition, targetPosition]);
useDrag({
onStart: onDragStart,
onDrag: onDrag,
onStop: onDragStop,
const dragging = useDragNode({
onStart: onNodeDragStart,
onDrag: onNodeDrag,
onStop: onNodeDragStop,
nodeRef: nodeElement,
disabled: !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
selectNodesOnDrag,
});
if (hidden) {

View File

@@ -10,7 +10,7 @@ import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph';
import useDrag, { UseDragData, UseDragEvent } from '../../hooks/useDrag';
import useDragNode from '../../hooks/useDragNode';
export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -19,13 +19,11 @@ export interface NodesSelectionProps {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string;
}
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({
transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
updateNodePosition: s.updateNodePosition,
});
function NodesSelection({
@@ -35,65 +33,32 @@ function NodesSelection({
onSelectionContextMenu,
noPanClassName,
}: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, updateNodePosition } = useStore(selector, shallow);
const { transform, userSelectionActive, selectedNodes } = useStore(selector, shallow);
const [tX, tY, tScale] = transform;
const nodeRef = useRef(null);
const style = useMemo(
() => ({
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
}),
[tX, tY, tScale]
);
const selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
height: selectedNodesBbox.height,
top: selectedNodesBbox.y,
left: selectedNodesBbox.x,
}),
[selectedNodesBbox]
);
const onStart = useCallback(
(event: UseDragEvent) => {
onSelectionDragStart?.(event.sourceEvent, selectedNodes);
},
[onSelectionDragStart, selectedNodes]
(event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDragStart?.(event, nodes),
[onSelectionDragStart]
);
const onDrag = useCallback(
(event: UseDragEvent, data: UseDragData) => {
updateNodePosition({
diff: {
x: data.dx,
y: data.dy,
},
});
onSelectionDrag?.(event.sourceEvent, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosition]
(event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDrag?.(event, nodes),
[onSelectionDrag]
);
const onStop = useCallback(
(event: UseDragEvent) => {
onSelectionDragStop?.(event.sourceEvent, selectedNodes);
},
[selectedNodes, onSelectionDragStop]
(event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDragStop?.(event, nodes),
[onSelectionDragStop]
);
const onContextMenu = useCallback(
(event: MouseEvent) => {
onSelectionContextMenu?.(event, selectedNodes);
},
(event: MouseEvent) => onSelectionContextMenu?.(event, selectedNodes),
[onSelectionContextMenu, selectedNodes]
);
useDrag({
useDragNode({
onStart,
onDrag,
onStop,
@@ -105,8 +70,23 @@ function NodesSelection({
}
return (
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}>
<div ref={nodeRef} className="react-flow__nodesselection-rect" onContextMenu={onContextMenu} style={innerStyle} />
<div
className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])}
style={{
transform: `translate(${tX}px,${tY}px) scale(${tScale})`,
}}
>
<div
ref={nodeRef}
className="react-flow__nodesselection-rect"
onContextMenu={onContextMenu}
style={{
width: selectedNodesBbox.width,
height: selectedNodesBbox.height,
top: selectedNodesBbox.y,
left: selectedNodesBbox.x,
}}
/>
</div>
);
}

View File

@@ -1,22 +1,29 @@
import React, { memo, useMemo, ComponentType, MouseEvent, useEffect, useRef } from 'react';
import React, { memo, useMemo, ComponentType, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import { Node, NodeTypesWrapped, Position, ReactFlowState, WrapNodeProps } from '../../types';
import {
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
interface NodeRendererProps {
nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean;
onNodeClick?: (event: MouseEvent, element: Node) => void;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void;
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void;
onNodeMouseMove?: (event: MouseEvent, node: Node) => void;
onNodeMouseLeave?: (event: MouseEvent, node: Node) => void;
onNodeContextMenu?: (event: MouseEvent, node: Node) => void;
onNodeDragStart?: (event: MouseEvent, node: Node) => void;
onNodeDrag?: (event: MouseEvent, node: Node) => void;
onNodeDragStop?: (event: MouseEvent, node: Node) => void;
onNodeClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;

View File

@@ -1,138 +0,0 @@
import { RefObject, useEffect, useRef } from 'react';
import { D3DragEvent, drag, SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import { useStoreApi } from '../store';
import { pointToRendererPoint } from '../utils/graph';
import { NodeInternals, XYPosition } from '../types';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number };
type UseDragParams = {
onStart: (event: UseDragEvent) => void;
onDrag: (event: UseDragEvent, data: UseDragData) => void;
onStop: (event: UseDragEvent) => void;
nodeRef: RefObject<Element>;
disabled?: boolean;
noDragClassName?: string;
handleSelector?: string;
nodeId?: string;
};
function getOffset(event: UseDragEvent, nodeRef: RefObject<Element>): XYPosition {
const bounds = nodeRef.current?.getBoundingClientRect() || { x: 0, y: 0 };
const parent = (nodeRef.current as HTMLDivElement)?.offsetParent;
const parentBounds = parent?.getBoundingClientRect() || { x: 0, y: 0 };
return {
x: event.x - (bounds.x - parentBounds.x - (parent?.scrollLeft || 0)),
y: event.y - (bounds.y - parentBounds.y - (parent?.scrollTop || 0)),
};
}
function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): XYPosition {
const parentNodeId = nodeId ? nodeInternals.get(nodeId)?.parentNode : null;
const parentNode = parentNodeId ? nodeInternals.get(parentNodeId) : null;
return {
x: parentNode?.positionAbsolute?.x || 0,
y: parentNode?.positionAbsolute?.y || 0,
};
}
function selectorExistsTargetToNode(target: Element, selector: string, nodeRef: RefObject<Element>): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === nodeRef.current) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
function useDrag({
onStart,
onDrag,
onStop,
nodeRef,
disabled = false,
noDragClassName,
handleSelector,
nodeId,
}: UseDragParams) {
const store = useStoreApi();
const startPos = useRef<XYPosition>({ x: 0, y: 0 });
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
const parentPos = useRef<XYPosition>({ x: 0, y: 0 });
useEffect(() => {
if (nodeRef?.current) {
const selection = select(nodeRef.current);
if (disabled) {
selection.on('.drag', null);
} else {
const dragHandler = drag()
.on('start', (event: UseDragEvent) => {
const { transform, nodeInternals } = store.getState();
const offset = getOffset(event, nodeRef);
parentPos.current = getParentNodePosition(nodeInternals, nodeId);
startPos.current = {
x: offset.x - transform[0],
y: offset.y - transform[1],
};
onStart(event);
})
.on('drag', (event: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const pos = pointToRendererPoint(
{
x: event.x - startPos.current.x,
y: event.y - startPos.current.y,
},
transform,
snapToGrid,
snapGrid
);
pos.x -= parentPos.current.x;
pos.y -= parentPos.current.y;
// skip events without movement
if (lastPos.current.x !== pos.x || lastPos.current.y !== pos.y) {
lastPos.current = pos;
onDrag(event, {
dx: pos.x,
dy: pos.y,
});
event.on('end', (event) => {
onStop(event);
});
}
})
.filter((event: any) => {
const filter = !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName);
return handleSelector
? selectorExistsTargetToNode(event.target, handleSelector, nodeRef) && filter
: filter;
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}
}
}, [onStart, onDrag, onStop, nodeRef, disabled, noDragClassName, handleSelector, nodeId]);
return null;
}
export default useDrag;

View File

@@ -0,0 +1,192 @@
import { RefObject, useEffect, useRef, MouseEvent, useState, useMemo, useCallback } from 'react';
import { D3DragEvent, drag, SubjectPosition } from 'd3-drag';
import { select } from 'd3-selection';
import { useStoreApi } from '../../store';
import { pointToRendererPoint } from '../../utils/graph';
import { NodeDragItem, NodeDragHandler, XYPosition } from '../../types';
import {
getDragItems,
getEventHandlerParams,
getParentNodePosition,
selectorExistsTargetToNode,
updatePosition,
} from './utils';
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number };
type UseDragParams = {
nodeRef: RefObject<Element>;
onStart?: NodeDragHandler;
onDrag?: NodeDragHandler;
onStop?: NodeDragHandler;
disabled?: boolean;
noDragClassName?: string;
handleSelector?: string;
nodeId?: string;
isSelectable?: boolean;
selectNodesOnDrag?: boolean;
};
function useDrag({
onStart,
onDrag,
onStop,
nodeRef,
disabled = false,
noDragClassName,
handleSelector,
nodeId,
isSelectable,
selectNodesOnDrag,
}: UseDragParams) {
const [dragging, setDragging] = useState<boolean>(false);
const store = useStoreApi();
const dragItems = useRef<NodeDragItem[]>();
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });
const parentPos = useRef<XYPosition>({ x: 0, y: 0 });
// TODO: should we store the ref or the bounds in the store?
// So that it is easier / more reliable to access?
const countainerBounds = useMemo(() => {
if (typeof document !== 'undefined') {
return document.querySelector('.react-flow')?.getBoundingClientRect();
}
}, []);
// returns the mouse position projected to the RF coordinate system
const getMousePosition = useCallback(
(event: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const mousePos = pointToRendererPoint(
{
x: event.sourceEvent.clientX - (countainerBounds?.x || 0) - (window.scrollX || 0),
y: event.sourceEvent.clientY - (countainerBounds?.y || 0) - (window.scrollY || 0),
},
transform,
snapToGrid,
snapGrid
);
mousePos.x -= parentPos.current.x;
mousePos.y -= parentPos.current.y;
return mousePos;
},
[countainerBounds?.x, countainerBounds?.y, store]
);
useEffect(() => {
if (nodeRef?.current && countainerBounds) {
const selection = select(nodeRef.current);
if (disabled) {
selection.on('.drag', null);
} else {
const dragHandler = drag()
.on('start', (event: UseDragEvent) => {
const { nodeInternals, addSelectedNodes, unselectNodesAndEdges, multiSelectionActive } = 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 });
}
}
}
const mousePos = getMousePosition(event);
dragItems.current = getDragItems(nodeInternals, mousePos);
if (onStart && dragItems.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStart(event.sourceEvent as MouseEvent, currentNode, nodes);
}
})
.on('drag', (event: UseDragEvent) => {
const { updateNodePositions, nodeInternals, nodeExtent } = store.getState();
const mousePos = getMousePosition(event);
// skip events without movement
if ((lastPos.current.x !== mousePos.x || lastPos.current.y !== mousePos.y) && dragItems.current) {
lastPos.current = mousePos;
dragItems.current = dragItems.current.map((n) => updatePosition(n, mousePos, nodeInternals, nodeExtent));
updateNodePositions(dragItems.current);
setDragging(true);
if (onDrag) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onDrag(event.sourceEvent as MouseEvent, currentNode, nodes);
}
}
event.on('end', (event) => {
if (onStop && dragItems.current) {
const [currentNode, nodes] = getEventHandlerParams({
nodeId,
dragItems: dragItems.current,
nodeInternals,
});
onStop(event.sourceEvent as MouseEvent, currentNode, nodes);
}
});
})
.filter((event: any) => {
const filter = !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName);
return handleSelector
? selectorExistsTargetToNode(event.target, handleSelector, nodeRef) && filter
: filter;
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}
}
}, [
onStart,
onDrag,
onStop,
nodeRef,
disabled,
noDragClassName,
handleSelector,
isSelectable,
store,
nodeId,
selectNodesOnDrag,
countainerBounds,
getMousePosition,
]);
return dragging;
}
export default useDrag;

View File

@@ -0,0 +1,133 @@
import { RefObject } from 'react';
import { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types';
import { clampPosition } from '../../utils';
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
export function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): XYPosition {
const parentNodeId = nodeId ? nodeInternals.get(nodeId)?.parentNode : null;
const parentNode = parentNodeId ? nodeInternals.get(parentNodeId) : null;
return {
x: parentNode?.positionAbsolute?.x || 0,
y: parentNode?.positionAbsolute?.y || 0,
};
}
export function selectorExistsTargetToNode(target: Element, selector: string, nodeRef: RefObject<Element>): boolean {
let current = target;
do {
if (current?.matches(selector)) return true;
if (current === nodeRef.current) return false;
current = current.parentElement as Element;
} while (current);
return false;
}
// looks for all selected nodes and created a NodeDragItem for each of them
export function getDragItems(nodeInternals: NodeInternals, mousePos: XYPosition): NodeDragItem[] {
return Array.from(nodeInternals.values())
.filter((n) => n.selected && (!n.parentNode || !isParentSelected(n, nodeInternals)))
.map((n) => ({
id: n.id,
position: n.position,
distance: {
x: mousePos.x - n.position.x,
y: mousePos.y - n.position.y,
},
delta: {
x: 0,
y: 0,
},
extent: n.extent,
parentNode: n.parentNode,
width: n.width,
height: n.height,
}));
}
export function updatePosition(
dragItem: NodeDragItem,
mousePos: XYPosition,
nodeInternals: NodeInternals,
nodeExtent?: CoordinateExtent
): NodeDragItem {
let currentExtent = dragItem.extent || nodeExtent;
let nextPosition = { x: mousePos.x - dragItem.distance.x, y: mousePos.y - dragItem.distance.y };
if (dragItem.extent === 'parent') {
if (dragItem.parentNode && dragItem.width && dragItem.height) {
const parent = nodeInternals.get(dragItem.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - dragItem.width, parent.height - dragItem.height],
]
: currentExtent;
} else {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent;
}
}
nextPosition = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
dragItem.delta = {
x: nextPosition.x - dragItem.position.x,
y: nextPosition.y - dragItem.position.y,
};
dragItem.position = nextPosition;
return dragItem;
}
// returns two params:
// 1. the dragged node (or the first of the list, if we are dragging a node selection)
// 2. array of selected nodes (handy when multi selection is active)
export function getEventHandlerParams({
nodeId,
dragItems,
nodeInternals,
}: {
nodeId?: string;
dragItems: NodeDragItem[];
nodeInternals: NodeInternals;
}): [Node, Node[]] {
const extentedDragItems: Node[] = dragItems.map((n) => {
const node = nodeInternals.get(n.id)!;
return {
...node,
position: n.position,
positionAbsolute: {
x: (node.positionAbsolute?.x || 0) + n.delta.x,
y: (node.positionAbsolute?.y || 0) + n.delta.y,
},
};
});
return [nodeId ? extentedDragItems.find((n) => n.id === nodeId)! : extentedDragItems[0], extentedDragItems];
}

View File

@@ -9,21 +9,19 @@ import {
Node,
Edge,
NodeDimensionUpdate,
NodeDiffUpdate,
CoordinateExtent,
NodeDimensionChange,
EdgeSelectionChange,
NodeSelectionChange,
NodePositionChange,
NodeDragItem,
} from '../types';
import { getHandleBounds } from '../components/Nodes/utils';
import { createSelectionChange, getSelectionChanges } from '../utils/changes';
import {
createNodeInternals,
createPositionChange,
handleControlledEdgeSelectionChange,
handleControlledNodeSelectionChange,
isParentSelected,
fitView,
} from './utils';
import initialState from './initialState';
@@ -96,20 +94,19 @@ const createStore = () =>
onNodesChange?.(changes);
}
},
updateNodePosition: ({ id, diff }: NodeDiffUpdate) => {
const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes, snapGrid, snapToGrid } = get();
updateNodePositions: (nodeDragItems: NodeDragItem[]) => {
const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) {
const changes: NodePositionChange[] = [];
nodeInternals.forEach((node) => {
if (node.selected) {
if (!node.parentNode || !isParentSelected(node, nodeInternals)) {
changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid }));
}
} else if (node.id === id) {
changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid }));
}
nodeDragItems.forEach((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
position: node.position,
};
changes.push(change);
});
if (changes?.length) {

View File

@@ -15,7 +15,6 @@ const initialState: ReactFlowStore = {
onEdgesChange: null,
hasDefaultNodes: false,
hasDefaultEdges: false,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null,
d3Selection: null,
d3ZoomHandler: undefined,

View File

@@ -92,24 +92,6 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals;
}
export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean {
if (!node.parentNode) {
return false;
}
const parentNode = nodeInternals.get(node.parentNode);
if (!parentNode) {
return false;
}
if (parentNode.selected) {
return true;
}
return isParentSelected(parentNode, nodeInternals);
}
type CreatePostionChangeParams = {
node: Node;
nodeExtent: CoordinateExtent;
@@ -119,49 +101,6 @@ type CreatePostionChangeParams = {
snapGrid?: SnapGrid;
};
export function createPositionChange({
node,
diff,
nodeExtent,
nodeInternals,
snapToGrid,
snapGrid,
}: CreatePostionChangeParams): NodePositionChange {
const change: NodePositionChange = {
id: node.id,
type: 'position',
};
if (diff) {
const nextPosition = { x: diff.x, y: diff.y };
let currentExtent = node.extent || nodeExtent;
if (node.extent === 'parent') {
if (node.parentNode && node.width && node.height) {
const parent = nodeInternals.get(node.parentNode);
currentExtent =
parent?.width && parent?.height
? [
[0, 0],
[parent.width - node.width, parent.height - node.height],
]
: currentExtent;
} else {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn('[React Flow]: Only child nodes can use a parent extent. Help: https://reactflow.dev/error#500');
}
currentExtent = nodeExtent;
}
}
change.position = currentExtent ? clampPosition(nextPosition, currentExtent as CoordinateExtent) : nextPosition;
}
return change;
}
type InternalFitViewOptions = {
initial?: boolean;
} & FitViewOptions;

View File

@@ -36,6 +36,8 @@ import {
OnMove,
OnMoveStart,
OnMoveEnd,
NodeDragHandler,
NodeMouseHandler,
} from '.';
import { HandleType } from './handles';
@@ -47,16 +49,16 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange;
onNodeClick?: (event: React.MouseEvent, node: Node) => void;
onNodeClick?: NodeMouseHandler;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void;
onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void;
onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect;

View File

@@ -3,7 +3,7 @@ import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeDiffUpdate, NodeProps, WrapNodeProps } from './nodes';
import { Node, NodeInternals, NodeDimensionUpdate, NodeProps, WrapNodeProps, NodeDragItem } from './nodes';
import { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.';
@@ -125,7 +125,6 @@ export type ReactFlowStore = {
transform: Transform;
nodeInternals: NodeInternals;
edges: Edge[];
selectedNodesBbox: Rect;
onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean;
@@ -182,7 +181,7 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void;
updateNodePositions: (nodeDragItems: NodeDragItem[]) => void;
resetSelectedElements: () => void;
unselectNodesAndEdges: () => void;
addSelectedNodes: (nodeIds: string[]) => void;

View File

@@ -50,6 +50,7 @@ export interface NodeProps<T = any> {
}
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export interface WrapNodeProps<T = any> {
id: string;
@@ -70,9 +71,9 @@ export interface WrapNodeProps<T = any> {
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeMouseHandler;
onNodeDrag?: NodeMouseHandler;
onNodeDragStop?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
style?: CSSProperties;
className?: string;
sourcePosition: Position;
@@ -91,11 +92,6 @@ export type NodeHandleBounds = {
target: HandleElement[] | null;
};
export type NodeDiffUpdate = {
id?: string;
diff?: XYPosition;
};
export type NodeDimensionUpdate = {
id: string;
nodeElement: HTMLDivElement;
@@ -108,3 +104,17 @@ export type NodeBounds = XYPosition & {
width: number | null;
height: number | null;
};
export type NodeDragItem = {
id: string;
// relative node position
position: XYPosition;
// distance from the mouse cursor to the node when start dragging
distance: XYPosition;
// delta to previous position
delta: XYPosition;
width?: number | null;
height?: number | null;
extent?: 'parent' | CoordinateExtent;
parentNode?: string;
};