From 1eee0cd377a526f9ee058944b32b9cb4226d53ab Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 May 2022 18:56:26 +0200 Subject: [PATCH] refactor(useDragNode): cleanup, refactoring --- example/src/Overview/index.tsx | 7 +- src/components/Nodes/wrapNode.tsx | 93 ++---------- src/components/NodesSelection/index.tsx | 76 ++++------ src/container/NodeRenderer/index.tsx | 29 ++-- src/hooks/useDrag.ts | 138 ----------------- src/hooks/useDragNode/index.ts | 192 ++++++++++++++++++++++++ src/hooks/useDragNode/utils.ts | 133 ++++++++++++++++ src/store/index.ts | 23 ++- src/store/initialState.ts | 1 - src/store/utils.ts | 61 -------- src/types/component-props.ts | 20 +-- src/types/general.ts | 5 +- src/types/nodes.ts | 26 +++- 13 files changed, 425 insertions(+), 379 deletions(-) delete mode 100644 src/hooks/useDrag.ts create mode 100644 src/hooks/useDragNode/index.ts create mode 100644 src/hooks/useDragNode/utils.ts diff --git a/example/src/Overview/index.tsx b/example/src/Overview/index.tsx index 795c75b0..4113b0ec 100644 --- a/example/src/Overview/index.tsx +++ b/example/src/Overview/index.tsx @@ -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: ( <> diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index d4865c11..bd675644 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -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) => { 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(null); const prevSourcePosition = useRef(sourcePosition); const prevTargetPosition = useRef(targetPosition); @@ -103,67 +87,6 @@ export default (NodeComponent: ComponentType) => { [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) => { } }, [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) { diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index cdf3ded0..e991f7a1 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -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 ( -
-
+
+
); } diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 3a8dc95d..c6dd8e52 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -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; diff --git a/src/hooks/useDrag.ts b/src/hooks/useDrag.ts deleted file mode 100644 index 89b7fc9c..00000000 --- a/src/hooks/useDrag.ts +++ /dev/null @@ -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; -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; - disabled?: boolean; - noDragClassName?: string; - handleSelector?: string; - nodeId?: string; -}; - -function getOffset(event: UseDragEvent, nodeRef: RefObject): 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): 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({ x: 0, y: 0 }); - const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null }); - const parentPos = useRef({ 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; diff --git a/src/hooks/useDragNode/index.ts b/src/hooks/useDragNode/index.ts new file mode 100644 index 00000000..be24015f --- /dev/null +++ b/src/hooks/useDragNode/index.ts @@ -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; +export type UseDragData = { dx: number; dy: number }; + +type UseDragParams = { + nodeRef: RefObject; + 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(false); + const store = useStoreApi(); + const dragItems = useRef(); + const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null }); + const parentPos = useRef({ 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; diff --git a/src/hooks/useDragNode/utils.ts b/src/hooks/useDragNode/utils.ts new file mode 100644 index 00000000..5b9ef6bd --- /dev/null +++ b/src/hooks/useDragNode/utils.ts @@ -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): 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]; +} diff --git a/src/store/index.ts b/src/store/index.ts index d1c1e9c7..ee7d90ec 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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) { diff --git a/src/store/initialState.ts b/src/store/initialState.ts index 020c0eab..1ad29aed 100644 --- a/src/store/initialState.ts +++ b/src/store/initialState.ts @@ -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, diff --git a/src/store/utils.ts b/src/store/utils.ts index a5104c6c..6cd542c6 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -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; diff --git a/src/types/component-props.ts b/src/types/component-props.ts index e55093a2..4d2a8f50 100644 --- a/src/types/component-props.ts +++ b/src/types/component-props.ts @@ -36,6 +36,8 @@ import { OnMove, OnMoveStart, OnMoveEnd, + NodeDragHandler, + NodeMouseHandler, } from '.'; import { HandleType } from './handles'; @@ -47,16 +49,16 @@ export interface ReactFlowProps extends HTMLAttributes { 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; diff --git a/src/types/general.ts b/src/types/general.ts index a579ce33..3adee294 100644 --- a/src/types/general.ts +++ b/src/types/general.ts @@ -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; diff --git a/src/types/nodes.ts b/src/types/nodes.ts index 4f0262b7..6c416ef5 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -50,6 +50,7 @@ export interface NodeProps { } export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void; +export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void; export interface WrapNodeProps { id: string; @@ -70,9 +71,9 @@ export interface WrapNodeProps { 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; +};