From a1cc9da535aea7c3907e4eaf7fdebfc5701bcf58 Mon Sep 17 00:00:00 2001 From: Richard Schulz Date: Tue, 29 Nov 2022 17:24:34 +0100 Subject: [PATCH 01/30] Add generic support for the type property on the Node type --- packages/core/src/types/nodes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/types/nodes.ts b/packages/core/src/types/nodes.ts index 39d8755c..4d4c66b1 100644 --- a/packages/core/src/types/nodes.ts +++ b/packages/core/src/types/nodes.ts @@ -5,11 +5,11 @@ import { internalsSymbol } from '../utils'; import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; // interface for the user node items -export type Node = { +export type Node = { id: string; position: XYPosition; data: T; - type?: string; + type: U; style?: CSSProperties; className?: string; sourcePosition?: Position; From 4d7a5e785ceaddf7b5932d624806161a04b30963 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 12 Jan 2023 14:34:39 +0100 Subject: [PATCH 02/30] feat(node-resizer): add onBeforeResize handler --- .../examples/NodeResizer/CustomResizer.tsx | 4 +-- .../examples/NodeResizer/CustomResizer2.tsx | 6 ++-- .../examples/NodeResizer/CustomResizer3.tsx | 31 +++++++++++++++++++ .../examples/NodeResizer/NodeResizerNode.tsx | 5 +++ .../src/examples/NodeResizer/index.tsx | 11 ++++++- packages/node-resizer/src/NodeResizer.tsx | 3 ++ packages/node-resizer/src/ResizeControl.tsx | 5 +++ packages/node-resizer/src/types.ts | 3 +- 8 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx index 996e24c5..6cf58fd9 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx @@ -1,7 +1,7 @@ -import { memo, FC, CSSProperties } from 'react'; +import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer'; +import { NodeResizeControl } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; import ResizeIcon from './ResizeIcon'; diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx index 27833556..63ff061b 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx @@ -1,14 +1,14 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer'; +import { NodeResizeControl } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; const CustomNode: FC = ({ id, data }) => { return ( <> - - + +
{data.label}
diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx new file mode 100644 index 00000000..adae9adb --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx @@ -0,0 +1,31 @@ +import { memo, FC } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; + +import { NodeResizeControl, ResizeDragEvent, ResizeEventParams } from '@reactflow/node-resizer'; +import '@reactflow/node-resizer/dist/style.css'; + +const onBeforeResize = (event: ResizeDragEvent, params: ResizeEventParams) => { + console.log('before resize', params); + + if (params.width > 100) { + return false; + } +}; + +const onResize = (event: ResizeDragEvent, params: ResizeEventParams) => { + console.log('resize', params); +}; + +const CustomNode: FC = ({ id, data }) => { + return ( + <> + + + +
{data.label}
+ + + ); +}; + +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index a8f76eca..0f6ef37e 100644 --- a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -8,6 +8,10 @@ const onResizeStart = (_: ResizeDragEvent, params: ResizeEventParams) => { console.log('resize start', params); }; +const onBeforeResize = (_: ResizeDragEvent, params: ResizeEventParams) => { + console.log('before resize', params); +}; + const onResize = (_: ResizeDragEvent, params: ResizeEventParams) => { console.log('resize', params); }; @@ -24,6 +28,7 @@ const CustomNode: FC = ({ data, selected }) => { minHeight={100} isVisible={selected} onResizeStart={onResizeStart} + onBeforeResize={onBeforeResize} onResize={onResize} onResizeEnd={onResizeEnd} /> diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index 68afe5df..01bb0851 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -4,11 +4,13 @@ import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useE import NodeResizerNode from './NodeResizerNode'; import CustomResizer from './CustomResizer'; import CustomResizer2 from './CustomResizer2'; +import CustomResizer3 from './CustomResizer3'; const nodeTypes = { resizer: NodeResizerNode, customResizer: CustomResizer, customResizer2: CustomResizer2, + customResizer3: CustomResizer3, }; const initialEdges = [ @@ -60,6 +62,13 @@ const initialNodes = [ position: { x: 250, y: 250 }, style: { border: '1px solid #222', fontSize: 10 }, }, + { + id: '6', + type: 'customResizer3', + data: { label: 'resize controls' }, + position: { x: 400, y: 200 }, + style: { border: '1px solid #222', fontSize: 10 }, + }, ]; const CustomNodeFlow = () => { @@ -87,7 +96,7 @@ const CustomNodeFlow = () => { > - + ); diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index f676e79a..9d36919b 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -15,6 +15,7 @@ export default function NodeResizer({ minWidth = 10, minHeight = 10, onResizeStart, + onBeforeResize, onResize, onResizeEnd, }: NodeResizerProps) { @@ -36,6 +37,7 @@ export default function NodeResizer({ minWidth={minWidth} minHeight={minHeight} onResizeStart={onResizeStart} + onBeforeResize={onBeforeResize} onResize={onResize} onResizeEnd={onResizeEnd} /> @@ -51,6 +53,7 @@ export default function NodeResizer({ minWidth={minWidth} minHeight={minHeight} onResizeStart={onResizeStart} + onBeforeResize={onBeforeResize} onResize={onResize} onResizeEnd={onResizeEnd} /> diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index 654b3249..6aebd256 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -32,6 +32,7 @@ function ResizeControl({ minWidth = 10, minHeight = 10, onResizeStart, + onBeforeResize, onResize, onResizeEnd, }: ResizeControlProps) { @@ -98,6 +99,7 @@ function ResizeControl({ const height = Math.max(startHeight + (invertY ? -distY : distY), minHeight); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; + const onBeforeResizeResult = onBeforeResize?.(event, { ...prevValues.current }); if (invertX || invertY) { const x = invertX ? startNodeX - (width - startWidth) : startNodeX; @@ -139,6 +141,9 @@ function ResizeControl({ prevValues.current.width = width; prevValues.current.height = height; } + if (onBeforeResizeResult === false) { + return; + } onResize?.(event, { ...prevValues.current }); triggerNodeChanges(changes); diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 9dc808f6..8388e89d 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -19,6 +19,7 @@ export type NodeResizerProps = { minWidth?: number; minHeight?: number; onResizeStart?: (event: ResizeDragEvent, params: ResizeEventParams) => void; + onBeforeResize?: (event: ResizeDragEvent, params: ResizeEventParams) => unknown; onResize?: (event: ResizeDragEvent, params: ResizeEventParams) => void; onResizeEnd?: (event: ResizeDragEvent, params: ResizeEventParams) => void; }; @@ -34,7 +35,7 @@ export enum ResizeControlVariant { export type ResizeControlProps = Pick< NodeResizerProps, - 'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'onResizeStart' | 'onResize' | 'onResizeEnd' + 'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'onResizeStart' | 'onBeforeResize' | 'onResize' | 'onResizeEnd' > & { position?: ControlPosition; variant?: ResizeControlVariant; From e6b71d474f7941e72b641bd1c040ec8e6c1abf8d Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 15 Jan 2023 11:56:46 +0100 Subject: [PATCH 03/30] feat(node-resizer): add direction, cleanup types --- .../examples/NodeResizer/CustomResizer3.tsx | 8 +++--- .../examples/NodeResizer/NodeResizerNode.tsx | 10 +++---- packages/node-resizer/src/ResizeControl.tsx | 20 ++++++++++++-- packages/node-resizer/src/types.ts | 21 +++++++++++---- packages/node-resizer/src/utils.ts | 26 +++++++++++++++++++ 5 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 packages/node-resizer/src/utils.ts diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx index adae9adb..33a3e5f5 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx @@ -1,10 +1,10 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizeControl, ResizeDragEvent, ResizeEventParams } from '@reactflow/node-resizer'; +import { NodeResizeControl, OnResize, OnBeforeResize } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; -const onBeforeResize = (event: ResizeDragEvent, params: ResizeEventParams) => { +const onBeforeResize: OnBeforeResize = (event, params) => { console.log('before resize', params); if (params.width > 100) { @@ -12,8 +12,8 @@ const onBeforeResize = (event: ResizeDragEvent, params: ResizeEventParams) => { } }; -const onResize = (event: ResizeDragEvent, params: ResizeEventParams) => { - console.log('resize', params); +const onResize: OnResize = (event, params) => { + console.log('resize', params.direction); }; const CustomNode: FC = ({ id, data }) => { diff --git a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index 0f6ef37e..e9f09894 100644 --- a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -1,22 +1,22 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizer, ResizeDragEvent, ResizeEventParams } from '@reactflow/node-resizer'; +import { NodeResizer, OnBeforeResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; -const onResizeStart = (_: ResizeDragEvent, params: ResizeEventParams) => { +const onResizeStart: OnResizeStart = (_, params) => { console.log('resize start', params); }; -const onBeforeResize = (_: ResizeDragEvent, params: ResizeEventParams) => { +const onBeforeResize: OnBeforeResize = (_, params) => { console.log('before resize', params); }; -const onResize = (_: ResizeDragEvent, params: ResizeEventParams) => { +const onResize: OnResize = (_, params) => { console.log('resize', params); }; -const onResizeEnd = (_: ResizeDragEvent, params: ResizeEventParams) => { +const onResizeEnd: OnResizeEnd = (_, params) => { console.log('resize end', params); }; diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index 6aebd256..138d2345 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -12,6 +12,7 @@ import { } from '@reactflow/core'; import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; +import { getDirection } from './utils'; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; @@ -99,7 +100,6 @@ function ResizeControl({ const height = Math.max(startHeight + (invertY ? -distY : distY), minHeight); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; - const onBeforeResizeResult = onBeforeResize?.(event, { ...prevValues.current }); if (invertX || invertY) { const x = invertX ? startNodeX - (width - startWidth) : startNodeX; @@ -141,11 +141,27 @@ function ResizeControl({ prevValues.current.width = width; prevValues.current.height = height; } + + if (changes.length === 0) { + return; + } + + const onBeforeResizeResult = onBeforeResize?.(event, { ...prevValues.current }); + if (onBeforeResizeResult === false) { return; } - onResize?.(event, { ...prevValues.current }); + const direction = getDirection({ + width: prevValues.current.width, + prevWidth, + height: prevValues.current.height, + prevHeight, + invertX, + invertY, + }); + + onResize?.(event, { ...prevValues.current, direction }); triggerNodeChanges(changes); } }) diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 8388e89d..2bd674db 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -1,13 +1,24 @@ import type { CSSProperties, ReactNode } from 'react'; import type { D3DragEvent, SubjectPosition } from 'd3-drag'; -export type ResizeEventParams = { +export type ResizeParams = { x: number; y: number; width: number; height: number; }; +export type ResizeParamsWithDirection = ResizeParams & { + direction: number[]; +}; + +type OnResizeHandler = (event: ResizeDragEvent, params: Params) => Result; + +export type OnResizeStart = OnResizeHandler; +export type OnBeforeResize = OnResizeHandler; +export type OnResize = OnResizeHandler; +export type OnResizeEnd = OnResizeHandler; + export type NodeResizerProps = { nodeId?: string; color?: string; @@ -18,10 +29,10 @@ export type NodeResizerProps = { isVisible?: boolean; minWidth?: number; minHeight?: number; - onResizeStart?: (event: ResizeDragEvent, params: ResizeEventParams) => void; - onBeforeResize?: (event: ResizeDragEvent, params: ResizeEventParams) => unknown; - onResize?: (event: ResizeDragEvent, params: ResizeEventParams) => void; - onResizeEnd?: (event: ResizeDragEvent, params: ResizeEventParams) => void; + onResizeStart?: OnResizeStart; + onBeforeResize?: OnBeforeResize; + onResize?: OnResize; + onResizeEnd?: OnResizeEnd; }; export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; diff --git a/packages/node-resizer/src/utils.ts b/packages/node-resizer/src/utils.ts new file mode 100644 index 00000000..00d81d81 --- /dev/null +++ b/packages/node-resizer/src/utils.ts @@ -0,0 +1,26 @@ +type GetDirectionParams = { + width: number; + prevWidth: number; + height: number; + prevHeight: number; + invertX: boolean; + invertY: boolean; +}; + +// returns an array of two numbers (0, 1 or -1) representing the direction of the resize +// 0 = no change, 1 = increase, -1 = decrease +export function getDirection({ width, prevWidth, height, prevHeight, invertX, invertY }: GetDirectionParams) { + const deltaWidth = width - prevWidth; + const deltaHeight = height - prevHeight; + + const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0]; + + if (deltaWidth && invertX) { + direction[0] = direction[0] * -1; + } + + if (deltaHeight && invertY) { + direction[1] = direction[1] * -1; + } + return direction; +} From 1ab7a0f0d9368e86cece63f34960464b0035f2f1 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jan 2023 14:39:11 +0100 Subject: [PATCH 04/30] refactor(node-resizer): rename onBeforeResize to shouldResize, cleanup types --- .../examples/NodeResizer/CustomResizer3.tsx | 8 +++++--- .../examples/NodeResizer/NodeResizerNode.tsx | 14 ++++++++------ packages/node-resizer/src/NodeResizer.tsx | 6 +++--- packages/node-resizer/src/ResizeControl.tsx | 18 ++++++++++-------- packages/node-resizer/src/types.ts | 6 +++--- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx index 33a3e5f5..422f2982 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer3.tsx @@ -1,15 +1,17 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizeControl, OnResize, OnBeforeResize } from '@reactflow/node-resizer'; +import { NodeResizeControl, OnResize, ShouldResize } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; -const onBeforeResize: OnBeforeResize = (event, params) => { +const shouldResize: ShouldResize = (event, params) => { console.log('before resize', params); if (params.width > 100) { return false; } + + return true; }; const onResize: OnResize = (event, params) => { @@ -20,7 +22,7 @@ const CustomNode: FC = ({ id, data }) => { return ( <> - +
{data.label}
diff --git a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index e9f09894..03e4960f 100644 --- a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -1,17 +1,13 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; -import { NodeResizer, OnBeforeResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer'; +import { NodeResizer, ShouldResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; const onResizeStart: OnResizeStart = (_, params) => { console.log('resize start', params); }; -const onBeforeResize: OnBeforeResize = (_, params) => { - console.log('before resize', params); -}; - const onResize: OnResize = (_, params) => { console.log('resize', params); }; @@ -20,6 +16,12 @@ const onResizeEnd: OnResizeEnd = (_, params) => { console.log('resize end', params); }; +const shouldResize: ShouldResize = (_, params) => { + console.log('should resize', params); + + return true; +}; + const CustomNode: FC = ({ data, selected }) => { return ( <> @@ -27,8 +29,8 @@ const CustomNode: FC = ({ data, selected }) => { minWidth={100} minHeight={100} isVisible={selected} + shouldResize={shouldResize} onResizeStart={onResizeStart} - onBeforeResize={onBeforeResize} onResize={onResize} onResizeEnd={onResizeEnd} /> diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 9d36919b..c7a3b8a6 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -14,8 +14,8 @@ export default function NodeResizer({ color, minWidth = 10, minHeight = 10, + shouldResize, onResizeStart, - onBeforeResize, onResize, onResizeEnd, }: NodeResizerProps) { @@ -37,7 +37,7 @@ export default function NodeResizer({ minWidth={minWidth} minHeight={minHeight} onResizeStart={onResizeStart} - onBeforeResize={onBeforeResize} + shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} /> @@ -53,7 +53,7 @@ export default function NodeResizer({ minWidth={minWidth} minHeight={minHeight} onResizeStart={onResizeStart} - onBeforeResize={onBeforeResize} + shouldResize={shouldResize} onResize={onResize} onResizeEnd={onResizeEnd} /> diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index 138d2345..3397537d 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -32,8 +32,8 @@ function ResizeControl({ color, minWidth = 10, minHeight = 10, + shouldResize, onResizeStart, - onBeforeResize, onResize, onResizeEnd, }: ResizeControlProps) { @@ -146,12 +146,6 @@ function ResizeControl({ return; } - const onBeforeResizeResult = onBeforeResize?.(event, { ...prevValues.current }); - - if (onBeforeResizeResult === false) { - return; - } - const direction = getDirection({ width: prevValues.current.width, prevWidth, @@ -161,7 +155,15 @@ function ResizeControl({ invertY, }); - onResize?.(event, { ...prevValues.current, direction }); + const nextValues = { ...prevValues.current, direction }; + + const callResize = shouldResize?.(event, nextValues); + + if (callResize === false) { + return; + } + + onResize?.(event, nextValues); triggerNodeChanges(changes); } }) diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 2bd674db..75d40ace 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -14,8 +14,8 @@ export type ResizeParamsWithDirection = ResizeParams & { type OnResizeHandler = (event: ResizeDragEvent, params: Params) => Result; +export type ShouldResize = OnResizeHandler; export type OnResizeStart = OnResizeHandler; -export type OnBeforeResize = OnResizeHandler; export type OnResize = OnResizeHandler; export type OnResizeEnd = OnResizeHandler; @@ -29,8 +29,8 @@ export type NodeResizerProps = { isVisible?: boolean; minWidth?: number; minHeight?: number; + shouldResize?: ShouldResize; onResizeStart?: OnResizeStart; - onBeforeResize?: OnBeforeResize; onResize?: OnResize; onResizeEnd?: OnResizeEnd; }; @@ -46,7 +46,7 @@ export enum ResizeControlVariant { export type ResizeControlProps = Pick< NodeResizerProps, - 'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'onResizeStart' | 'onBeforeResize' | 'onResize' | 'onResizeEnd' + 'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'shouldResize' | 'onResizeStart' | 'onResize' | 'onResizeEnd' > & { position?: ControlPosition; variant?: ResizeControlVariant; From e413bceb196553f33e5c55893732f12ceaaf860f Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 16 Jan 2023 16:39:48 +0100 Subject: [PATCH 05/30] 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 06/30] 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} >