From e413bceb196553f33e5c55893732f12ceaaf860f Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jan 2023 16:39:48 +0100 Subject: [PATCH 1/9] feat(connecting): move pane when mouse is close to edge of canvas #2753 --- .../core/src/components/Handle/handler.ts | 64 ++++++++++++++----- packages/core/src/store/index.ts | 19 ++++++ packages/core/src/types/general.ts | 1 + packages/core/src/utils/index.ts | 1 + 4 files changed, 69 insertions(+), 16 deletions(-) diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index 0052120f..133090d6 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -1,9 +1,9 @@ import type { MouseEvent as ReactMouseEvent } from 'react'; import { StoreApi } from 'zustand'; -import { getHostForElement } from '../../utils'; +import { clamp, getHostForElement } from '../../utils'; import { ConnectionMode } from '../../types'; -import type { OnConnect, Connection, HandleType, ReactFlowState } from '../../types'; +import type { OnConnect, Connection, HandleType, ReactFlowState, XYPosition } from '../../types'; type ValidConnectionFunc = (connection: Connection) => boolean; @@ -75,6 +75,18 @@ function resetRecentHandle(hoveredHandle: Element): void { hoveredHandle?.classList.remove('react-flow__handle-connecting'); } +// returns a number between 0 and 1 that represents the velocity of the movement +// when the mouse is close to the edge of the canvas +function getVelocity(value: number, min: number, max: number): number { + if (value < min) { + return clamp(Math.abs(value - min), 1, 100) / 100; + } else if (value > max) { + return -clamp(Math.abs(value - max), 1, 100) / 100; + } + + return 0; +} + export function handleMouseDown({ event, handleId, @@ -98,11 +110,26 @@ export function handleMouseDown({ elementEdgeUpdaterType?: HandleType; onEdgeUpdateEnd?: (evt: MouseEvent) => void; }): void { - const reactFlowNode = (event.target as Element).closest('.react-flow'); // when react-flow is used inside a shadow root we can't use document const doc = getHostForElement(event.target as HTMLElement); + const { onConnectStart, connectionMode, domNode } = getState(); + let connectionPosition: XYPosition | null = null; + let requestAnimationFrameId = 0; - if (!doc) { + // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas + const updateViewport = (): void => { + if (!connectionPosition || !containerBounds) { + return; + } + + const xMovement = getVelocity(connectionPosition.x, 35, containerBounds.width - 35) * 20; + const yMovement = getVelocity(connectionPosition.y, 35, containerBounds.height - 35) * 20; + + getState().movePane({ x: xMovement, y: yMovement }); + requestAnimationFrameId = requestAnimationFrame(updateViewport); + }; + + if (!doc || !domNode) { return; } @@ -110,33 +137,36 @@ export function handleMouseDown({ const elementBelowIsTarget = elementBelow?.classList.contains('target'); const elementBelowIsSource = elementBelow?.classList.contains('source'); - if (!reactFlowNode || (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType)) { + if (!elementBelowIsTarget && !elementBelowIsSource && !elementEdgeUpdaterType) { return; } - const { onConnectStart, connectionMode } = getState(); const handleType = elementEdgeUpdaterType ? elementEdgeUpdaterType : elementBelowIsTarget ? 'target' : 'source'; - const containerBounds = reactFlowNode.getBoundingClientRect(); + const containerBounds = domNode.getBoundingClientRect(); let recentHoveredHandle: Element; + connectionPosition = { + x: event.clientX - containerBounds.left, + y: event.clientY - containerBounds.top, + }; + + updateViewport(); setState({ - connectionPosition: { - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top, - }, + connectionPosition, connectionNodeId: nodeId, connectionHandleId: handleId, connectionHandleType: handleType, }); - onConnectStart?.(event, { nodeId, handleId, handleType }); function onMouseMove(event: MouseEvent) { + connectionPosition = { + x: event.clientX - containerBounds.left, + y: event.clientY - containerBounds.top, + }; + setState({ - connectionPosition: { - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top, - }, + connectionPosition, }); const { connection, elementBelow, isValid, isHoveringHandle } = checkElementBelowIsValid( @@ -162,6 +192,8 @@ export function handleMouseDown({ } function onMouseUp(event: MouseEvent) { + cancelAnimationFrame(requestAnimationFrameId); + const { connection, isValid } = checkElementBelowIsValid( event, connectionMode, diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 5c4d65a6..4efac70f 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -1,4 +1,5 @@ import { createStore } from 'zustand'; +import { zoomIdentity } from 'd3-zoom'; import { clampPosition, getDimensions, internalsSymbol } from '../utils'; import { applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -19,6 +20,7 @@ import type { UnselectNodesAndEdgesParams, NodeChange, } from '../types'; +import { XYPosition } from 'react-flow-renderer'; const createRFStore = () => createStore((set, get) => ({ @@ -250,6 +252,23 @@ const createRFStore = () => nodeInternals: new Map(nodeInternals), }); }, + movePane: (delta: XYPosition) => { + const { transform, width, height, d3Zoom, d3Selection, translateExtent } = get(); + + if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) { + return; + } + + const nextTransform = zoomIdentity.translate(transform[0] + delta.x, transform[1] + delta.y).scale(transform[2]); + + const extent: CoordinateExtent = [ + [0, 0], + [width, height], + ]; + + const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, translateExtent); + d3Zoom.transform(d3Selection, constrainedTransform); + }, cancelConnection: () => set({ connectionNodeId: initialState.connectionNodeId, diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 9d60e839..6ec5e103 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -230,6 +230,7 @@ export type ReactFlowActions = { cancelConnection: () => void; reset: () => void; triggerNodeChanges: (changes: NodeChange[]) => void; + movePane: (delta: XYPosition) => void; }; export type ReactFlowState = ReactFlowStore & ReactFlowActions; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 85220467..45d9c5e4 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,4 +1,5 @@ import type { KeyboardEvent as ReactKeyboardEvent } from 'react'; + import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types'; export const getDimensions = (node: HTMLDivElement): Dimensions => ({ From 86f7a9a43d051463ddb87c32d5aea0e69c4422bc Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jan 2023 18:23:20 +0100 Subject: [PATCH 2/9] feat(nodes): move canvas when nodes are at the edge of the canvas #2753 --- .../vite-app/src/examples/Switch/index.tsx | 7 +- .../core/src/components/Handle/handler.ts | 14 +- packages/core/src/hooks/useDrag/index.ts | 153 ++++++++++++------ packages/core/src/utils/index.ts | 12 ++ 4 files changed, 118 insertions(+), 68 deletions(-) diff --git a/examples/vite-app/src/examples/Switch/index.tsx b/examples/vite-app/src/examples/Switch/index.tsx index eb5d6519..7fde6bbd 100644 --- a/examples/vite-app/src/examples/Switch/index.tsx +++ b/examples/vite-app/src/examples/Switch/index.tsx @@ -1,4 +1,4 @@ -import React, { MouseEvent, useCallback } from 'react'; +import { MouseEvent, useCallback } from 'react'; import ReactFlow, { addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from 'reactflow'; const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -35,7 +35,7 @@ const nodesA: Node[] = [ ]; const edgesA: Edge[] = [ - { id: 'e1-2', source: '1a', target: '2a', ariaLabel: null }, + { id: 'e1-2', source: '1a', target: '2a', ariaLabel: undefined }, { id: 'e1-3', source: '1a', target: '3a' }, ]; @@ -83,6 +83,8 @@ const edgesB: Edge[] = [ { id: 'e4b', source: 'inputb', target: '4b' }, ]; +const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node.position); + const BasicFlow = () => { const [nodes, setNodes, onNodesChange] = useNodesState(nodesA); const [edges, setEdges, onEdgesChange] = useEdgesState(edgesA); @@ -99,6 +101,7 @@ const BasicFlow = () => { onConnect={onConnect} onNodeDragStop={onNodeDragStop} disableKeyboardA11y + onNodeDrag={onNodeDrag} >