From e413bceb196553f33e5c55893732f12ceaaf860f Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jan 2023 16:39:48 +0100 Subject: [PATCH] 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 => ({