diff --git a/cypress/support/commands.js b/cypress/support/commands.js index a3926be6..3522b806 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -26,9 +26,13 @@ Cypress.Commands.add('drag', (selector, { x, y }) => { return cy - .get(selector) - .trigger('mousedown', { which: 1 }) - .trigger('mousemove', { clientX: x, clientY: y }) - .wait(50) - .trigger('mouseup', { force: true }); + .window() + .then((window) => + cy + .get(selector) + .trigger('mousedown', { which: 1, view: window }) + .trigger('mousemove', { clientX: x, clientY: y, force: true }) + .wait(50) + .trigger('mouseup', { view: window, force: tru }) + ); }); diff --git a/src/components/Nodes/useMemoizedMouseHandler.ts b/src/components/Nodes/useMemoizedMouseHandler.ts index 36334695..7d3067f3 100644 --- a/src/components/Nodes/useMemoizedMouseHandler.ts +++ b/src/components/Nodes/useMemoizedMouseHandler.ts @@ -5,18 +5,17 @@ import { ReactFlowState, Node } from '../../types'; function useMemoizedMouseHandler( id: string, - dragging: boolean, getState: GetState, handler?: (event: MouseEvent, node: Node) => void ) { const memoizedHandler = useCallback( (event: MouseEvent) => { - if (typeof handler !== 'undefined' && !dragging) { + if (typeof handler !== 'undefined') { const node = getState().nodeInternals.get(id)!; handler(event, { ...node }); } }, - [handler, dragging, id] + [handler, id] ); return memoizedHandler; diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index c4ca752e..0c113b55 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -1,4 +1,14 @@ -import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; +import React, { + useEffect, + useRef, + memo, + ComponentType, + CSSProperties, + useMemo, + MouseEvent, + useCallback, + useState, +} from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; @@ -41,7 +51,6 @@ export default (NodeComponent: ComponentType) => { sourcePosition, targetPosition, hidden, - dragging, resizeObserver, dragHandle, zIndex, @@ -49,6 +58,8 @@ export default (NodeComponent: ComponentType) => { noPanClassName, noDragClassName, }: WrapNodeProps) => { + const draggingRef = useRef(false); + const [dragging, setDragging] = useState(false); const store = useStoreApi(); const { addSelectedNodes, unselectNodesAndEdges, updateNodePosition, updateNodeDimensions } = useStore( selector, @@ -69,11 +80,11 @@ export default (NodeComponent: ComponentType) => { [zIndex, xPos, yPos, hasPointerEvents, style] ); - const onMouseEnterHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseEnter); - const onMouseMoveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseMove); - const onMouseLeaveHandler = useMemoizedMouseHandler(id, dragging, store.getState, onMouseLeave); - const onContextMenuHandler = useMemoizedMouseHandler(id, false, store.getState, onContextMenu); - const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, false, store.getState, onNodeDoubleClick); + const onMouseEnterHandler = useMemoizedMouseHandler(id, store.getState, onMouseEnter); + const onMouseMoveHandler = useMemoizedMouseHandler(id, store.getState, onMouseMove); + const onMouseLeaveHandler = useMemoizedMouseHandler(id, store.getState, onMouseLeave); + const onContextMenuHandler = useMemoizedMouseHandler(id, store.getState, onContextMenu); + const onNodeDoubleClickHandler = useMemoizedMouseHandler(id, store.getState, onNodeDoubleClick); const onSelectNodeHandler = useCallback( (event: MouseEvent) => { @@ -123,13 +134,14 @@ export default (NodeComponent: ComponentType) => { const onDrag = useCallback( (event: UseDragEvent, dragPos: UseDragData) => { - updateNodePosition({ id, dragging: true, diff: { x: dragPos.dx, y: dragPos.dy } }); + updateNodePosition({ id, diff: { x: dragPos.dx, y: dragPos.dy } }); + setDragging(true); + draggingRef.current = true; if (onNodeDrag) { const node = store.getState().nodeInternals.get(id)!; onNodeDrag(event.sourceEvent as MouseEvent, { ...node, - dragging: true, position: { x: node.position.x + dragPos.dx, y: node.position.y + dragPos.dy, @@ -146,15 +158,13 @@ export default (NodeComponent: ComponentType) => { const onDragStop = useCallback( (event: UseDragEvent) => { - // onDragStop also gets called when user just clicks on a node. - // Because of that we set dragging to true inside the onDrag handler and handle the click here let node; if (onClick || onNodeDragStop) { node = store.getState().nodeInternals.get(id)!; } - if (!dragging) { + if (!draggingRef.current) { if (isSelectable && !selectNodesOnDrag && !selected) { addSelectedNodes([id]); } @@ -165,17 +175,14 @@ export default (NodeComponent: ComponentType) => { return; } - - updateNodePosition({ - id, - dragging: false, - }); + draggingRef.current = false; + setDragging(false); if (onNodeDragStop && node) { - onNodeDragStop(event.sourceEvent as MouseEvent, { ...node, dragging: false }); + onNodeDragStop(event.sourceEvent as MouseEvent, { ...node }); } }, - [id, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, dragging, selected] + [id, onNodeDragStop, onClick] ); useEffect(() => { diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 84a22823..cdf3ded0 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -72,7 +72,6 @@ function NodesSelection({ x: data.dx, y: data.dy, }, - dragging: true, }); onSelectionDrag?.(event.sourceEvent, selectedNodes); @@ -82,10 +81,6 @@ function NodesSelection({ const onStop = useCallback( (event: UseDragEvent) => { - updateNodePosition({ - dragging: false, - }); - onSelectionDragStop?.(event.sourceEvent, selectedNodes); }, [selectedNodes, onSelectionDragStop] diff --git a/src/container/NodeRenderer/index.tsx b/src/container/NodeRenderer/index.tsx index 4a043061..3a8dc95d 100644 --- a/src/container/NodeRenderer/index.tsx +++ b/src/container/NodeRenderer/index.tsx @@ -93,7 +93,6 @@ const NodeRenderer = (props: NodeRendererProps) => { hidden={node.hidden} xPos={node.positionAbsolute?.x ?? 0} yPos={node.positionAbsolute?.y ?? 0} - dragging={!!node.dragging} selectNodesOnDrag={props.selectNodesOnDrag} onClick={props.onNodeClick} onMouseEnter={props.onNodeMouseEnter} diff --git a/src/hooks/useDrag.ts b/src/hooks/useDrag.ts index c697ff98..f4aacfbf 100644 --- a/src/hooks/useDrag.ts +++ b/src/hooks/useDrag.ts @@ -16,7 +16,6 @@ type UseDragParams = { nodeRef: RefObject; disabled?: boolean; noDragClassName?: string; - // @TODO: implement handleSelector functionality handleSelector?: string; nodeId?: string; }; @@ -42,7 +41,27 @@ function getParentNodePosition(nodeInternals: NodeInternals, nodeId?: string): X }; } -function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragClassName, nodeId }: UseDragParams) { +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 }); @@ -93,8 +112,15 @@ function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragCla }); } }) - .on('end', onStop) - .filter((event: any) => !event.ctrlKey && !event.button && !event.target.className.includes(noDragClassName)); + .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); @@ -103,7 +129,7 @@ function useDrag({ onStart, onDrag, onStop, nodeRef, disabled = false, noDragCla }; } } - }, [disabled, noDragClassName, nodeId]); + }, [onStart, onDrag, onStop, nodeRef, disabled, noDragClassName, handleSelector, nodeId]); return null; } diff --git a/src/store/index.ts b/src/store/index.ts index a1855a3d..d1c1e9c7 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -96,7 +96,7 @@ const createStore = () => onNodesChange?.(changes); } }, - updateNodePosition: ({ id, diff, dragging }: NodeDiffUpdate) => { + updateNodePosition: ({ id, diff }: NodeDiffUpdate) => { const { onNodesChange, nodeExtent, nodeInternals, hasDefaultNodes, snapGrid, snapToGrid } = get(); if (hasDefaultNodes || onNodesChange) { @@ -105,14 +105,10 @@ const createStore = () => nodeInternals.forEach((node) => { if (node.selected) { if (!node.parentNode || !isParentSelected(node, nodeInternals)) { - changes.push( - createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals, snapToGrid, snapGrid }) - ); + changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid })); } } else if (node.id === id) { - changes.push( - createPositionChange({ node, diff, dragging, nodeExtent, nodeInternals, snapToGrid, snapGrid }) - ); + changes.push(createPositionChange({ node, diff, nodeExtent, nodeInternals, snapToGrid, snapGrid })); } }); diff --git a/src/store/utils.ts b/src/store/utils.ts index e40366d9..a5104c6c 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -44,7 +44,7 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) const parentNodes: ParentNodes = {}; nodes.forEach((node) => { - const z = isNumeric(node.zIndex) ? node.zIndex : node.dragging || node.selected ? 1000 : 0; + const z = isNumeric(node.zIndex) ? node.zIndex : node.selected ? 1000 : 0; const currInternals = nodeInternals.get(node.id); const internals: Node = { @@ -115,7 +115,6 @@ type CreatePostionChangeParams = { nodeExtent: CoordinateExtent; nodeInternals: NodeInternals; diff?: XYPosition; - dragging?: boolean; snapToGrid?: boolean; snapGrid?: SnapGrid; }; @@ -123,7 +122,6 @@ type CreatePostionChangeParams = { export function createPositionChange({ node, diff, - dragging, nodeExtent, nodeInternals, snapToGrid, @@ -132,7 +130,6 @@ export function createPositionChange({ const change: NodePositionChange = { id: node.id, type: 'position', - dragging: !!dragging, }; if (diff) { diff --git a/src/types/changes.ts b/src/types/changes.ts index d896d03e..f833fbf0 100644 --- a/src/types/changes.ts +++ b/src/types/changes.ts @@ -13,7 +13,6 @@ export type NodePositionChange = { id: string; type: 'position'; position?: XYPosition; - dragging?: boolean; }; export type NodeSelectionChange = { diff --git a/src/types/nodes.ts b/src/types/nodes.ts index d8592cc4..4f0262b7 100644 --- a/src/types/nodes.ts +++ b/src/types/nodes.ts @@ -15,7 +15,6 @@ export interface Node { sourcePosition?: Position; hidden?: boolean; selected?: boolean; - dragging?: boolean; draggable?: boolean; selectable?: boolean; connectable?: boolean; @@ -79,7 +78,6 @@ export interface WrapNodeProps { sourcePosition: Position; targetPosition: Position; hidden?: boolean; - dragging: boolean; resizeObserver: ResizeObserver | null; dragHandle?: string; zIndex: number; @@ -96,7 +94,6 @@ export type NodeHandleBounds = { export type NodeDiffUpdate = { id?: string; diff?: XYPosition; - dragging?: boolean; }; export type NodeDimensionUpdate = { diff --git a/src/utils/changes.ts b/src/utils/changes.ts index e5278a6f..9279a3f0 100644 --- a/src/utils/changes.ts +++ b/src/utils/changes.ts @@ -69,10 +69,6 @@ function applyChanges(changes: any[], elements: any[]): any[] { updateItem.position = currentChange.position; } - if (typeof currentChange.dragging !== 'undefined') { - updateItem.dragging = currentChange.dragging; - } - if (updateItem.expandParent) { handleParentExpand(res, updateItem); } diff --git a/src/utils/graph.ts b/src/utils/graph.ts index 222a1e59..37765a01 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -176,7 +176,7 @@ export const getNodesInside = ( const visibleNodes: Node[] = []; nodeInternals.forEach((node) => { - const { positionAbsolute, width, height, dragging, selectable = true } = node; + const { positionAbsolute, width, height, selectable = true } = node; if (excludeNonSelectableNodes && !selectable) { return false; @@ -187,7 +187,7 @@ export const getNodesInside = ( const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const overlappingArea = Math.ceil(xOverlap * yOverlap); const notInitialized = - typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null || dragging; + typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null; const partiallyVisible = partially && overlappingArea > 0; const area = (width || 0) * (height || 0);