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
+4 -3
View File
@@ -15,9 +15,9 @@ import ReactFlow, {
OnSelectionChangeParams, OnSelectionChangeParams,
} from 'react-flow-renderer'; } from 'react-flow-renderer';
const onNodeDragStart = (_: ReactMouseEvent, node: Node) => console.log('drag start', node); const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
const onNodeDrag = (_: ReactMouseEvent, node: Node) => console.log('drag', node); const onNodeDrag = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag', node, nodes);
const onNodeDragStop = (_: ReactMouseEvent, node: Node) => console.log('drag stop', node); 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 onNodeDoubleClick = (_: ReactMouseEvent, node: Node) => console.log('node double click', node);
const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event); const onPaneClick = (event: ReactMouseEvent) => console.log('pane click', event);
const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event); const onPaneScroll = (event?: ReactMouseEvent) => console.log('pane scroll', event);
@@ -50,6 +50,7 @@ const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'input',
draggable: false,
data: { data: {
label: ( label: (
<> <>
+9 -84
View File
@@ -1,14 +1,4 @@
import React, { import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
useEffect,
useRef,
memo,
ComponentType,
CSSProperties,
useMemo,
MouseEvent,
useCallback,
useState,
} from 'react';
import cc from 'classcat'; import cc from 'classcat';
import shallow from 'zustand/shallow'; import shallow from 'zustand/shallow';
@@ -16,12 +6,10 @@ 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 useMemoizedMouseHandler from './useMemoizedMouseHandler'; import useMemoizedMouseHandler from './useMemoizedMouseHandler';
import useDrag, { UseDragEvent, UseDragData } from '../../hooks/useDrag'; import useDragNode from '../../hooks/useDragNode';
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
addSelectedNodes: s.addSelectedNodes, addSelectedNodes: s.addSelectedNodes,
updateNodePosition: s.updateNodePosition,
unselectNodesAndEdges: s.unselectNodesAndEdges,
updateNodeDimensions: s.updateNodeDimensions, updateNodeDimensions: s.updateNodeDimensions,
}); });
@@ -58,12 +46,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
noPanClassName, noPanClassName,
noDragClassName, noDragClassName,
}: WrapNodeProps) => { }: WrapNodeProps) => {
const [dragging, setDragging] = useState(false);
const store = useStoreApi(); const store = useStoreApi();
const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore( const { addSelectedNodes, updateNodeDimensions } = useStore(selector, shallow);
selector,
shallow
);
const nodeElement = useRef<HTMLDivElement>(null); const nodeElement = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition); const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition); const prevTargetPosition = useRef(targetPosition);
@@ -103,67 +87,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
[isSelectable, selected, onClick, id] [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(() => { useEffect(() => {
if (nodeElement.current && !hidden) { if (nodeElement.current && !hidden) {
const currNode = nodeElement.current; const currNode = nodeElement.current;
@@ -193,15 +116,17 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
} }
}, [id, type, sourcePosition, targetPosition]); }, [id, type, sourcePosition, targetPosition]);
useDrag({ const dragging = useDragNode({
onStart: onDragStart, onStart: onNodeDragStart,
onDrag: onDrag, onDrag: onNodeDrag,
onStop: onDragStop, onStop: onNodeDragStop,
nodeRef: nodeElement, nodeRef: nodeElement,
disabled: !isDraggable, disabled: !isDraggable,
noDragClassName, noDragClassName,
handleSelector: dragHandle, handleSelector: dragHandle,
nodeId: id, nodeId: id,
isSelectable,
selectNodesOnDrag,
}); });
if (hidden) { if (hidden) {
+28 -48
View File
@@ -10,7 +10,7 @@ import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, ReactFlowState } from '../../types'; import { Node, ReactFlowState } from '../../types';
import { getRectOfNodes } from '../../utils/graph'; import { getRectOfNodes } from '../../utils/graph';
import useDrag, { UseDragData, UseDragEvent } from '../../hooks/useDrag'; import useDragNode from '../../hooks/useDragNode';
export interface NodesSelectionProps { export interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void; onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void;
@@ -19,13 +19,11 @@ export interface NodesSelectionProps {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void; onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
noPanClassName?: string; noPanClassName?: string;
} }
// @TODO: work with nodeInternals instead of converting it to an array
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
transform: s.transform, transform: s.transform,
selectedNodesBbox: s.selectedNodesBbox,
userSelectionActive: s.userSelectionActive, userSelectionActive: s.userSelectionActive,
selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected), selectedNodes: Array.from(s.nodeInternals.values()).filter((n) => n.selected),
updateNodePosition: s.updateNodePosition,
}); });
function NodesSelection({ function NodesSelection({
@@ -35,65 +33,32 @@ function NodesSelection({
onSelectionContextMenu, onSelectionContextMenu,
noPanClassName, noPanClassName,
}: NodesSelectionProps) { }: NodesSelectionProps) {
const { transform, userSelectionActive, selectedNodes, updateNodePosition } = useStore(selector, shallow); const { transform, userSelectionActive, selectedNodes } = useStore(selector, shallow);
const [tX, tY, tScale] = transform; const [tX, tY, tScale] = transform;
const nodeRef = useRef(null); 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 selectedNodesBbox = useMemo(() => getRectOfNodes(selectedNodes), [selectedNodes]);
const innerStyle = useMemo(
() => ({
width: selectedNodesBbox.width,
height: selectedNodesBbox.height,
top: selectedNodesBbox.y,
left: selectedNodesBbox.x,
}),
[selectedNodesBbox]
);
const onStart = useCallback( const onStart = useCallback(
(event: UseDragEvent) => { (event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDragStart?.(event, nodes),
onSelectionDragStart?.(event.sourceEvent, selectedNodes); [onSelectionDragStart]
},
[onSelectionDragStart, selectedNodes]
); );
const onDrag = useCallback( const onDrag = useCallback(
(event: UseDragEvent, data: UseDragData) => { (event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDrag?.(event, nodes),
updateNodePosition({ [onSelectionDrag]
diff: {
x: data.dx,
y: data.dy,
},
});
onSelectionDrag?.(event.sourceEvent, selectedNodes);
},
[onSelectionDrag, selectedNodes, updateNodePosition]
); );
const onStop = useCallback( const onStop = useCallback(
(event: UseDragEvent) => { (event: MouseEvent, _: Node, nodes: Node[]) => onSelectionDragStop?.(event, nodes),
onSelectionDragStop?.(event.sourceEvent, selectedNodes); [onSelectionDragStop]
},
[selectedNodes, onSelectionDragStop]
); );
const onContextMenu = useCallback( const onContextMenu = useCallback(
(event: MouseEvent) => { (event: MouseEvent) => onSelectionContextMenu?.(event, selectedNodes),
onSelectionContextMenu?.(event, selectedNodes);
},
[onSelectionContextMenu, selectedNodes] [onSelectionContextMenu, selectedNodes]
); );
useDrag({ useDragNode({
onStart, onStart,
onDrag, onDrag,
onStop, onStop,
@@ -105,8 +70,23 @@ function NodesSelection({
} }
return ( return (
<div className={cc(['react-flow__nodesselection', 'react-flow__container', noPanClassName])} style={style}> <div
<div ref={nodeRef} className="react-flow__nodesselection-rect" onContextMenu={onContextMenu} style={innerStyle} /> 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> </div>
); );
} }
+18 -11
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 shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes'; import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { Node, NodeTypesWrapped, Position, ReactFlowState, WrapNodeProps } from '../../types'; import {
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
interface NodeRendererProps { interface NodeRendererProps {
nodeTypes: NodeTypesWrapped; nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean; selectNodesOnDrag: boolean;
onNodeClick?: (event: MouseEvent, element: Node) => void; onNodeClick?: NodeMouseHandler;
onNodeDoubleClick?: (event: MouseEvent, element: Node) => void; onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: (event: MouseEvent, node: Node) => void; onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: (event: MouseEvent, node: Node) => void; onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: (event: MouseEvent, node: Node) => void; onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: (event: MouseEvent, node: Node) => void; onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: (event: MouseEvent, node: Node) => void; onNodeDragStart?: NodeDragHandler;
onNodeDrag?: (event: MouseEvent, node: Node) => void; onNodeDrag?: NodeDragHandler;
onNodeDragStop?: (event: MouseEvent, node: Node) => void; onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean; onlyRenderVisibleElements: boolean;
noPanClassName: string; noPanClassName: string;
noDragClassName: string; noDragClassName: string;
-138
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;
+192
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;
+133
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];
}
+10 -13
View File
@@ -9,21 +9,19 @@ import {
Node, Node,
Edge, Edge,
NodeDimensionUpdate, NodeDimensionUpdate,
NodeDiffUpdate,
CoordinateExtent, CoordinateExtent,
NodeDimensionChange, NodeDimensionChange,
EdgeSelectionChange, EdgeSelectionChange,
NodeSelectionChange, NodeSelectionChange,
NodePositionChange, NodePositionChange,
NodeDragItem,
} 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';
import { import {
createNodeInternals, createNodeInternals,
createPositionChange,
handleControlledEdgeSelectionChange, handleControlledEdgeSelectionChange,
handleControlledNodeSelectionChange, handleControlledNodeSelectionChange,
isParentSelected,
fitView, fitView,
} from './utils'; } from './utils';
import initialState from './initialState'; import initialState from './initialState';
@@ -96,20 +94,19 @@ const createStore = () =>
onNodesChange?.(changes); onNodesChange?.(changes);
} }
}, },
updateNodePosition: ({ id, diff }: NodeDiffUpdate) => { updateNodePositions: (nodeDragItems: NodeDragItem[]) => {
const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes, snapGrid, snapToGrid } = get(); const { onNodesChange, nodeInternals, hasDefaultNodes } = get();
if (hasDefaultNodes || onNodesChange) { if (hasDefaultNodes || onNodesChange) {
const changes: NodePositionChange[] = []; const changes: NodePositionChange[] = [];
nodeInternals.forEach((node) => { nodeDragItems.forEach((node) => {
if (node.selected) { const change: NodePositionChange = {
if (!node.parentNode || !isParentSelected(node, nodeInternals)) { id: node.id,
changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid })); type: 'position',
} position: node.position,
} else if (node.id === id) { };
changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid })); changes.push(change);
}
}); });
if (changes?.length) { if (changes?.length) {
-1
View File
@@ -15,7 +15,6 @@ const initialState: ReactFlowStore = {
onEdgesChange: null, onEdgesChange: null,
hasDefaultNodes: false, hasDefaultNodes: false,
hasDefaultEdges: false, hasDefaultEdges: false,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
d3Zoom: null, d3Zoom: null,
d3Selection: null, d3Selection: null,
d3ZoomHandler: undefined, d3ZoomHandler: undefined,
-61
View File
@@ -92,24 +92,6 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals)
return nextNodeInternals; 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 = { type CreatePostionChangeParams = {
node: Node; node: Node;
nodeExtent: CoordinateExtent; nodeExtent: CoordinateExtent;
@@ -119,49 +101,6 @@ type CreatePostionChangeParams = {
snapGrid?: SnapGrid; 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 = { type InternalFitViewOptions = {
initial?: boolean; initial?: boolean;
} & FitViewOptions; } & FitViewOptions;
+11 -9
View File
@@ -36,6 +36,8 @@ import {
OnMove, OnMove,
OnMoveStart, OnMoveStart,
OnMoveEnd, OnMoveEnd,
NodeDragHandler,
NodeMouseHandler,
} from '.'; } from '.';
import { HandleType } from './handles'; import { HandleType } from './handles';
@@ -47,16 +49,16 @@ export interface ReactFlowProps extends HTMLAttributes<HTMLDivElement> {
defaultEdgeOptions?: DefaultEdgeOptions; defaultEdgeOptions?: DefaultEdgeOptions;
onNodesChange?: OnNodesChange; onNodesChange?: OnNodesChange;
onEdgesChange?: OnEdgesChange; onEdgesChange?: OnEdgesChange;
onNodeClick?: (event: React.MouseEvent, node: Node) => void; onNodeClick?: NodeMouseHandler;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void; onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onNodeDoubleClick?: (event: ReactMouseEvent, node: Node) => void; onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: (event: ReactMouseEvent, node: Node) => void; onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: (event: ReactMouseEvent, node: Node) => void; onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStart?: NodeDragHandler;
onNodeDrag?: (event: ReactMouseEvent, node: Node) => void; onNodeDrag?: NodeDragHandler;
onNodeDragStop?: (event: ReactMouseEvent, node: Node) => void; onNodeDragStop?: NodeDragHandler;
onNodesDelete?: OnNodesDelete; onNodesDelete?: OnNodesDelete;
onEdgesDelete?: OnEdgesDelete; onEdgesDelete?: OnEdgesDelete;
onConnect?: OnConnect; onConnect?: OnConnect;
+2 -3
View File
@@ -3,7 +3,7 @@ import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import { NodeChange, EdgeChange } from './changes'; 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 { Edge, EdgeProps, WrapEdgeProps } from './edges';
import { HandleType, StartHandle } from './handles'; import { HandleType, StartHandle } from './handles';
import { DefaultEdgeOptions } from '.'; import { DefaultEdgeOptions } from '.';
@@ -125,7 +125,6 @@ export type ReactFlowStore = {
transform: Transform; transform: Transform;
nodeInternals: NodeInternals; nodeInternals: NodeInternals;
edges: Edge[]; edges: Edge[];
selectedNodesBbox: Rect;
onNodesChange: OnNodesChange | null; onNodesChange: OnNodesChange | null;
onEdgesChange: OnEdgesChange | null; onEdgesChange: OnEdgesChange | null;
hasDefaultNodes: boolean; hasDefaultNodes: boolean;
@@ -182,7 +181,7 @@ export type ReactFlowActions = {
setEdges: (edges: Edge[]) => void; setEdges: (edges: Edge[]) => void;
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
updateNodePosition: (update: NodeDiffUpdate) => void; updateNodePositions: (nodeDragItems: NodeDragItem[]) => void;
resetSelectedElements: () => void; resetSelectedElements: () => void;
unselectNodesAndEdges: () => void; unselectNodesAndEdges: () => void;
addSelectedNodes: (nodeIds: string[]) => void; addSelectedNodes: (nodeIds: string[]) => void;
+18 -8
View File
@@ -50,6 +50,7 @@ export interface NodeProps<T = any> {
} }
export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export interface WrapNodeProps<T = any> { export interface WrapNodeProps<T = any> {
id: string; id: string;
@@ -70,9 +71,9 @@ export interface WrapNodeProps<T = any> {
onMouseMove?: NodeMouseHandler; onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler; onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler; onContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeMouseHandler; onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeMouseHandler; onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeMouseHandler; onNodeDragStop?: NodeDragHandler;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
sourcePosition: Position; sourcePosition: Position;
@@ -91,11 +92,6 @@ export type NodeHandleBounds = {
target: HandleElement[] | null; target: HandleElement[] | null;
}; };
export type NodeDiffUpdate = {
id?: string;
diff?: XYPosition;
};
export type NodeDimensionUpdate = { export type NodeDimensionUpdate = {
id: string; id: string;
nodeElement: HTMLDivElement; nodeElement: HTMLDivElement;
@@ -108,3 +104,17 @@ export type NodeBounds = XYPosition & {
width: number | null; width: number | null;
height: 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;
};