refactor(useDragNode): cleanup, refactoring
This commit is contained in:
@@ -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: (
|
||||
<>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
src/hooks/useDragNode/index.ts
Normal file
192
src/hooks/useDragNode/index.ts
Normal 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
src/hooks/useDragNode/utils.ts
Normal file
133
src/hooks/useDragNode/utils.ts
Normal 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];
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user