From f776239ecbc9217135bc2e82a79c4463b8b2266d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 22 Nov 2022 16:13:23 +0100 Subject: [PATCH 01/24] feat(resize-node): example implementation of resize component --- .../vite-app/src/examples/Basic/index.tsx | 2 +- .../core/src/components/Nodes/NodeResizer.tsx | 137 ++++++++++++++++++ .../core/src/components/Nodes/wrapNode.tsx | 3 + 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/components/Nodes/NodeResizer.tsx diff --git a/examples/vite-app/src/examples/Basic/index.tsx b/examples/vite-app/src/examples/Basic/index.tsx index 92077b35..dfba56ef 100644 --- a/examples/vite-app/src/examples/Basic/index.tsx +++ b/examples/vite-app/src/examples/Basic/index.tsx @@ -94,7 +94,7 @@ const BasicFlow = () => { fitView defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} - nodeOrigin={nodeOrigin} + // nodeOrigin={nodeOrigin} > diff --git a/packages/core/src/components/Nodes/NodeResizer.tsx b/packages/core/src/components/Nodes/NodeResizer.tsx new file mode 100644 index 00000000..e5e2024c --- /dev/null +++ b/packages/core/src/components/Nodes/NodeResizer.tsx @@ -0,0 +1,137 @@ +import { useRef, useEffect } from 'react'; +import { drag } from 'd3-drag'; +import { select } from 'd3-selection'; +import type { D3DragEvent, SubjectPosition } from 'd3'; +import { useStoreApi } from '../../hooks/useStore'; +import { Dimensions, Node, XYPosition } from '../../types'; + +type NodeResizerProps = { + nodeId: string; +}; + +type ResizeHandleProps = { + nodeId: string; + invertX?: boolean; + invertY?: boolean; + enableX?: boolean; + enableY?: boolean; + top?: number | string; + left?: number | string; +}; + +type ResizeDragEvent = D3DragEvent; + +function ResizeHandle({ + top = 0, + left = 0, + invertX = false, + invertY = false, + enableX = false, + enableY = false, + nodeId, +}: ResizeHandleProps) { + const store = useStoreApi(); + const resizeHandleRef = useRef(null); + const initialDimensionsRef = useRef({ width: 0, height: 0, x: 0, y: 0 }); + const nodeElementRef = useRef(null); + + useEffect(() => { + if (!resizeHandleRef.current) { + return; + } + + const selection = select(resizeHandleRef.current); + const dragHandler = drag() + .on('start', () => { + const { transform, nodeInternals } = store.getState(); + const node = nodeInternals.get(nodeId); + const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; + const bbox = nodeElement.getBoundingClientRect(); + initialDimensionsRef.current = { + width: bbox.width / transform[2], + height: bbox.height / transform[2], + x: node?.position.x || 0, + y: node?.position.y || 0, + }; + nodeElementRef.current = nodeElement; + }) + .on('drag', (evt: ResizeDragEvent) => { + const { transform, updateNodePositions, nodeInternals } = store.getState(); + + const nodeEl = nodeElementRef.current; + const node = nodeInternals.get(nodeId); + + if (nodeEl && node) { + const moveX = invertX ? -evt.x : evt.x; + const moveY = invertY ? -evt.y : evt.y; + const distX = enableX ? moveX - evt.subject.x : 0; + const distY = enableY ? moveY - evt.subject.y : 0; + const dragX = distX / transform[2]; + const dragY = distY / transform[2]; + + const xPos = invertX ? initialDimensionsRef.current.x - dragX : initialDimensionsRef.current.x; + const yPos = invertY ? initialDimensionsRef.current.y - dragY : initialDimensionsRef.current.y; + + console.log(dragX); + + nodeEl.style.width = `${initialDimensionsRef.current.width + dragX}px`; + nodeEl.style.height = `${initialDimensionsRef.current.height + dragY}px`; + nodeEl.style.transform = `translate(${xPos}px, ${yPos}px)`; + + if (invertX || invertY) { + const positionUpdate = { x: xPos, y: yPos }; + + updateNodePositions( + [ + { + id: nodeId, + position: positionUpdate, + // positionAbsolute: node.positionAbsolute, + } as Node, + ], + true, + false + ); + } + } + }); + + selection.call(dragHandler); + + return () => { + selection.on('.drag', null); + }; + }, [nodeId]); + + return ( +
+ ); +} + +export default function NodeResizer({ nodeId }: NodeResizerProps) { + return ( + <> + + + + + + + + + + ); +} diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx index afb1f761..ba5daea6 100644 --- a/packages/core/src/components/Nodes/wrapNode.tsx +++ b/packages/core/src/components/Nodes/wrapNode.tsx @@ -2,6 +2,8 @@ import { useEffect, useRef, memo } from 'react'; import type { ComponentType, MouseEvent, KeyboardEvent } from 'react'; import cc from 'classcat'; +import NodeResizer from './NodeResizer'; + import { useStoreApi } from '../../hooks/useStore'; import { Provider } from '../../contexts/NodeIdContext'; import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; @@ -218,6 +220,7 @@ export default (NodeComponent: ComponentType) => { zIndex={zIndex} /> +
); }; From c6b5122d0f1d4df842a31c32084dd85366f520fc Mon Sep 17 00:00:00 2001 From: Wenchen Li <9028430+neo@users.noreply.github.com> Date: Mon, 28 Nov 2022 17:31:57 -0500 Subject: [PATCH 02/24] Invalidate a Node trying to connect itself with the same handle It also be written as `!(elementBelowNodeId === nodeId && elementBelowHandleId === handleId)` --- packages/core/src/components/Handle/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/components/Handle/handler.ts b/packages/core/src/components/Handle/handler.ts index ff726d30..0052120f 100644 --- a/packages/core/src/components/Handle/handler.ts +++ b/packages/core/src/components/Handle/handler.ts @@ -60,7 +60,7 @@ export function checkElementBelowIsValid( const isValid = connectionMode === ConnectionMode.Strict ? (isTarget && elementBelowIsSource) || (!isTarget && elementBelowIsTarget) - : true; + : elementBelowNodeId !== nodeId || elementBelowHandleId !== handleId; if (isValid) { result.isValid = isValidConnection(connection); From 6ee4eb4c42316735fae5443931af2c6783e04d25 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 1 Dec 2022 14:53:04 +0100 Subject: [PATCH 03/24] refactor(node-resizer): use package, update resize behaviour --- examples/vite-app/package.json | 1 + examples/vite-app/src/App/index.tsx | 6 ++ .../src/examples/NodeResizer/CustomNode.tsx | 18 ++++ .../src/examples/NodeResizer/index.tsx | 62 ++++++++++++ .../core/src/components/Nodes/wrapNode.tsx | 3 - packages/core/src/hooks/useDrag/index.ts | 26 +---- .../core/src/hooks/useGetPointerPosition.ts | 31 ++++++ packages/core/src/index.ts | 1 + packages/core/src/types/general.ts | 4 +- packages/node-resizer/.eslintrc.js | 4 + packages/node-resizer/CHANGELOG.md | 1 + packages/node-resizer/README.md | 10 ++ packages/node-resizer/package.json | 71 ++++++++++++++ .../src}/NodeResizer.tsx | 98 ++++++++++--------- packages/node-resizer/src/index.tsx | 2 + packages/node-resizer/src/types.ts | 9 ++ packages/node-resizer/tsconfig.json | 6 ++ pnpm-lock.yaml | 73 ++++++++------ 18 files changed, 323 insertions(+), 103 deletions(-) create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomNode.tsx create mode 100644 examples/vite-app/src/examples/NodeResizer/index.tsx create mode 100644 packages/core/src/hooks/useGetPointerPosition.ts create mode 100644 packages/node-resizer/.eslintrc.js create mode 100644 packages/node-resizer/CHANGELOG.md create mode 100644 packages/node-resizer/README.md create mode 100644 packages/node-resizer/package.json rename packages/{core/src/components/Nodes => node-resizer/src}/NodeResizer.tsx (52%) create mode 100644 packages/node-resizer/src/index.tsx create mode 100644 packages/node-resizer/src/types.ts create mode 100644 packages/node-resizer/tsconfig.json diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 677f7289..6554fc62 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -13,6 +13,7 @@ "test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'" }, "dependencies": { + "@reactflow/node-resizer": "workspace:^0.0.0", "classcat": "^5.0.3", "dagre": "^0.8.5", "localforage": "^1.10.0", diff --git a/examples/vite-app/src/App/index.tsx b/examples/vite-app/src/App/index.tsx index 5b596f48..bb68c028 100644 --- a/examples/vite-app/src/App/index.tsx +++ b/examples/vite-app/src/App/index.tsx @@ -20,6 +20,7 @@ import Intersection from '../examples/Intersection'; import Layouting from '../examples/Layouting'; import MultiFlows from '../examples/MultiFlows'; import NestedNodes from '../examples/NestedNodes'; +import NodeResizer from '../examples/NodeResizer'; import NodeTypeChange from '../examples/NodeTypeChange'; import NodeTypesObjectChange from '../examples/NodeTypesObjectChange'; import Overview from '../examples/Overview'; @@ -174,6 +175,11 @@ const routes: IRoute[] = [ path: '/node-toolbar', component: NodeToolbar, }, + { + name: 'NodeResizer', + path: '/node-resizer', + component: NodeResizer, + }, { name: 'Overview', path: '/overview', diff --git a/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx b/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx new file mode 100644 index 00000000..7a5dda5e --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx @@ -0,0 +1,18 @@ +import { memo, FC } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; + +import { NodeResizer } from '@reactflow/node-resizer'; + +const ColorSelectorNode: FC = ({ id, data }) => { + return ( + <> + +
{data.label}
+ + + + + ); +}; + +export default memo(ColorSelectorNode); diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx new file mode 100644 index 00000000..c6a4bcbc --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; +import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow'; + +import CustomNode from './CustomNode'; + +const nodeTypes = { + custom: CustomNode, +}; + +const initialEdges = [ + { + id: 'e1-2', + source: '1', + target: '2', + }, +]; + +const initialNodes = [ + { + id: '1', + type: 'input', + data: { label: 'An input node' }, + position: { x: 0, y: 0 }, + sourcePosition: Position.Right, + }, + { + id: '2', + type: 'custom', + data: { label: 'resize me!' }, + position: { x: 250, y: 0 }, + style: { padding: 10, border: '1px solid #222' }, + }, +]; + +const CustomNodeFlow = () => { + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); + + const onConnect = useCallback( + (connection: Connection) => setEdges((eds) => addEdge({ ...connection }, eds)), + [setEdges] + ); + + return ( + + + + ); +}; + +export default CustomNodeFlow; diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx index ba5daea6..afb1f761 100644 --- a/packages/core/src/components/Nodes/wrapNode.tsx +++ b/packages/core/src/components/Nodes/wrapNode.tsx @@ -2,8 +2,6 @@ import { useEffect, useRef, memo } from 'react'; import type { ComponentType, MouseEvent, KeyboardEvent } from 'react'; import cc from 'classcat'; -import NodeResizer from './NodeResizer'; - import { useStoreApi } from '../../hooks/useStore'; import { Provider } from '../../contexts/NodeIdContext'; import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; @@ -220,7 +218,6 @@ export default (NodeComponent: ComponentType) => { zIndex={zIndex} /> - ); }; diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts index 319c1993..1ececcad 100644 --- a/packages/core/src/hooks/useDrag/index.ts +++ b/packages/core/src/hooks/useDrag/index.ts @@ -1,15 +1,14 @@ -import { useEffect, useRef, useState, useCallback } from 'react'; +import { useEffect, useRef, useState } from 'react'; import type { RefObject, MouseEvent } from 'react'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import type { D3DragEvent, SubjectPosition } from 'd3'; import { useStoreApi } from '../../hooks/useStore'; import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils'; import { handleNodeClick } from '../../components/Nodes/utils'; -import type { NodeDragItem, Node, SelectionDragHandler } from '../../types'; +import useGetPointerPosition from '../useGetPointerPosition'; +import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent } from '../../types'; -export type UseDragEvent = D3DragEvent; export type UseDragData = { dx: number; dy: number }; type UseDragParams = { @@ -40,24 +39,7 @@ function useDrag({ const dragItems = useRef(); const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null }); - // returns the pointer position projected to the RF coordinate system - const getPointerPosition = useCallback(({ sourceEvent }: UseDragEvent) => { - const { transform, snapGrid, snapToGrid } = store.getState(); - const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX; - const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY; - - const pointerPos = { - x: (x - transform[0]) / transform[2], - y: (y - transform[1]) / transform[2], - }; - - // we need the snapped position in order to be able to skip unnecessary drag events - return { - xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x, - ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y, - ...pointerPos, - }; - }, []); + const getPointerPosition = useGetPointerPosition(); useEffect(() => { if (nodeRef?.current) { diff --git a/packages/core/src/hooks/useGetPointerPosition.ts b/packages/core/src/hooks/useGetPointerPosition.ts new file mode 100644 index 00000000..38c12470 --- /dev/null +++ b/packages/core/src/hooks/useGetPointerPosition.ts @@ -0,0 +1,31 @@ +import { useCallback } from 'react'; + +import { useStoreApi } from './useStore'; +import type { UseDragEvent } from '../types'; + +function useGetPointerPosition() { + const store = useStoreApi(); + + // returns the pointer position projected to the RF coordinate system + const getPointerPosition = useCallback(({ sourceEvent }: UseDragEvent) => { + const { transform, snapGrid, snapToGrid } = store.getState(); + const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX; + const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY; + + const pointerPos = { + x: (x - transform[0]) / transform[2], + y: (y - transform[1]) / transform[2], + }; + + // we need the snapped position in order to be able to skip unnecessary drag events + return { + xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x, + ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y, + ...pointerPos, + }; + }, []); + + return getPointerPosition; +} + +export default useGetPointerPosition; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e2e077db..53a0d9f1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -37,5 +37,6 @@ export { useStore, useStoreApi } from './hooks/useStore'; export { default as useOnViewportChange } from './hooks/useOnViewportChange'; export { default as useOnSelectionChange } from './hooks/useOnSelectionChange'; export { default as useNodesInitialized } from './hooks/useNodesInitialized'; +export { default as useGetPointerPosition } from './hooks/useGetPointerPosition'; export * from './types'; diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index b18f4d32..1ceb1cff 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } from 'react'; -import type { Selection as D3Selection, ZoomBehavior } from 'd3'; +import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; import type { NodeChange, EdgeChange } from './changes'; @@ -245,3 +245,5 @@ export type ProOptions = { account?: string; hideAttribution: boolean; }; + +export type UseDragEvent = D3DragEvent; diff --git a/packages/node-resizer/.eslintrc.js b/packages/node-resizer/.eslintrc.js new file mode 100644 index 00000000..31cfbd11 --- /dev/null +++ b/packages/node-resizer/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['@reactflow/eslint-config'], +}; diff --git a/packages/node-resizer/CHANGELOG.md b/packages/node-resizer/CHANGELOG.md new file mode 100644 index 00000000..a5e10d68 --- /dev/null +++ b/packages/node-resizer/CHANGELOG.md @@ -0,0 +1 @@ +# @reactflow/node-resizer diff --git a/packages/node-resizer/README.md b/packages/node-resizer/README.md new file mode 100644 index 00000000..d808473e --- /dev/null +++ b/packages/node-resizer/README.md @@ -0,0 +1,10 @@ +# @reactflow/node-resizer + +A resizer component for React Flow that can be attached to a node. + +## Installation + +```sh +npm install @reactflow/node-resizer +``` + diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json new file mode 100644 index 00000000..43ee7540 --- /dev/null +++ b/packages/node-resizer/package.json @@ -0,0 +1,71 @@ +{ + "name": "@reactflow/node-resizer", + "version": "0.0.0", + "description": "A helper component for resizing nodes.", + "keywords": [ + "react", + "node-based UI", + "graph", + "diagram", + "workflow", + "react-flow" + ], + "files": [ + "dist" + ], + "source": "src/index.tsx", + "main": "dist/umd/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "sideEffects": [ + "*.css" + ], + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/wbkd/react-flow.git", + "directory": "packages/node-resizer" + }, + "scripts": { + "dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch", + "build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production", + "lint": "eslint --ext .js,.jsx,.ts,.tsx src", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@babel/runtime": "^7.18.9", + "@reactflow/core": "workspace:*", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.1.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + }, + "devDependencies": { + "@reactflow/eslint-config": "workspace:*", + "@reactflow/rollup-config": "workspace:*", + "@reactflow/tsconfig": "workspace:*", + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/node": "^18.7.16", + "@types/react": "^18.0.19", + "@types/react-dom": "^18.0.6", + "react": "^18.2.0", + "typescript": "^4.8.3" + }, + "rollup": { + "globals": { + "zustand": "Zustand", + "zustand/shallow": "zustandShallow", + "classcat": "cc" + }, + "name": "ReactFlowNodeResizer" + } +} diff --git a/packages/core/src/components/Nodes/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx similarity index 52% rename from packages/core/src/components/Nodes/NodeResizer.tsx rename to packages/node-resizer/src/NodeResizer.tsx index e5e2024c..7a938682 100644 --- a/packages/core/src/components/Nodes/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -2,8 +2,8 @@ import { useRef, useEffect } from 'react'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; import type { D3DragEvent, SubjectPosition } from 'd3'; -import { useStoreApi } from '../../hooks/useStore'; -import { Dimensions, Node, XYPosition } from '../../types'; +import { useStoreApi, useGetPointerPosition } from '@reactflow/core'; +import type { Dimensions, Node, XYPosition } from '@reactflow/core'; type NodeResizerProps = { nodeId: string; @@ -32,8 +32,16 @@ function ResizeHandle({ }: ResizeHandleProps) { const store = useStoreApi(); const resizeHandleRef = useRef(null); - const initialDimensionsRef = useRef({ width: 0, height: 0, x: 0, y: 0 }); + const initialDimensionsRef = useRef({ + width: 0, + height: 0, + x: 0, + y: 0, + nodeX: 0, + nodeY: 0, + }); const nodeElementRef = useRef(null); + const getPointerPosition = useGetPointerPosition(); useEffect(() => { if (!resizeHandleRef.current) { @@ -42,56 +50,55 @@ function ResizeHandle({ const selection = select(resizeHandleRef.current); const dragHandler = drag() - .on('start', () => { - const { transform, nodeInternals } = store.getState(); - const node = nodeInternals.get(nodeId); - const nodeElement = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; - const bbox = nodeElement.getBoundingClientRect(); - initialDimensionsRef.current = { - width: bbox.width / transform[2], - height: bbox.height / transform[2], - x: node?.position.x || 0, - y: node?.position.y || 0, - }; - nodeElementRef.current = nodeElement; - }) - .on('drag', (evt: ResizeDragEvent) => { - const { transform, updateNodePositions, nodeInternals } = store.getState(); + .on('start', (event: ResizeDragEvent) => { + const node = store.getState().nodeInternals.get(nodeId); + const pointerPos = getPointerPosition(event); + initialDimensionsRef.current = { + width: node?.width ?? 0, + height: node?.height ?? 0, + nodeX: node?.position.x ?? 0, + nodeY: node?.position.y ?? 0, + x: pointerPos.xSnapped, + y: pointerPos.ySnapped, + }; + nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; + }) + .on('drag', (event: ResizeDragEvent) => { + const { updateNodePositions, nodeInternals } = store.getState(); + const pointerPos = getPointerPosition(event); const nodeEl = nodeElementRef.current; const node = nodeInternals.get(nodeId); if (nodeEl && node) { - const moveX = invertX ? -evt.x : evt.x; - const moveY = invertY ? -evt.y : evt.y; - const distX = enableX ? moveX - evt.subject.x : 0; - const distY = enableY ? moveY - evt.subject.y : 0; - const dragX = distX / transform[2]; - const dragY = distY / transform[2]; - - const xPos = invertX ? initialDimensionsRef.current.x - dragX : initialDimensionsRef.current.x; - const yPos = invertY ? initialDimensionsRef.current.y - dragY : initialDimensionsRef.current.y; - - console.log(dragX); - - nodeEl.style.width = `${initialDimensionsRef.current.width + dragX}px`; - nodeEl.style.height = `${initialDimensionsRef.current.height + dragY}px`; - nodeEl.style.transform = `translate(${xPos}px, ${yPos}px)`; + const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0; + const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0; + const width = initialDimensionsRef.current.width + (invertX ? -distX : distX); + const height = initialDimensionsRef.current.height + (invertY ? -distY : distY); if (invertX || invertY) { - const positionUpdate = { x: xPos, y: yPos }; + const x = invertX ? initialDimensionsRef.current.nodeX + distX : node.position.x; + const y = invertY ? initialDimensionsRef.current.nodeY + distY : node.position.y; - updateNodePositions( - [ - { - id: nodeId, - position: positionUpdate, - // positionAbsolute: node.positionAbsolute, - } as Node, - ], - true, - false - ); + if (x !== node.position.x || y !== node.position.y) { + updateNodePositions( + [ + { + id: nodeId, + position: { x, y }, + } as Node, + ], + true, + false + ); + } + } + + if (width !== node.width) { + nodeEl.style.width = `${width}px`; + } + if (height !== node.height) { + nodeEl.style.height = `${height}px`; } } }); @@ -114,6 +121,7 @@ function ResizeHandle({ width: 10, height: 10, background: 'red', + opacity: 0.7, borderRadius: '50%', transform: 'translate(-50%, -50%)', }} diff --git a/packages/node-resizer/src/index.tsx b/packages/node-resizer/src/index.tsx new file mode 100644 index 00000000..ea5bf55b --- /dev/null +++ b/packages/node-resizer/src/index.tsx @@ -0,0 +1,2 @@ +export { default as NodeResizer } from './NodeResizer'; +export * from './types'; diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts new file mode 100644 index 00000000..1e6576b6 --- /dev/null +++ b/packages/node-resizer/src/types.ts @@ -0,0 +1,9 @@ +import { Position } from '@reactflow/core'; +import type { HTMLAttributes } from 'react'; + +export type NodeToolbarProps = HTMLAttributes & { + nodeId: string; + isVisible?: boolean; + position?: Position; + offset?: number; +}; diff --git a/packages/node-resizer/tsconfig.json b/packages/node-resizer/tsconfig.json new file mode 100644 index 00000000..d0e5a1ce --- /dev/null +++ b/packages/node-resizer/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@reactflow/tsconfig/react.json", + "display": "@reactflow/node-resizer", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9414c1..a8406c2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,7 @@ importers: examples/vite-app: specifiers: '@cypress/skip-test': ^2.6.1 + '@reactflow/node-resizer': workspace:^0.0.0 '@types/react': ^18.0.17 '@types/react-dom': ^18.0.6 '@vitejs/plugin-react': ^2.1.0 @@ -73,6 +74,7 @@ importers: typescript: ^4.8.3 vite: ^3.1.0 dependencies: + '@reactflow/node-resizer': link:../../packages/node-resizer classcat: registry.npmjs.org/classcat/5.0.4 dagre: registry.npmjs.org/dagre/0.8.5 localforage: registry.npmjs.org/localforage/1.10.0 @@ -215,6 +217,45 @@ importers: react: registry.npmjs.org/react/18.2.0 typescript: registry.npmjs.org/typescript/4.8.3 + packages/node-resizer: + specifiers: + '@babel/runtime': ^7.18.9 + '@reactflow/core': workspace:* + '@reactflow/eslint-config': workspace:* + '@reactflow/rollup-config': workspace:* + '@reactflow/tsconfig': workspace:* + '@types/d3': ^7.4.0 + '@types/d3-drag': ^3.0.1 + '@types/d3-selection': ^3.0.3 + '@types/node': ^18.7.16 + '@types/react': ^18.0.19 + '@types/react-dom': ^18.0.6 + classcat: ^5.0.3 + d3-drag: ^3.0.0 + d3-selection: ^3.0.0 + react: ^18.2.0 + typescript: ^4.8.3 + zustand: ^4.1.1 + dependencies: + '@babel/runtime': registry.npmjs.org/@babel/runtime/7.19.0 + '@reactflow/core': link:../core + classcat: registry.npmjs.org/classcat/5.0.4 + d3-drag: registry.npmjs.org/d3-drag/3.0.0 + d3-selection: registry.npmjs.org/d3-selection/3.0.0 + zustand: registry.npmjs.org/zustand/4.1.1_react@18.2.0 + devDependencies: + '@reactflow/eslint-config': link:../../tooling/eslint-config + '@reactflow/rollup-config': link:../../tooling/rollup-config + '@reactflow/tsconfig': link:../../tooling/tsconfig + '@types/d3': registry.npmjs.org/@types/d3/7.4.0 + '@types/d3-drag': registry.npmjs.org/@types/d3-drag/3.0.1 + '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 + '@types/node': registry.npmjs.org/@types/node/18.7.16 + '@types/react': registry.npmjs.org/@types/react/18.0.19 + '@types/react-dom': registry.npmjs.org/@types/react-dom/18.0.6 + react: registry.npmjs.org/react/18.2.0 + typescript: registry.npmjs.org/typescript/4.8.3 + packages/node-toolbar: specifiers: '@babel/runtime': ^7.18.9 @@ -1380,7 +1421,6 @@ packages: resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz} name: '@types/d3-array' version: 3.0.3 - dev: false registry.npmjs.org/@types/d3-axis/3.0.1: resolution: {integrity: sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz} @@ -1388,7 +1428,6 @@ packages: version: 3.0.1 dependencies: '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - dev: false registry.npmjs.org/@types/d3-brush/3.0.1: resolution: {integrity: sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz} @@ -1396,19 +1435,16 @@ packages: version: 3.0.1 dependencies: '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - dev: false registry.npmjs.org/@types/d3-chord/3.0.1: resolution: {integrity: sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz} name: '@types/d3-chord' version: 3.0.1 - dev: false registry.npmjs.org/@types/d3-color/3.1.0: resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz} name: '@types/d3-color' version: 3.1.0 - dev: false registry.npmjs.org/@types/d3-contour/3.0.1: resolution: {integrity: sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz} @@ -1417,19 +1453,16 @@ packages: dependencies: '@types/d3-array': registry.npmjs.org/@types/d3-array/3.0.3 '@types/geojson': registry.npmjs.org/@types/geojson/7946.0.10 - dev: false registry.npmjs.org/@types/d3-delaunay/6.0.1: resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz} name: '@types/d3-delaunay' version: 6.0.1 - dev: false registry.npmjs.org/@types/d3-dispatch/3.0.1: resolution: {integrity: sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz} name: '@types/d3-dispatch' version: 3.0.1 - dev: false registry.npmjs.org/@types/d3-drag/3.0.1: resolution: {integrity: sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz} @@ -1437,19 +1470,16 @@ packages: version: 3.0.1 dependencies: '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - dev: false registry.npmjs.org/@types/d3-dsv/3.0.0: resolution: {integrity: sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz} name: '@types/d3-dsv' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-ease/3.0.0: resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz} name: '@types/d3-ease' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-fetch/3.0.1: resolution: {integrity: sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz} @@ -1457,19 +1487,16 @@ packages: version: 3.0.1 dependencies: '@types/d3-dsv': registry.npmjs.org/@types/d3-dsv/3.0.0 - dev: false registry.npmjs.org/@types/d3-force/3.0.3: resolution: {integrity: sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz} name: '@types/d3-force' version: 3.0.3 - dev: false registry.npmjs.org/@types/d3-format/3.0.1: resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz} name: '@types/d3-format' version: 3.0.1 - dev: false registry.npmjs.org/@types/d3-geo/3.0.2: resolution: {integrity: sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz} @@ -1477,13 +1504,11 @@ packages: version: 3.0.2 dependencies: '@types/geojson': registry.npmjs.org/@types/geojson/7946.0.10 - dev: false registry.npmjs.org/@types/d3-hierarchy/3.1.0: resolution: {integrity: sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz} name: '@types/d3-hierarchy' version: 3.1.0 - dev: false registry.npmjs.org/@types/d3-interpolate/3.0.1: resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz} @@ -1491,37 +1516,31 @@ packages: version: 3.0.1 dependencies: '@types/d3-color': registry.npmjs.org/@types/d3-color/3.1.0 - dev: false registry.npmjs.org/@types/d3-path/3.0.0: resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz} name: '@types/d3-path' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-polygon/3.0.0: resolution: {integrity: sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz} name: '@types/d3-polygon' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-quadtree/3.0.2: resolution: {integrity: sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz} name: '@types/d3-quadtree' version: 3.0.2 - dev: false registry.npmjs.org/@types/d3-random/3.0.1: resolution: {integrity: sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz} name: '@types/d3-random' version: 3.0.1 - dev: false registry.npmjs.org/@types/d3-scale-chromatic/3.0.0: resolution: {integrity: sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz} name: '@types/d3-scale-chromatic' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-scale/4.0.2: resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz} @@ -1529,13 +1548,11 @@ packages: version: 4.0.2 dependencies: '@types/d3-time': registry.npmjs.org/@types/d3-time/3.0.0 - dev: false registry.npmjs.org/@types/d3-selection/3.0.3: resolution: {integrity: sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz} name: '@types/d3-selection' version: 3.0.3 - dev: false registry.npmjs.org/@types/d3-shape/3.1.0: resolution: {integrity: sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz} @@ -1543,25 +1560,21 @@ packages: version: 3.1.0 dependencies: '@types/d3-path': registry.npmjs.org/@types/d3-path/3.0.0 - dev: false registry.npmjs.org/@types/d3-time-format/4.0.0: resolution: {integrity: sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz} name: '@types/d3-time-format' version: 4.0.0 - dev: false registry.npmjs.org/@types/d3-time/3.0.0: resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz} name: '@types/d3-time' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-timer/3.0.0: resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz} name: '@types/d3-timer' version: 3.0.0 - dev: false registry.npmjs.org/@types/d3-transition/3.0.2: resolution: {integrity: sha512-jo5o/Rf+/u6uerJ/963Dc39NI16FQzqwOc54bwvksGAdVfvDrqDpVeq95bEvPtBwLCVZutAEyAtmSyEMxN7vxQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.2.tgz} @@ -1569,7 +1582,6 @@ packages: version: 3.0.2 dependencies: '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - dev: false registry.npmjs.org/@types/d3-zoom/3.0.1: resolution: {integrity: sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz} @@ -1578,7 +1590,6 @@ packages: dependencies: '@types/d3-interpolate': registry.npmjs.org/@types/d3-interpolate/3.0.1 '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - dev: false registry.npmjs.org/@types/d3/7.4.0: resolution: {integrity: sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz} @@ -1615,7 +1626,6 @@ packages: '@types/d3-timer': registry.npmjs.org/@types/d3-timer/3.0.0 '@types/d3-transition': registry.npmjs.org/@types/d3-transition/3.0.2 '@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1 - dev: false registry.npmjs.org/@types/estree/0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz} @@ -1633,7 +1643,6 @@ packages: resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz} name: '@types/geojson' version: 7946.0.10 - dev: false registry.npmjs.org/@types/is-ci/3.0.0: resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz} From f106feb9a931fe27e1c7c64f516474a9ac799882 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 1 Dec 2022 15:10:15 +0100 Subject: [PATCH 04/24] refactor(node-resizer): check change events --- .../src/examples/NodeResizer/index.tsx | 20 ++++++++++++++++--- packages/node-resizer/src/NodeResizer.tsx | 6 +++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index c6a4bcbc..3b176e39 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -1,5 +1,13 @@ import { useCallback } from 'react'; -import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow'; +import ReactFlow, { + Controls, + addEdge, + Position, + Connection, + useNodesState, + useEdgesState, + NodeChange, +} from 'reactflow'; import CustomNode from './CustomNode'; @@ -33,7 +41,7 @@ const initialNodes = [ ]; const CustomNodeFlow = () => { - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + const [nodes, setNodes, _onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = useCallback( @@ -41,6 +49,13 @@ const CustomNodeFlow = () => { [setEdges] ); + const onNodesChange = useCallback( + (changes: NodeChange[]) => { + _onNodesChange(changes); + }, + [_onNodesChange] + ); + return ( { onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes} - snapToGrid={true} fitView minZoom={0.3} maxZoom={2} diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 7a938682..9e6764bf 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -1,9 +1,9 @@ import { useRef, useEffect } from 'react'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import type { D3DragEvent, SubjectPosition } from 'd3'; import { useStoreApi, useGetPointerPosition } from '@reactflow/core'; import type { Dimensions, Node, XYPosition } from '@reactflow/core'; +import type { D3DragEvent, SubjectPosition } from 'd3'; type NodeResizerProps = { nodeId: string; @@ -77,8 +77,8 @@ function ResizeHandle({ const height = initialDimensionsRef.current.height + (invertY ? -distY : distY); if (invertX || invertY) { - const x = invertX ? initialDimensionsRef.current.nodeX + distX : node.position.x; - const y = invertY ? initialDimensionsRef.current.nodeY + distY : node.position.y; + const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX; + const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY; if (x !== node.position.x || y !== node.position.y) { updateNodePositions( From b511b15ef8b01ca97c860f3538ad1bd18e7a8639 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 1 Dec 2022 20:28:37 +0100 Subject: [PATCH 05/24] refactor(core): make it possible to batch changes --- .../core/src/container/NodeRenderer/index.tsx | 1 + packages/core/src/store/index.ts | 15 +++- packages/core/src/types/changes.ts | 1 + packages/core/src/types/general.ts | 9 +- packages/core/src/utils/changes.ts | 85 ++++++++++--------- 5 files changed, 67 insertions(+), 44 deletions(-) diff --git a/packages/core/src/container/NodeRenderer/index.tsx b/packages/core/src/container/NodeRenderer/index.tsx index 70a62dfe..389cd2d0 100644 --- a/packages/core/src/container/NodeRenderer/index.tsx +++ b/packages/core/src/container/NodeRenderer/index.tsx @@ -57,6 +57,7 @@ const NodeRenderer = (props: NodeRendererProps) => { nodeElement: entry.target as HTMLDivElement, forceUpdate: true, })); + updateNodeDimensions(updates); }); diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index 5ffd64c3..e05f2048 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -102,7 +102,12 @@ const createRFStore = () => onNodesChange?.(changes); } }, - updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => { + updateNodePositions: ( + nodeDragItems: NodeDragItem[] | Node[], + positionChanged = true, + dragging = false, + applyChanges = true + ) => { const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get(); if (hasDefaultNodes || onNodesChange) { @@ -128,9 +133,15 @@ const createRFStore = () => set({ nodeInternals: nextNodeInternals }); } - onNodesChange?.(changes); + if (applyChanges) { + onNodesChange?.(changes); + } } + + return changes; } + + return null; }, addSelectedNodes: (selectedNodeIds: string[]) => { const { multiSelectionActive, nodeInternals, edges } = get(); diff --git a/packages/core/src/types/changes.ts b/packages/core/src/types/changes.ts index b4395803..ec2f6693 100644 --- a/packages/core/src/types/changes.ts +++ b/packages/core/src/types/changes.ts @@ -8,6 +8,7 @@ export type NodeDimensionChange = { id: string; type: 'dimensions'; dimensions: Dimensions; + updateStyle?: boolean; }; export type NodePositionChange = { diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 1ceb1cff..2daed27b 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -3,7 +3,7 @@ import type { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3'; import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils'; -import type { NodeChange, EdgeChange } from './changes'; +import type { NodeChange, EdgeChange, NodePositionChange } from './changes'; import type { Node, NodeInternals, @@ -215,7 +215,12 @@ export type ReactFlowActions = { setEdges: (edges: Edge[]) => void; setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged: boolean, dragging: boolean) => void; + updateNodePositions: ( + nodeDragItems: NodeDragItem[] | Node[], + positionChanged: boolean, + dragging: boolean, + applyChanges?: boolean + ) => NodePositionChange[] | null; resetSelectedElements: () => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void; addSelectedNodes: (nodeIds: string[]) => void; diff --git a/packages/core/src/utils/changes.ts b/packages/core/src/utils/changes.ts index 897c1ffb..edfd0719 100644 --- a/packages/core/src/utils/changes.ts +++ b/packages/core/src/utils/changes.ts @@ -50,58 +50,63 @@ function applyChanges(changes: any[], elements: any[]): any[] { const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item); return elements.reduce((res: any[], item: any) => { - const currentChange = changes.find((c) => c.id === item.id); + const currentChanges = changes.filter((c) => c.id === item.id); - if (currentChange) { - switch (currentChange.type) { - case 'select': { - res.push({ ...item, selected: currentChange.selected }); - return res; - } - case 'position': { - const updateItem = { ...item }; + if (currentChanges.length === 0) { + res.push(item); + return res; + } - if (typeof currentChange.position !== 'undefined') { - updateItem.position = currentChange.position; + const updateItem = { ...item }; + + for (const currentChange of currentChanges) { + if (currentChange) { + switch (currentChange.type) { + case 'select': { + updateItem.selected = currentChange.selected; + break; } + case 'position': { + if (typeof currentChange.position !== 'undefined') { + updateItem.position = currentChange.position; + } - if (typeof currentChange.positionAbsolute !== 'undefined') { - updateItem.positionAbsolute = currentChange.positionAbsolute; + if (typeof currentChange.positionAbsolute !== 'undefined') { + updateItem.positionAbsolute = currentChange.positionAbsolute; + } + + if (typeof currentChange.dragging !== 'undefined') { + updateItem.dragging = currentChange.dragging; + } + + if (updateItem.expandParent) { + handleParentExpand(res, updateItem); + } + break; } + case 'dimensions': { + if (typeof currentChange.dimensions !== 'undefined') { + updateItem.width = currentChange.dimensions.width; + updateItem.height = currentChange.dimensions.height; + } - if (typeof currentChange.dragging !== 'undefined') { - updateItem.dragging = currentChange.dragging; + if (typeof currentChange.updateStyle !== 'undefined') { + updateItem.style = { ...(updateItem.style || {}), ...currentChange.dimensions }; + } + + if (updateItem.expandParent) { + handleParentExpand(res, updateItem); + } + break; } - - if (updateItem.expandParent) { - handleParentExpand(res, updateItem); + case 'remove': { + return res; } - - res.push(updateItem); - return res; - } - case 'dimensions': { - const updateItem = { ...item }; - - if (typeof currentChange.dimensions !== 'undefined') { - updateItem.width = currentChange.dimensions.width; - updateItem.height = currentChange.dimensions.height; - } - - if (updateItem.expandParent) { - handleParentExpand(res, updateItem); - } - - res.push(updateItem); - return res; - } - case 'remove': { - return res; } } } - res.push(item); + res.push(updateItem); return res; }, initElements); } From afce7a2fee1a74fb65f9cc6315ffd4e2297a3a7d Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 1 Dec 2022 20:28:47 +0100 Subject: [PATCH 06/24] feat(resizer): batch changes --- .../src/examples/NodeResizer/index.tsx | 20 +----- packages/node-resizer/package.json | 2 +- packages/node-resizer/src/NodeResizer.tsx | 66 ++++++++++++++----- pnpm-lock.yaml | 2 +- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index 3b176e39..af0a660c 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -1,13 +1,5 @@ import { useCallback } from 'react'; -import ReactFlow, { - Controls, - addEdge, - Position, - Connection, - useNodesState, - useEdgesState, - NodeChange, -} from 'reactflow'; +import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow'; import CustomNode from './CustomNode'; @@ -41,7 +33,7 @@ const initialNodes = [ ]; const CustomNodeFlow = () => { - const [nodes, setNodes, _onNodesChange] = useNodesState(initialNodes); + const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const onConnect = useCallback( @@ -49,13 +41,6 @@ const CustomNodeFlow = () => { [setEdges] ); - const onNodesChange = useCallback( - (changes: NodeChange[]) => { - _onNodesChange(changes); - }, - [_onNodesChange] - ); - return ( { fitView minZoom={0.3} maxZoom={2} + snapToGrid > diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json index 43ee7540..4ef4f1ba 100644 --- a/packages/node-resizer/package.json +++ b/packages/node-resizer/package.json @@ -38,7 +38,7 @@ "dependencies": { "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", - "classcat": "^5.0.3", + "classcat": "^5.0.4", "d3-drag": "^3.0.0", "d3-selection": "^3.0.0", "zustand": "^4.1.1" diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 9e6764bf..0660adf6 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -1,7 +1,14 @@ import { useRef, useEffect } from 'react'; +import cc from 'classcat'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import { useStoreApi, useGetPointerPosition } from '@reactflow/core'; +import { + useStoreApi, + useGetPointerPosition, + NodeChange, + NodePositionChange, + NodeDimensionChange, +} from '@reactflow/core'; import type { Dimensions, Node, XYPosition } from '@reactflow/core'; import type { D3DragEvent, SubjectPosition } from 'd3'; @@ -17,6 +24,7 @@ type ResizeHandleProps = { enableY?: boolean; top?: number | string; left?: number | string; + className?: string; }; type ResizeDragEvent = D3DragEvent; @@ -29,6 +37,7 @@ function ResizeHandle({ enableX = false, enableY = false, nodeId, + className, }: ResizeHandleProps) { const store = useStoreApi(); const resizeHandleRef = useRef(null); @@ -65,12 +74,13 @@ function ResizeHandle({ nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; }) .on('drag', (event: ResizeDragEvent) => { - const { updateNodePositions, nodeInternals } = store.getState(); + const { updateNodePositions, nodeInternals, onNodesChange } = store.getState(); const pointerPos = getPointerPosition(event); const nodeEl = nodeElementRef.current; const node = nodeInternals.get(nodeId); if (nodeEl && node) { + const changes: NodeChange[] = []; const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0; const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0; const width = initialDimensionsRef.current.width + (invertX ? -distX : distX); @@ -81,7 +91,7 @@ function ResizeHandle({ const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY; if (x !== node.position.x || y !== node.position.y) { - updateNodePositions( + const positionChanges: NodePositionChange[] | null = updateNodePositions( [ { id: nodeId, @@ -89,16 +99,31 @@ function ResizeHandle({ } as Node, ], true, + false, false ); + + if (positionChanges?.length) { + changes.push(positionChanges[0]); + } } } - if (width !== node.width) { - nodeEl.style.width = `${width}px`; + if (width !== node.width || height !== node.height) { + const dimensionChange: NodeDimensionChange = { + id: nodeId, + type: 'dimensions', + updateStyle: true, + dimensions: { + width: width !== node.width ? width : node.width, + height: height !== node.height ? height : node.height, + }, + }; + changes.push(dimensionChange); } - if (height !== node.height) { - nodeEl.style.height = `${height}px`; + + if (changes.length) { + onNodesChange?.(changes); } } }); @@ -112,7 +137,7 @@ function ResizeHandle({ return (
- - - - - - - - + + + + + + + + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a8406c2d..4eafb92f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -230,7 +230,7 @@ importers: '@types/node': ^18.7.16 '@types/react': ^18.0.19 '@types/react-dom': ^18.0.6 - classcat: ^5.0.3 + classcat: ^5.0.4 d3-drag: ^3.0.0 d3-selection: ^3.0.0 react: ^18.2.0 From 8e43021050c68460397dc7fc6762c4af0e3c5265 Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 4 Dec 2022 15:04:07 +0100 Subject: [PATCH 07/24] refactor(resizer): export resize control, add line control, styling --- .../examples/NodeResizer/CustomResizer.tsx | 27 +++ .../examples/NodeResizer/CustomResizer2.tsx | 19 ++ .../{CustomNode.tsx => NodeResizerNode.tsx} | 8 +- .../src/examples/NodeResizer/ResizeIcon.tsx | 24 +++ .../src/examples/NodeResizer/index.tsx | 28 ++- packages/node-resizer/package.json | 4 +- packages/node-resizer/src/NodeResizer.tsx | 190 ++---------------- packages/node-resizer/src/ResizeControl.tsx | 141 +++++++++++++ packages/node-resizer/src/index.tsx | 2 + packages/node-resizer/src/style.css | 100 +++++++++ packages/node-resizer/src/types.ts | 32 ++- 11 files changed, 387 insertions(+), 188 deletions(-) create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx rename examples/vite-app/src/examples/NodeResizer/{CustomNode.tsx => NodeResizerNode.tsx} (72%) create mode 100644 examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx create mode 100644 packages/node-resizer/src/ResizeControl.tsx create mode 100644 packages/node-resizer/src/style.css diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx new file mode 100644 index 00000000..7ad2e3a3 --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx @@ -0,0 +1,27 @@ +import { memo, FC, CSSProperties } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; + +import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer'; +import '@reactflow/node-resizer/dist/style.css'; +import ResizeIcon from './ResizeIcon'; + +const controlStyle = { + background: 'transparent', + border: 'none', +}; + +const CustomNode: FC = ({ id, data }) => { + return ( + <> + + + + + +
{data.label}
+ + + ); +}; + +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx new file mode 100644 index 00000000..c25a01b6 --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx @@ -0,0 +1,19 @@ +import { memo, FC } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; + +import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer'; +import '@reactflow/node-resizer/dist/style.css'; + +const CustomNode: FC = ({ id, data }) => { + return ( + <> + + + +
{data.label}
+ + + ); +}; + +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx similarity index 72% rename from examples/vite-app/src/examples/NodeResizer/CustomNode.tsx rename to examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index 7a5dda5e..3fab7d5d 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -2,17 +2,17 @@ import { memo, FC } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; import { NodeResizer } from '@reactflow/node-resizer'; +import '@reactflow/node-resizer/dist/style.css'; -const ColorSelectorNode: FC = ({ id, data }) => { +const CustomNode: FC = ({ id, data }) => { return ( <> +
{data.label}
- - ); }; -export default memo(ColorSelectorNode); +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx new file mode 100644 index 00000000..f37b9a5d --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx @@ -0,0 +1,24 @@ +function ResizeIcon() { + return ( + + + + + + + + ); +} + +export default ResizeIcon; diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index af0a660c..3f3b982a 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -1,10 +1,14 @@ import { useCallback } from 'react'; import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow'; -import CustomNode from './CustomNode'; +import NodeResizerNode from './NodeResizerNode'; +import CustomResizer from './CustomResizer'; +import CustomResizer2 from './CustomResizer2'; const nodeTypes = { - custom: CustomNode, + resizer: NodeResizerNode, + customResizer: CustomResizer, + customResizer2: CustomResizer2, }; const initialEdges = [ @@ -25,10 +29,24 @@ const initialNodes = [ }, { id: '2', - type: 'custom', - data: { label: 'resize me!' }, + type: 'resizer', + data: { label: 'default resizer' }, position: { x: 250, y: 0 }, - style: { padding: 10, border: '1px solid #222' }, + style: { padding: 10, border: '1px solid #222', fontSize: 10 }, + }, + { + id: '3', + type: 'customResizer', + data: { label: 'resize control with child component' }, + position: { x: 250, y: 150 }, + style: { padding: 10, border: '1px solid #222', fontSize: 10, width: 100 }, + }, + { + id: '4', + type: 'customResizer2', + data: { label: 'resize controls' }, + position: { x: 100, y: 150 }, + style: { padding: 10, border: '1px solid #222', fontSize: 10 }, }, ]; diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json index 4ef4f1ba..3025ccb7 100644 --- a/packages/node-resizer/package.json +++ b/packages/node-resizer/package.json @@ -31,7 +31,9 @@ }, "scripts": { "dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch", - "build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production", + "build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production && npm run css", + "css": "postcss src/*.css --config ../../tooling/postcss-config/postcss.config.js --dir dist", + "css-watch": "pnpm css --watch", "lint": "eslint --ext .js,.jsx,.ts,.tsx src", "typecheck": "tsc --noEmit" }, diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 0660adf6..9074ea2f 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -1,179 +1,25 @@ -import { useRef, useEffect } from 'react'; -import cc from 'classcat'; -import { drag } from 'd3-drag'; -import { select } from 'd3-selection'; -import { - useStoreApi, - useGetPointerPosition, - NodeChange, - NodePositionChange, - NodeDimensionChange, -} from '@reactflow/core'; -import type { Dimensions, Node, XYPosition } from '@reactflow/core'; -import type { D3DragEvent, SubjectPosition } from 'd3'; +import ResizeControl from './ResizeControl'; +import type { NodeResizerProps } from './types'; -type NodeResizerProps = { - nodeId: string; -}; - -type ResizeHandleProps = { - nodeId: string; - invertX?: boolean; - invertY?: boolean; - enableX?: boolean; - enableY?: boolean; - top?: number | string; - left?: number | string; - className?: string; -}; - -type ResizeDragEvent = D3DragEvent; - -function ResizeHandle({ - top = 0, - left = 0, - invertX = false, - invertY = false, - enableX = false, - enableY = false, +export default function NodeResizer({ nodeId, - className, -}: ResizeHandleProps) { - const store = useStoreApi(); - const resizeHandleRef = useRef(null); - const initialDimensionsRef = useRef({ - width: 0, - height: 0, - x: 0, - y: 0, - nodeX: 0, - nodeY: 0, - }); - const nodeElementRef = useRef(null); - const getPointerPosition = useGetPointerPosition(); - - useEffect(() => { - if (!resizeHandleRef.current) { - return; - } - - const selection = select(resizeHandleRef.current); - const dragHandler = drag() - .on('start', (event: ResizeDragEvent) => { - const node = store.getState().nodeInternals.get(nodeId); - const pointerPos = getPointerPosition(event); - - initialDimensionsRef.current = { - width: node?.width ?? 0, - height: node?.height ?? 0, - nodeX: node?.position.x ?? 0, - nodeY: node?.position.y ?? 0, - x: pointerPos.xSnapped, - y: pointerPos.ySnapped, - }; - nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; - }) - .on('drag', (event: ResizeDragEvent) => { - const { updateNodePositions, nodeInternals, onNodesChange } = store.getState(); - const pointerPos = getPointerPosition(event); - const nodeEl = nodeElementRef.current; - const node = nodeInternals.get(nodeId); - - if (nodeEl && node) { - const changes: NodeChange[] = []; - const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0; - const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0; - const width = initialDimensionsRef.current.width + (invertX ? -distX : distX); - const height = initialDimensionsRef.current.height + (invertY ? -distY : distY); - - if (invertX || invertY) { - const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX; - const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY; - - if (x !== node.position.x || y !== node.position.y) { - const positionChanges: NodePositionChange[] | null = updateNodePositions( - [ - { - id: nodeId, - position: { x, y }, - } as Node, - ], - true, - false, - false - ); - - if (positionChanges?.length) { - changes.push(positionChanges[0]); - } - } - } - - if (width !== node.width || height !== node.height) { - const dimensionChange: NodeDimensionChange = { - id: nodeId, - type: 'dimensions', - updateStyle: true, - dimensions: { - width: width !== node.width ? width : node.width, - height: height !== node.height ? height : node.height, - }, - }; - changes.push(dimensionChange); - } - - if (changes.length) { - onNodesChange?.(changes); - } - } - }); - - selection.call(dragHandler); - - return () => { - selection.on('.drag', null); - }; - }, [nodeId]); - - return ( -
- ); -} - -export default function NodeResizer({ nodeId }: NodeResizerProps) { + handleClassName, + handleStyle, + lineClassName, + lineStyle, +}: NodeResizerProps) { return ( <> - - - - - - - - + + + + + + + + + + ); } diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx new file mode 100644 index 00000000..b57f021a --- /dev/null +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -0,0 +1,141 @@ +import { useRef, useEffect } from 'react'; +import cc from 'classcat'; +import { drag } from 'd3-drag'; +import { select } from 'd3-selection'; +import { + useStoreApi, + useGetPointerPosition, + NodeChange, + NodePositionChange, + NodeDimensionChange, +} from '@reactflow/core'; +import type { Dimensions, Node, XYPosition } from '@reactflow/core'; + +import type { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps } from './types'; + +function ResizeControl({ + nodeId, + position = 'bottom-right', + variant = 'handle', + className, + style = {}, + children, +}: ResizeControlProps) { + const store = useStoreApi(); + const resizeHandleRef = useRef(null); + const initialDimensionsRef = useRef({ + width: 0, + height: 0, + x: 0, + y: 0, + nodeX: 0, + nodeY: 0, + }); + const nodeElementRef = useRef(null); + const getPointerPosition = useGetPointerPosition(); + + useEffect(() => { + if (!resizeHandleRef.current) { + return; + } + + const selection = select(resizeHandleRef.current); + const dragHandler = drag() + .on('start', (event: ResizeDragEvent) => { + const node = store.getState().nodeInternals.get(nodeId); + const pointerPos = getPointerPosition(event); + + initialDimensionsRef.current = { + width: node?.width ?? 0, + height: node?.height ?? 0, + nodeX: node?.position.x ?? 0, + nodeY: node?.position.y ?? 0, + x: pointerPos.xSnapped, + y: pointerPos.ySnapped, + }; + nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; + }) + .on('drag', (event: ResizeDragEvent) => { + const { updateNodePositions, nodeInternals, onNodesChange } = store.getState(); + const pointerPos = getPointerPosition(event); + const nodeEl = nodeElementRef.current; + const node = nodeInternals.get(nodeId); + const enableX = position.includes('right') || position.includes('left'); + const enableY = position.includes('bottom') || position.includes('top'); + const invertX = position.includes('left'); + const invertY = position.includes('top'); + + if (nodeEl && node) { + const changes: NodeChange[] = []; + const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0; + const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0; + const width = initialDimensionsRef.current.width + (invertX ? -distX : distX); + const height = initialDimensionsRef.current.height + (invertY ? -distY : distY); + + if (invertX || invertY) { + const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX; + const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY; + + if (x !== node.position.x || y !== node.position.y) { + const positionChanges: NodePositionChange[] | null = updateNodePositions( + [ + { + id: nodeId, + position: { x, y }, + } as Node, + ], + true, + false, + false + ); + + if (positionChanges?.length) { + changes.push(positionChanges[0]); + } + } + } + + if (width !== node.width || height !== node.height) { + const dimensionChange: NodeDimensionChange = { + id: nodeId, + type: 'dimensions', + updateStyle: true, + dimensions: { + width: width !== node.width ? width : node.width, + height: height !== node.height ? height : node.height, + }, + }; + changes.push(dimensionChange); + } + + if (changes.length) { + onNodesChange?.(changes); + } + } + }); + + selection.call(dragHandler); + + return () => { + selection.on('.drag', null); + }; + }, [nodeId, position, getPointerPosition]); + + const positionClassNames = position.split('-'); + + return ( +
+ {children} +
+ ); +} + +export function ResizeControlLine(props: ResizeControlLineProps) { + return ; +} + +export default ResizeControl; diff --git a/packages/node-resizer/src/index.tsx b/packages/node-resizer/src/index.tsx index ea5bf55b..c3cd1d5d 100644 --- a/packages/node-resizer/src/index.tsx +++ b/packages/node-resizer/src/index.tsx @@ -1,2 +1,4 @@ export { default as NodeResizer } from './NodeResizer'; +export { default as NodeResizeControl } from './ResizeControl'; + export * from './types'; diff --git a/packages/node-resizer/src/style.css b/packages/node-resizer/src/style.css new file mode 100644 index 00000000..71077758 --- /dev/null +++ b/packages/node-resizer/src/style.css @@ -0,0 +1,100 @@ +.react-flow__resize-control { + position: absolute; + background-color: rgba(195, 195, 195, 1); + border: 1px solid white; + width: 4px; + height: 4px; +} + +.react-flow__resize-control.left, +.react-flow__resize-control.right { + cursor: ew-resize; +} + +.react-flow__resize-control.top, +.react-flow__resize-control.bottom { + cursor: ns-resize; +} + +.react-flow__resize-control.top.left, +.react-flow__resize-control.bottom.right { + cursor: nwse-resize; +} + +.react-flow__resize-control.bottom.left, +.react-flow__resize-control.top.right { + cursor: nesw-resize; +} + +/* handle styles */ +.react-flow__resize-control.handle { + transform: translate(-50%, -50%); +} + +.react-flow__resize-control.handle.left { + left: 0; + top: 50%; +} +.react-flow__resize-control.handle.right { + left: 100%; + top: 50%; +} +.react-flow__resize-control.handle.top { + left: 50%; + top: 0; +} +.react-flow__resize-control.handle.bottom { + left: 50%; + top: 100%; +} +.react-flow__resize-control.handle.top.left { + left: 0; +} +.react-flow__resize-control.handle.bottom.left { + left: 0; +} +.react-flow__resize-control.handle.top.right { + left: 100%; +} +.react-flow__resize-control.handle.bottom.right { + left: 100%; +} + +/* line styles */ +.react-flow__resize-control.line { + border: none; + background: none; +} +.react-flow__resize-control.line.left, +.react-flow__resize-control.line.right { + width: 1px; + transform: translate(-50%, 0); + top: 0; + height: 100%; +} + +.react-flow__resize-control.line.left { + left: 0; + border-left: 1px solid rgba(195, 195, 195, 1); +} +.react-flow__resize-control.line.right { + left: 100%; + border-right: 1px solid rgba(195, 195, 195, 1); +} + +.react-flow__resize-control.line.top, +.react-flow__resize-control.line.bottom { + height: 1px; + transform: translate(0, -50%); + left: 0; + width: 100%; +} + +.react-flow__resize-control.line.top { + top: 0; + border-top: 1px solid rgba(195, 195, 195, 1); +} +.react-flow__resize-control.line.bottom { + border-bottom: 1px solid rgba(195, 195, 195, 1); + top: 100%; +} diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 1e6576b6..7eb6b32a 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -1,9 +1,29 @@ -import { Position } from '@reactflow/core'; -import type { HTMLAttributes } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import type { D3DragEvent, SubjectPosition } from 'd3-drag'; -export type NodeToolbarProps = HTMLAttributes & { +export type NodeResizerProps = { nodeId: string; - isVisible?: boolean; - position?: Position; - offset?: number; + handleClassName?: string; + handleStyle?: CSSProperties; + lineClassName?: string; + lineStyle?: CSSProperties; }; + +export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; + +export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + +export type ResizeControlProps = { + nodeId: string; + position: ControlPosition; + variant?: 'line' | 'handle'; + className?: string; + style?: CSSProperties; + children?: ReactNode; +}; + +export type ResizeControlLineProps = ResizeControlProps & { + position: ControlLinePosition; +}; + +export type ResizeDragEvent = D3DragEvent; From 9da984f3e4ecc2cc5c9bcad96762bd05a5f6fe39 Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 4 Dec 2022 15:18:32 +0100 Subject: [PATCH 08/24] refactor(resizer): handle default nodes --- packages/core/src/index.ts | 1 + packages/node-resizer/src/ResizeControl.tsx | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0915a456..60a8451f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,6 +22,7 @@ export { getNodePositionWithOrigin, } from './utils/graph'; export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; +export { createNodeInternals } from './store/utils'; export { getMarkerEnd } from './components/Edges/utils'; export { default as ReactFlowProvider } from './components/ReactFlowProvider'; export { default as Panel } from './components/Panel'; diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index b57f021a..832cd647 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -8,6 +8,8 @@ import { NodeChange, NodePositionChange, NodeDimensionChange, + applyNodeChanges, + createNodeInternals, } from '@reactflow/core'; import type { Dimensions, Node, XYPosition } from '@reactflow/core'; @@ -56,7 +58,7 @@ function ResizeControl({ nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; }) .on('drag', (event: ResizeDragEvent) => { - const { updateNodePositions, nodeInternals, onNodesChange } = store.getState(); + const { updateNodePositions, nodeInternals, onNodesChange, hasDefaultNodes, nodeOrigin } = store.getState(); const pointerPos = getPointerPosition(event); const nodeEl = nodeElementRef.current; const node = nodeInternals.get(nodeId); @@ -109,6 +111,12 @@ function ResizeControl({ } if (changes.length) { + if (hasDefaultNodes) { + const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); + const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); + store.setState({ nodeInternals: nextNodeInternals }); + } + onNodesChange?.(changes); } } From 8bca8e4bfc6e2dde7e85340d64f033f991b5997c Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 4 Dec 2022 17:50:11 +0100 Subject: [PATCH 09/24] refacitr(resizer): cleanup --- packages/node-resizer/package.json | 2 - packages/node-resizer/src/NodeResizer.tsx | 28 ++++++---- packages/node-resizer/src/ResizeControl.tsx | 62 +++++++++++---------- packages/node-resizer/src/types.ts | 7 ++- 4 files changed, 56 insertions(+), 43 deletions(-) diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json index 3025ccb7..0a169f31 100644 --- a/packages/node-resizer/package.json +++ b/packages/node-resizer/package.json @@ -38,7 +38,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", "classcat": "^5.0.4", "d3-drag": "^3.0.0", @@ -53,7 +52,6 @@ "@reactflow/eslint-config": "workspace:*", "@reactflow/rollup-config": "workspace:*", "@reactflow/tsconfig": "workspace:*", - "@types/d3": "^7.4.0", "@types/d3-drag": "^3.0.1", "@types/d3-selection": "^3.0.3", "@types/node": "^18.7.16", diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 9074ea2f..75267d86 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -1,5 +1,8 @@ import ResizeControl from './ResizeControl'; -import type { NodeResizerProps } from './types'; +import { ControlPosition, NodeResizerProps, ResizeControlVariant, ControlLinePosition } from './types'; + +const handleControls: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right']; +const lineControls: ControlLinePosition[] = ['top', 'right', 'bottom', 'left']; export default function NodeResizer({ nodeId, @@ -10,16 +13,19 @@ export default function NodeResizer({ }: NodeResizerProps) { return ( <> - - - - - - - - - - + {lineControls.map((c) => ( + + ))} + {handleControls.map((c) => ( + + ))} ); } diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index 832cd647..ec2e46dd 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -1,4 +1,4 @@ -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, memo } from 'react'; import cc from 'classcat'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; @@ -6,26 +6,25 @@ import { useStoreApi, useGetPointerPosition, NodeChange, - NodePositionChange, NodeDimensionChange, applyNodeChanges, createNodeInternals, } from '@reactflow/core'; import type { Dimensions, Node, XYPosition } from '@reactflow/core'; -import type { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps } from './types'; +import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; function ResizeControl({ nodeId, position = 'bottom-right', - variant = 'handle', + variant = ResizeControlVariant.Handle, className, style = {}, children, }: ResizeControlProps) { const store = useStoreApi(); - const resizeHandleRef = useRef(null); - const initialDimensionsRef = useRef({ + const resizeControlRef = useRef(null); + const startValues = useRef({ width: 0, height: 0, x: 0, @@ -33,53 +32,58 @@ function ResizeControl({ nodeX: 0, nodeY: 0, }); - const nodeElementRef = useRef(null); const getPointerPosition = useGetPointerPosition(); useEffect(() => { - if (!resizeHandleRef.current) { + if (!resizeControlRef.current) { return; } - const selection = select(resizeHandleRef.current); + const selection = select(resizeControlRef.current); const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { const node = store.getState().nodeInternals.get(nodeId); - const pointerPos = getPointerPosition(event); + const { xSnapped, ySnapped } = getPointerPosition(event); - initialDimensionsRef.current = { + startValues.current = { width: node?.width ?? 0, height: node?.height ?? 0, nodeX: node?.position.x ?? 0, nodeY: node?.position.y ?? 0, - x: pointerPos.xSnapped, - y: pointerPos.ySnapped, + x: xSnapped, + y: ySnapped, }; - nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement; }) .on('drag', (event: ResizeDragEvent) => { const { updateNodePositions, nodeInternals, onNodesChange, hasDefaultNodes, nodeOrigin } = store.getState(); - const pointerPos = getPointerPosition(event); - const nodeEl = nodeElementRef.current; + const { xSnapped, ySnapped } = getPointerPosition(event); const node = nodeInternals.get(nodeId); const enableX = position.includes('right') || position.includes('left'); const enableY = position.includes('bottom') || position.includes('top'); const invertX = position.includes('left'); const invertY = position.includes('top'); - if (nodeEl && node) { + if (node) { const changes: NodeChange[] = []; - const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0; - const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0; - const width = initialDimensionsRef.current.width + (invertX ? -distX : distX); - const height = initialDimensionsRef.current.height + (invertY ? -distY : distY); + const { + x: startX, + y: startY, + width: startWidth, + height: startHeight, + nodeX: startNodeX, + nodeY: startNodeY, + } = startValues.current; + const distX = enableX ? xSnapped - startX : 0; + const distY = enableY ? ySnapped - startY : 0; + const width = startWidth + (invertX ? -distX : distX); + const height = startHeight + (invertY ? -distY : distY); if (invertX || invertY) { - const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX; - const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY; + const x = invertX ? startNodeX + distX : startNodeX; + const y = invertY ? startNodeY + distY : startNodeY; if (x !== node.position.x || y !== node.position.y) { - const positionChanges: NodePositionChange[] | null = updateNodePositions( + const positionChanges = updateNodePositions( [ { id: nodeId, @@ -129,12 +133,12 @@ function ResizeControl({ }; }, [nodeId, position, getPointerPosition]); - const positionClassNames = position.split('-'); + const positionClassNames = position?.split('-'); return (
{children} @@ -143,7 +147,7 @@ function ResizeControl({ } export function ResizeControlLine(props: ResizeControlLineProps) { - return ; + return ; } -export default ResizeControl; +export default memo(ResizeControl); diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 7eb6b32a..9368e434 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -13,10 +13,15 @@ export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; +export enum ResizeControlVariant { + Line = 'line', + Handle = 'handle', +} + export type ResizeControlProps = { nodeId: string; position: ControlPosition; - variant?: 'line' | 'handle'; + variant?: ResizeControlVariant; className?: string; style?: CSSProperties; children?: ReactNode; From 7d57c465718e15fdfadb24308f135807746c54ad Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 13:41:39 +0100 Subject: [PATCH 10/24] refactor(nodes): don't move or select nodes when input is focused --- packages/core/src/components/Nodes/wrapNode.tsx | 7 ++++++- packages/core/src/hooks/useKeyPress.ts | 12 +----------- packages/core/src/utils/index.ts | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/core/src/components/Nodes/wrapNode.tsx b/packages/core/src/components/Nodes/wrapNode.tsx index afb1f761..70b36ddb 100644 --- a/packages/core/src/components/Nodes/wrapNode.tsx +++ b/packages/core/src/components/Nodes/wrapNode.tsx @@ -8,7 +8,7 @@ import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; import useDrag from '../../hooks/useDrag'; import useUpdateNodePositions from '../../hooks/useUpdateNodePositions'; import { getMouseHandler, handleNodeClick } from './utils'; -import { elementSelectionKeys } from '../../utils'; +import { elementSelectionKeys, isInputDOMNode } from '../../utils'; import type { NodeProps, WrapNodeProps, XYPosition } from '../../types'; export const arrowKeyDiffs: Record = { @@ -84,7 +84,12 @@ export default (NodeComponent: ComponentType) => { }; const onKeyDown = (event: KeyboardEvent) => { + if (isInputDOMNode(event)) { + return; + } + const { snapGrid, snapToGrid } = store.getState(); + if (elementSelectionKeys.includes(event.key) && isSelectable) { const unselect = event.key === 'Escape'; if (unselect) { diff --git a/packages/core/src/hooks/useKeyPress.ts b/packages/core/src/hooks/useKeyPress.ts index 4273feaa..20dcf37e 100644 --- a/packages/core/src/hooks/useKeyPress.ts +++ b/packages/core/src/hooks/useKeyPress.ts @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useMemo } from 'react'; +import { isInputDOMNode } from '../utils'; import type { KeyCode } from '../types'; type Keys = Array; @@ -106,14 +107,3 @@ function isMatchingKey(keyCodes: Array, pressedKeys: PressedKeys, isUp: bo function useKeyOrCode(eventCode: string, keysToWatch: KeyCode): KeyOrCode { return keysToWatch.includes(eventCode) ? 'code' : 'key'; } - -function isInputDOMNode(event: KeyboardEvent): boolean { - // using composed path for handling shadow dom - const target = (event.composedPath?.()[0] || event.target) as HTMLElement; - - return ( - ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || - target?.hasAttribute('contenteditable') || - !!target?.closest('.nokey') - ); -} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index d869a81a..f6e9d994 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,3 +1,4 @@ +import type { KeyboardEvent as ReactKeyboardEvent } from 'react'; import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types'; export const getDimensions = (node: HTMLDivElement): Dimensions => ({ @@ -69,3 +70,18 @@ export const devWarn = (message: string) => { console.warn(`[React Flow]: ${message}`); } }; + +const isReactKeyboardEvent = (event: KeyboardEvent | ReactKeyboardEvent): event is ReactKeyboardEvent => + 'nativeEvent' in event; + +export function isInputDOMNode(event: KeyboardEvent | ReactKeyboardEvent): boolean { + const kbEvent = isReactKeyboardEvent(event) ? event.nativeEvent : event; + // using composed path for handling shadow dom + const target = (kbEvent.composedPath?.()?.[0] || event.target) as HTMLElement; + + return ( + ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || + target?.hasAttribute('contenteditable') || + !!target?.closest('.nokey') + ); +} From 0df02f35f8d6c54dae36af18278feadc77acb2d6 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 14:08:10 +0100 Subject: [PATCH 11/24] chore(changeset): add --- .changeset/tough-masks-teach.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tough-masks-teach.md diff --git a/.changeset/tough-masks-teach.md b/.changeset/tough-masks-teach.md new file mode 100644 index 00000000..433697f2 --- /dev/null +++ b/.changeset/tough-masks-teach.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +Ignore key events for nodes when input is focused From 851b8e3e0fb280e4eb43fb0c143094352702c7a3 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 16:50:20 +0100 Subject: [PATCH 12/24] fix(getRectOfNodes): handle node pos correctly closes #2641 --- packages/core/src/hooks/useDrag/utils.ts | 18 +++++-------- packages/core/src/utils/graph.ts | 33 ++++++++++++------------ packages/minimap/src/MiniMap.tsx | 6 ++--- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts index 61b0d3f9..74484baf 100644 --- a/packages/core/src/hooks/useDrag/utils.ts +++ b/packages/core/src/hooks/useDrag/utils.ts @@ -1,6 +1,6 @@ import type { RefObject } from 'react'; -import { clampPosition, devWarn } from '../../utils'; +import { clampPosition, devWarn, isNumeric } from '../../utils'; import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; import { getNodePositionWithOrigin } from '../../utils/graph'; @@ -69,17 +69,14 @@ export function calcNextPosition( if (node.extent === 'parent') { if (node.parentNode && node.width && node.height) { const parent = nodeInternals.get(node.parentNode); - const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin); + const { x: parentX, y: parentY } = getNodePositionWithOrigin(parent, nodeOrigin).positionAbsolute; currentExtent = - parentPosition.positionAbsolute && parent?.width && parent?.height + parent && isNumeric(parentX) && isNumeric(parentY) && isNumeric(parent.width) && isNumeric(parent.height) ? [ + [parentX + node.width * nodeOrigin[0], parentY + node.height * nodeOrigin[1]], [ - parentPosition.positionAbsolute.x + node.width * nodeOrigin[0], - parentPosition.positionAbsolute.y + node.height * nodeOrigin[1], - ], - [ - parentPosition.positionAbsolute.x + parent.width - node.width + node.width * nodeOrigin[0], - parentPosition.positionAbsolute.y + parent.height - node.height + node.height * nodeOrigin[1], + parentX + parent.width - node.width + node.width * nodeOrigin[0], + parentY + parent.height - node.height + node.height * nodeOrigin[1], ], ] : currentExtent; @@ -90,8 +87,7 @@ export function calcNextPosition( } } else if (node.extent && node.parentNode) { const parent = nodeInternals.get(node.parentNode); - const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin); - const { x: parentX, y: parentY } = parentPosition.positionAbsolute; + const { x: parentX, y: parentY } = getNodePositionWithOrigin(parent, nodeOrigin).positionAbsolute; currentExtent = [ [node.extent[0][0] + parentX, node.extent[0][1] + parentY], [node.extent[1][0] + parentX, node.extent[1][1] + parentY], diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index ea390967..ce728407 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -2,7 +2,7 @@ import type { Selection as D3Selection } from 'd3'; import { boxToRect, clamp, devWarn, getBoundsOfBoxes, getOverlappingArea, rectToBox } from '../utils'; -import type { +import { Node, Edge, Connection, @@ -156,18 +156,22 @@ export const getNodePositionWithOrigin = ( }; } - const offset: XYPosition = { - x: (node.width ?? 0) * nodeOrigin[0], - y: (node.height ?? 0) * nodeOrigin[1], + const offsetX = (node.width ?? 0) * nodeOrigin[0]; + const offsetY = (node.height ?? 0) * nodeOrigin[1]; + + const position: XYPosition = { + x: node.position.x - offsetX, + y: node.position.y - offsetY, }; return { - x: node.position.x - offset.x, - y: node.position.y - offset.y, - positionAbsolute: { - x: (node.positionAbsolute?.x ?? 0) - offset.x, - y: (node.positionAbsolute?.y ?? 0) - offset.y, - }, + ...position, + positionAbsolute: node.positionAbsolute + ? { + x: (node.positionAbsolute.x ?? 0) - offsetX, + y: (node.positionAbsolute.y ?? 0) - offsetY, + } + : position, }; }; @@ -178,15 +182,12 @@ export const getRectOfNodes = (nodes: Node[], nodeOrigin: NodeOrigin = [0, 0]): const box = nodes.reduce( (currBox, node) => { - const { positionAbsolute, ...position } = getNodePositionWithOrigin(node, nodeOrigin); - const nodeX = positionAbsolute ? positionAbsolute.x : position.x; - const nodeY = positionAbsolute ? positionAbsolute.y : position.y; - + const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; return getBoundsOfBoxes( currBox, rectToBox({ - x: nodeX, - y: nodeY, + x, + y, width: node.width || 0, height: node.height || 0, }) diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 722f1423..b1d4c555 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -167,13 +167,13 @@ function MiniMap({ > {ariaLabel && {ariaLabel}} {nodes.map((node) => { - const { positionAbsolute } = getNodePositionWithOrigin(node, nodeOrigin); + const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; return ( Date: Tue, 6 Dec 2022 16:52:38 +0100 Subject: [PATCH 13/24] chore(getNodePosOrigin): cleanup --- packages/core/src/utils/graph.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index ce728407..9a3d4c42 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -168,8 +168,8 @@ export const getNodePositionWithOrigin = ( ...position, positionAbsolute: node.positionAbsolute ? { - x: (node.positionAbsolute.x ?? 0) - offsetX, - y: (node.positionAbsolute.y ?? 0) - offsetY, + x: node.positionAbsolute.x - offsetX, + y: node.positionAbsolute.y - offsetY, } : position, }; From e6b5d90f61c8ee60e817bba232a162cae2ab3e2a Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 16:53:30 +0100 Subject: [PATCH 14/24] chore(changeset): add --- .changeset/lazy-pumpkins-remember.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/lazy-pumpkins-remember.md diff --git a/.changeset/lazy-pumpkins-remember.md b/.changeset/lazy-pumpkins-remember.md new file mode 100644 index 00000000..8a5a3473 --- /dev/null +++ b/.changeset/lazy-pumpkins-remember.md @@ -0,0 +1,6 @@ +--- +'@reactflow/core': patch +'@reactflow/minimap': patch +--- + +Fix getRectOfNodes From aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 17:11:30 +0100 Subject: [PATCH 15/24] chore(changeset): add --- .changeset/perfect-pants-serve.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/perfect-pants-serve.md diff --git a/.changeset/perfect-pants-serve.md b/.changeset/perfect-pants-serve.md new file mode 100644 index 00000000..851f79df --- /dev/null +++ b/.changeset/perfect-pants-serve.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +Invalidate a Node trying to connect itself with the same handle From 0a2c13d468d6693cec981bccca3fed528519451e Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 17:20:12 +0100 Subject: [PATCH 16/24] refactor(panning): allow middle mouse pan over edge closes #2621 --- packages/core/src/container/ZoomPane/index.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index a5783a7a..f72840b1 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -228,7 +228,11 @@ const ZoomPane = ({ const zoomScroll = zoomActivationKeyPressed || zoomOnScroll; const pinchZoom = zoomOnPinch && event.ctrlKey; - if (event.button === 1 && event.type === 'mousedown' && event.target.closest(`.react-flow__node`)) { + if ( + event.button === 1 && + event.type === 'mousedown' && + (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) + ) { return true; } From 6ee44e076eaa6908d07578a757a5187642b732ae Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 17:21:21 +0100 Subject: [PATCH 17/24] chore(changeset): add --- .changeset/mighty-needles-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-needles-rush.md diff --git a/.changeset/mighty-needles-rush.md b/.changeset/mighty-needles-rush.md new file mode 100644 index 00000000..0790ee82 --- /dev/null +++ b/.changeset/mighty-needles-rush.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +Allow middle mouse pan over edges From 0892e2ea9aaa19a563858b849f6065f966dbf546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 6 Dec 2022 17:56:56 +0100 Subject: [PATCH 18/24] refactor(node-resizer): use nodeId from context, fix glitches while resizing --- .../examples/NodeResizer/CustomResizer.tsx | 2 +- .../examples/NodeResizer/CustomResizer2.tsx | 6 +- .../examples/NodeResizer/NodeResizerNode.tsx | 4 +- .../src/examples/NodeResizer/ResizeIcon.tsx | 6 +- .../src/examples/NodeResizer/index.tsx | 35 +++-- packages/core/src/components/Handle/index.tsx | 8 +- packages/core/src/contexts/NodeIdContext.ts | 7 +- packages/core/src/index.ts | 2 +- packages/core/src/store/index.ts | 65 ++++----- packages/core/src/types/changes.ts | 3 +- packages/core/src/types/general.ts | 8 +- packages/core/src/types/nodes.ts | 1 + packages/core/src/utils/changes.ts | 4 + packages/node-resizer/src/NodeResizer.tsx | 16 ++- packages/node-resizer/src/ResizeControl.tsx | 128 +++++++++++------- packages/node-resizer/src/style.css | 23 ++-- packages/node-resizer/src/types.ts | 11 +- 17 files changed, 201 insertions(+), 128 deletions(-) diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx index 7ad2e3a3..996e24c5 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx @@ -13,7 +13,7 @@ const controlStyle = { const CustomNode: FC = ({ id, data }) => { return ( <> - + diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx index c25a01b6..27833556 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx @@ -7,10 +7,10 @@ import '@reactflow/node-resizer/dist/style.css'; const CustomNode: FC = ({ id, data }) => { return ( <> - - + + -
{data.label}
+
{data.label}
); diff --git a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index 3fab7d5d..3a68cd9f 100644 --- a/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -4,10 +4,10 @@ import { Handle, Position, NodeProps } from 'reactflow'; import { NodeResizer } from '@reactflow/node-resizer'; import '@reactflow/node-resizer/dist/style.css'; -const CustomNode: FC = ({ id, data }) => { +const CustomNode: FC = ({ id, data, selected }) => { return ( <> - +
{data.label}
diff --git a/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx index f37b9a5d..8db1cb92 100644 --- a/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx +++ b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx @@ -5,11 +5,11 @@ function ResizeIcon() { width="8" height="8" viewBox="0 0 24 24" - stroke-width="2" + strokeWidth="2" stroke="currentColor" fill="none" - stroke-linecap="round" - stroke-linejoin="round" + strokeLinecap="round" + strokeLinejoin="round" style={{ position: 'absolute', right: 2, bottom: 2 }} > diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index 3f3b982a..43f978ac 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -1,5 +1,5 @@ -import { useCallback } from 'react'; -import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow'; +import { CSSProperties, useCallback, useState } from 'react'; +import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState, Panel } from 'reactflow'; import NodeResizerNode from './NodeResizerNode'; import CustomResizer from './CustomResizer'; @@ -32,25 +32,40 @@ const initialNodes = [ type: 'resizer', data: { label: 'default resizer' }, position: { x: 250, y: 0 }, - style: { padding: 10, border: '1px solid #222', fontSize: 10 }, + style: { + width: 200, + height: 150, + border: '1px solid #222', + fontSize: 10, + }, }, { id: '3', type: 'customResizer', data: { label: 'resize control with child component' }, position: { x: 250, y: 150 }, - style: { padding: 10, border: '1px solid #222', fontSize: 10, width: 100 }, + style: { border: '1px solid #222', fontSize: 10, width: 100 }, + parentNode: '2', }, { id: '4', type: 'customResizer2', data: { label: 'resize controls' }, position: { x: 100, y: 150 }, - style: { padding: 10, border: '1px solid #222', fontSize: 10 }, + style: { border: '1px solid #222', fontSize: 10 }, + parentNode: '2', + }, + { + id: '5', + type: 'customResizer2', + data: { label: 'min width and height' }, + position: { x: 100, y: 150 }, + style: { border: '1px solid #222', fontSize: 10 }, }, ]; const CustomNodeFlow = () => { + const [snapToGrid, setSnapToGrid] = useState(false); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -67,12 +82,14 @@ const CustomNodeFlow = () => { onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes} - fitView - minZoom={0.3} - maxZoom={2} - snapToGrid + minZoom={-5} + maxZoom={5} + snapToGrid={snapToGrid} > + + + ); }; diff --git a/packages/core/src/components/Handle/index.tsx b/packages/core/src/components/Handle/index.tsx index c47035a9..a2d0502f 100644 --- a/packages/core/src/components/Handle/index.tsx +++ b/packages/core/src/components/Handle/index.tsx @@ -1,9 +1,9 @@ -import { memo, useContext, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react'; +import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; import { useStore, useStoreApi } from '../../hooks/useStore'; -import NodeIdContext from '../../contexts/NodeIdContext'; +import { useNodeId } from '../../contexts/NodeIdContext'; import { checkElementBelowIsValid, handleMouseDown } from './handler'; import { getHostForElement } from '../../utils'; import { addEdge } from '../../utils/graph'; @@ -37,7 +37,9 @@ const Handle = forwardRef( ref ) => { const store = useStoreApi(); - const nodeId = useContext(NodeIdContext) as string; + + // @fixme: remove type assertion and handle nodeId === null + const nodeId = useNodeId() as string; const { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow); const handleId = id || null; diff --git a/packages/core/src/contexts/NodeIdContext.ts b/packages/core/src/contexts/NodeIdContext.ts index c43a1c3b..28a14b78 100644 --- a/packages/core/src/contexts/NodeIdContext.ts +++ b/packages/core/src/contexts/NodeIdContext.ts @@ -1,7 +1,12 @@ -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; export const NodeIdContext = createContext(null); export const Provider = NodeIdContext.Provider; export const Consumer = NodeIdContext.Consumer; +export const useNodeId = (): string | null => { + const nodeId = useContext(NodeIdContext); + return nodeId; +}; + export default NodeIdContext; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 60a8451f..c4a5b19e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,7 +22,6 @@ export { getNodePositionWithOrigin, } from './utils/graph'; export { applyNodeChanges, applyEdgeChanges } from './utils/changes'; -export { createNodeInternals } from './store/utils'; export { getMarkerEnd } from './components/Edges/utils'; export { default as ReactFlowProvider } from './components/ReactFlowProvider'; export { default as Panel } from './components/Panel'; @@ -40,5 +39,6 @@ export { default as useOnViewportChange } from './hooks/useOnViewportChange'; export { default as useOnSelectionChange } from './hooks/useOnSelectionChange'; export { default as useNodesInitialized } from './hooks/useNodesInitialized'; export { default as useGetPointerPosition } from './hooks/useGetPointerPosition'; +export { useNodeId } from './contexts/NodeIdContext'; export * from './types'; diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index e05f2048..bdb02137 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -17,6 +17,7 @@ import type { NodePositionChange, NodeDragItem, UnselectNodesAndEdgesParams, + NodeChange, } from '../types'; const createRFStore = () => @@ -102,47 +103,41 @@ const createRFStore = () => onNodesChange?.(changes); } }, - updateNodePositions: ( - nodeDragItems: NodeDragItem[] | Node[], - positionChanged = true, - dragging = false, - applyChanges = true - ) => { - const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get(); + updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => { + const { triggerNodeChanges } = get(); - if (hasDefaultNodes || onNodesChange) { - const changes = nodeDragItems.map((node) => { - const change: NodePositionChange = { - id: node.id, - type: 'position', - dragging, - }; + const changes = nodeDragItems.map((node) => { + const change: NodePositionChange = { + id: node.id, + type: 'position', + dragging, + }; - if (positionChanged) { - change.positionAbsolute = node.positionAbsolute; - change.position = node.position; - } - - return change; - }); - - if (changes?.length) { - if (hasDefaultNodes) { - const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); - const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); - set({ nodeInternals: nextNodeInternals }); - } - - if (applyChanges) { - onNodesChange?.(changes); - } + if (positionChanged) { + change.positionAbsolute = node.positionAbsolute; + change.position = node.position; } - return changes; - } + return change; + }); - return null; + triggerNodeChanges(changes); }, + + triggerNodeChanges: (changes: NodeChange[]) => { + const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get(); + + if (changes?.length) { + if (hasDefaultNodes) { + const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); + const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); + set({ nodeInternals: nextNodeInternals }); + } + + onNodesChange?.(changes); + } + }, + addSelectedNodes: (selectedNodeIds: string[]) => { const { multiSelectionActive, nodeInternals, edges } = get(); let changedNodes: NodeSelectionChange[]; diff --git a/packages/core/src/types/changes.ts b/packages/core/src/types/changes.ts index ec2f6693..375c8bf2 100644 --- a/packages/core/src/types/changes.ts +++ b/packages/core/src/types/changes.ts @@ -7,8 +7,9 @@ import type { Edge } from './edges'; export type NodeDimensionChange = { id: string; type: 'dimensions'; - dimensions: Dimensions; + dimensions?: Dimensions; updateStyle?: boolean; + resizing?: boolean; }; export type NodePositionChange = { diff --git a/packages/core/src/types/general.ts b/packages/core/src/types/general.ts index 2daed27b..472df101 100644 --- a/packages/core/src/types/general.ts +++ b/packages/core/src/types/general.ts @@ -215,12 +215,7 @@ export type ReactFlowActions = { setEdges: (edges: Edge[]) => void; setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void; updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void; - updateNodePositions: ( - nodeDragItems: NodeDragItem[] | Node[], - positionChanged: boolean, - dragging: boolean, - applyChanges?: boolean - ) => NodePositionChange[] | null; + updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged: boolean, dragging: boolean) => void; resetSelectedElements: () => void; unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void; addSelectedNodes: (nodeIds: string[]) => void; @@ -231,6 +226,7 @@ export type ReactFlowActions = { setNodeExtent: (nodeExtent: CoordinateExtent) => void; cancelConnection: () => void; reset: () => void; + triggerNodeChanges: (changes: NodeChange[]) => void; }; export type ReactFlowState = ReactFlowStore & ReactFlowActions; diff --git a/packages/core/src/types/nodes.ts b/packages/core/src/types/nodes.ts index 39d8755c..c27e79cf 100644 --- a/packages/core/src/types/nodes.ts +++ b/packages/core/src/types/nodes.ts @@ -31,6 +31,7 @@ export type Node = { positionAbsolute?: XYPosition; ariaLabel?: string; focusable?: boolean; + resizing?: boolean; // only used internally [internalsSymbol]?: { diff --git a/packages/core/src/utils/changes.ts b/packages/core/src/utils/changes.ts index edfd0719..5deb3fdc 100644 --- a/packages/core/src/utils/changes.ts +++ b/packages/core/src/utils/changes.ts @@ -94,6 +94,10 @@ function applyChanges(changes: any[], elements: any[]): any[] { updateItem.style = { ...(updateItem.style || {}), ...currentChange.dimensions }; } + if (typeof currentChange.resizing === 'boolean') { + updateItem.resizing = currentChange.resizing; + } + if (updateItem.expandParent) { handleParentExpand(res, updateItem); } diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 75267d86..1b2aee1f 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -6,11 +6,17 @@ const lineControls: ControlLinePosition[] = ['top', 'right', 'bottom', 'left']; export default function NodeResizer({ nodeId, + isVisible = true, handleClassName, handleStyle, lineClassName, lineStyle, + color, }: NodeResizerProps) { + if (!isVisible) { + return null; + } + return ( <> {lineControls.map((c) => ( @@ -21,10 +27,18 @@ export default function NodeResizer({ nodeId={nodeId} position={c} variant={ResizeControlVariant.Line} + color={color} /> ))} {handleControls.map((c) => ( - + ))} ); diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index ec2e46dd..b7c42e59 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -7,21 +7,26 @@ import { useGetPointerPosition, NodeChange, NodeDimensionChange, - applyNodeChanges, - createNodeInternals, + useNodeId, + NodePositionChange, } from '@reactflow/core'; -import type { Dimensions, Node, XYPosition } from '@reactflow/core'; +import type { Dimensions, XYPosition } from '@reactflow/core'; import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; function ResizeControl({ nodeId, - position = 'bottom-right', + position, variant = ResizeControlVariant.Handle, className, style = {}, children, + color, + minWidth = 1, + minHeight = 1, }: ResizeControlProps) { + const contextNodeId = useNodeId(); + const id = typeof nodeId === 'string' ? nodeId : contextNodeId; const store = useStoreApi(); const resizeControlRef = useRef(null); const startValues = useRef({ @@ -32,17 +37,20 @@ function ResizeControl({ nodeX: 0, nodeY: 0, }); + const prevValues = useRef({ width: 0, height: 0, x: 0, y: 0 }); const getPointerPosition = useGetPointerPosition(); + const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'; + const controlPosition = position ?? defaultPosition; useEffect(() => { - if (!resizeControlRef.current) { + if (!resizeControlRef.current || !id) { return; } const selection = select(resizeControlRef.current); const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { - const node = store.getState().nodeInternals.get(nodeId); + const node = store.getState().nodeInternals.get(id); const { xSnapped, ySnapped } = getPointerPosition(event); startValues.current = { @@ -53,15 +61,22 @@ function ResizeControl({ x: xSnapped, y: ySnapped, }; + + prevValues.current = { + width: node?.width ?? 0, + height: node?.height ?? 0, + x: node?.position.x ?? 0, + y: node?.position.y ?? 0, + }; }) .on('drag', (event: ResizeDragEvent) => { - const { updateNodePositions, nodeInternals, onNodesChange, hasDefaultNodes, nodeOrigin } = store.getState(); + const { nodeInternals, triggerNodeChanges } = store.getState(); const { xSnapped, ySnapped } = getPointerPosition(event); - const node = nodeInternals.get(nodeId); - const enableX = position.includes('right') || position.includes('left'); - const enableY = position.includes('bottom') || position.includes('top'); - const invertX = position.includes('left'); - const invertY = position.includes('top'); + const node = nodeInternals.get(id); + const enableX = controlPosition.includes('right') || controlPosition.includes('left'); + const enableY = controlPosition.includes('bottom') || controlPosition.includes('top'); + const invertX = controlPosition.includes('left'); + const invertY = controlPosition.includes('top'); if (node) { const changes: NodeChange[] = []; @@ -73,57 +88,70 @@ function ResizeControl({ nodeX: startNodeX, nodeY: startNodeY, } = startValues.current; - const distX = enableX ? xSnapped - startX : 0; - const distY = enableY ? ySnapped - startY : 0; - const width = startWidth + (invertX ? -distX : distX); - const height = startHeight + (invertY ? -distY : distY); + + const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current; + + const distX = Math.floor(enableX ? xSnapped - startX : 0); + const distY = Math.floor(enableY ? ySnapped - startY : 0); + const width = Math.max(startWidth + (invertX ? -distX : distX), minWidth); + const height = Math.max(startHeight + (invertY ? -distY : distY), minHeight); + + const isWidthChange = width !== prevWidth; + const isHeightChange = height !== prevHeight; if (invertX || invertY) { - const x = invertX ? startNodeX + distX : startNodeX; - const y = invertY ? startNodeY + distY : startNodeY; + const x = invertX ? startNodeX - (width - startWidth) : startNodeX; + const y = invertY ? startNodeY - (height - startHeight) : startNodeY; - if (x !== node.position.x || y !== node.position.y) { - const positionChanges = updateNodePositions( - [ - { - id: nodeId, - position: { x, y }, - } as Node, - ], - true, - false, - false - ); + // only transform the node if the width or height changes + const isXPosChange = x !== prevX && isWidthChange; + const isYPosChange = y !== prevY && isHeightChange; - if (positionChanges?.length) { - changes.push(positionChanges[0]); - } + if (isXPosChange || isYPosChange) { + const positionChange: NodePositionChange = { + id: node.id, + type: 'position', + position: { + x: isXPosChange ? x : prevX, + y: isYPosChange ? y : prevY, + }, + }; + + changes.push(positionChange); + prevValues.current.x = positionChange.position!.x; + prevValues.current.y = positionChange.position!.y; } } - if (width !== node.width || height !== node.height) { + if (isWidthChange || isHeightChange) { const dimensionChange: NodeDimensionChange = { - id: nodeId, + id: id, type: 'dimensions', updateStyle: true, + resizing: true, dimensions: { - width: width !== node.width ? width : node.width, - height: height !== node.height ? height : node.height, + width: width, + height: height, }, }; changes.push(dimensionChange); + prevValues.current.width = width; + prevValues.current.height = height; } - if (changes.length) { - if (hasDefaultNodes) { - const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); - const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); - store.setState({ nodeInternals: nextNodeInternals }); - } - - onNodesChange?.(changes); - } + triggerNodeChanges(changes); } + }) + .on('end', () => { + const { triggerNodeChanges } = store.getState(); + + const dimensionChange: NodeDimensionChange = { + id: id, + type: 'dimensions', + resizing: true, + }; + + triggerNodeChanges([dimensionChange]); }); selection.call(dragHandler); @@ -131,15 +159,17 @@ function ResizeControl({ return () => { selection.on('.drag', null); }; - }, [nodeId, position, getPointerPosition]); + }, [id, controlPosition, getPointerPosition]); - const positionClassNames = position?.split('-'); + const positionClassNames = controlPosition.split('-'); + const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor'; + const controlStyle = color ? { ...style, [colorStyleProp]: color } : style; return (
{children}
diff --git a/packages/node-resizer/src/style.css b/packages/node-resizer/src/style.css index 71077758..0df9c1fb 100644 --- a/packages/node-resizer/src/style.css +++ b/packages/node-resizer/src/style.css @@ -1,9 +1,5 @@ .react-flow__resize-control { position: absolute; - background-color: rgba(195, 195, 195, 1); - border: 1px solid white; - width: 4px; - height: 4px; } .react-flow__resize-control.left, @@ -28,6 +24,11 @@ /* handle styles */ .react-flow__resize-control.handle { + width: 4px; + height: 4px; + border: 1px solid #fff; + border-radius: 1px; + background-color: #3367d9; transform: translate(-50%, -50%); } @@ -62,9 +63,11 @@ /* line styles */ .react-flow__resize-control.line { - border: none; - background: none; + border-color: #3367d9; + border-width: 0; + border-style: solid; } + .react-flow__resize-control.line.left, .react-flow__resize-control.line.right { width: 1px; @@ -75,11 +78,11 @@ .react-flow__resize-control.line.left { left: 0; - border-left: 1px solid rgba(195, 195, 195, 1); + border-left-width: 1px; } .react-flow__resize-control.line.right { left: 100%; - border-right: 1px solid rgba(195, 195, 195, 1); + border-right-width: 1px; } .react-flow__resize-control.line.top, @@ -92,9 +95,9 @@ .react-flow__resize-control.line.top { top: 0; - border-top: 1px solid rgba(195, 195, 195, 1); + border-top-width: 1px; } .react-flow__resize-control.line.bottom { - border-bottom: 1px solid rgba(195, 195, 195, 1); + border-bottom-width: 1px; top: 100%; } diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 9368e434..000ad536 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -2,11 +2,13 @@ import type { CSSProperties, ReactNode } from 'react'; import type { D3DragEvent, SubjectPosition } from 'd3-drag'; export type NodeResizerProps = { - nodeId: string; + nodeId?: string; + color?: string; handleClassName?: string; handleStyle?: CSSProperties; lineClassName?: string; lineStyle?: CSSProperties; + isVisible?: boolean; }; export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; @@ -19,12 +21,15 @@ export enum ResizeControlVariant { } export type ResizeControlProps = { - nodeId: string; - position: ControlPosition; + nodeId?: string; + position?: ControlPosition; variant?: ResizeControlVariant; + color?: string; className?: string; style?: CSSProperties; children?: ReactNode; + minWidth?: number; + minHeight?: number; }; export type ResizeControlLineProps = ResizeControlProps & { From 4c35893e0f45744a484753b4345c09771282fd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 6 Dec 2022 18:08:51 +0100 Subject: [PATCH 19/24] feat(node-toolbar): make nodeId optional and get id from context --- examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx | 2 +- packages/node-toolbar/src/NodeToolbar.tsx | 7 +++++-- packages/node-toolbar/src/types.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx b/examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx index 4f8ab270..ca2ac0b7 100644 --- a/examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx +++ b/examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx @@ -4,7 +4,7 @@ import { Handle, Position, NodeProps, NodeToolbar } from 'reactflow'; const CustomNode: FC = ({ id, data }) => { return ( <> - + diff --git a/packages/node-toolbar/src/NodeToolbar.tsx b/packages/node-toolbar/src/NodeToolbar.tsx index 1dfd7669..5c6cbc86 100644 --- a/packages/node-toolbar/src/NodeToolbar.tsx +++ b/packages/node-toolbar/src/NodeToolbar.tsx @@ -8,6 +8,7 @@ import { Rect, Position, internalsSymbol, + useNodeId, } from '@reactflow/core'; import cc from 'classcat'; import shallow from 'zustand/shallow'; @@ -72,9 +73,11 @@ function NodeToolbar({ offset = 10, ...rest }: NodeToolbarProps) { + const contextNodeId = useNodeId(); + const nodesSelector = useCallback( (state: ReactFlowState): Node[] => { - const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId; + const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; return nodeIds.reduce((acc, id) => { const node = state.nodeInternals.get(id); @@ -84,7 +87,7 @@ function NodeToolbar({ return acc; }, [] as Node[]); }, - [nodeId] + [nodeId, contextNodeId] ); const nodes = useStore(nodesSelector, nodesEqualityFn); const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); diff --git a/packages/node-toolbar/src/types.ts b/packages/node-toolbar/src/types.ts index a9f547ad..e2b0d73c 100644 --- a/packages/node-toolbar/src/types.ts +++ b/packages/node-toolbar/src/types.ts @@ -2,7 +2,7 @@ import { Position } from '@reactflow/core'; import type { HTMLAttributes } from 'react'; export type NodeToolbarProps = HTMLAttributes & { - nodeId: string | string[]; + nodeId?: string | string[]; isVisible?: boolean; position?: Position; offset?: number; From 449e468cd9ae8579227906e10855153eda16955a Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 18:14:06 +0100 Subject: [PATCH 20/24] chore(zoompane): use isWrapped helper func --- packages/core/src/container/ZoomPane/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index f72840b1..f94bbe58 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -231,7 +231,7 @@ const ZoomPane = ({ if ( event.button === 1 && event.type === 'mousedown' && - (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) + (isWrappedWithClass(event, 'react-flow__node') || isWrappedWithClass(event, 'react-flow__edge')) ) { return true; } From d29c401d598dbf2dcd5609b7adb8d029906a6f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 6 Dec 2022 18:14:26 +0100 Subject: [PATCH 21/24] chore(packages): add changesets --- .changeset/nasty-suns-hear.md | 5 +++++ .changeset/smooth-swans-exist.md | 5 +++++ .changeset/twelve-icons-tan.md | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .changeset/nasty-suns-hear.md create mode 100644 .changeset/smooth-swans-exist.md create mode 100644 .changeset/twelve-icons-tan.md diff --git a/.changeset/nasty-suns-hear.md b/.changeset/nasty-suns-hear.md new file mode 100644 index 00000000..1cc8c286 --- /dev/null +++ b/.changeset/nasty-suns-hear.md @@ -0,0 +1,5 @@ +--- +'@reactflow/node-toolbar': patch +--- + +Get nodeId from React Flow context if it is not passed explicitly as prop diff --git a/.changeset/smooth-swans-exist.md b/.changeset/smooth-swans-exist.md new file mode 100644 index 00000000..fcac6406 --- /dev/null +++ b/.changeset/smooth-swans-exist.md @@ -0,0 +1,5 @@ +--- +'@reactflow/core': patch +--- + +Export the useNodeId hook, refactor how changes are applied and create a helper function diff --git a/.changeset/twelve-icons-tan.md b/.changeset/twelve-icons-tan.md new file mode 100644 index 00000000..2c56eab6 --- /dev/null +++ b/.changeset/twelve-icons-tan.md @@ -0,0 +1,5 @@ +--- +'@reactflow/node-resizer': major +--- + +Add a node resizer component that can be used to resize a custom node From 531171234abb498c59c80d1c452ebaaeaf659815 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 18:28:54 +0100 Subject: [PATCH 22/24] chore(node-resizer): cleanup --- .../src/examples/NodeResizer/index.tsx | 9 ++-- packages/node-resizer/src/ResizeControl.tsx | 51 +++++++++---------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index 43f978ac..683f9ab5 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -43,9 +43,8 @@ const initialNodes = [ id: '3', type: 'customResizer', data: { label: 'resize control with child component' }, - position: { x: 250, y: 150 }, + position: { x: 250, y: 200 }, style: { border: '1px solid #222', fontSize: 10, width: 100 }, - parentNode: '2', }, { id: '4', @@ -53,13 +52,12 @@ const initialNodes = [ data: { label: 'resize controls' }, position: { x: 100, y: 150 }, style: { border: '1px solid #222', fontSize: 10 }, - parentNode: '2', }, { id: '5', type: 'customResizer2', data: { label: 'min width and height' }, - position: { x: 100, y: 150 }, + position: { x: 250, y: 250 }, style: { border: '1px solid #222', fontSize: 10 }, }, ]; @@ -82,9 +80,10 @@ const CustomNodeFlow = () => { onEdgesChange={onEdgesChange} onConnect={onConnect} nodeTypes={nodeTypes} - minZoom={-5} + minZoom={0.2} maxZoom={5} snapToGrid={snapToGrid} + fitView > diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx index b7c42e59..47050fec 100644 --- a/packages/node-resizer/src/ResizeControl.tsx +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -10,10 +10,17 @@ import { useNodeId, NodePositionChange, } from '@reactflow/core'; -import type { Dimensions, XYPosition } from '@reactflow/core'; import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; +const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; + +const initStartValues = { + ...initPrevValues, + pointerX: 0, + pointerY: 0, +}; + function ResizeControl({ nodeId, position, @@ -22,22 +29,15 @@ function ResizeControl({ style = {}, children, color, - minWidth = 1, - minHeight = 1, + minWidth = 10, + minHeight = 10, }: ResizeControlProps) { const contextNodeId = useNodeId(); const id = typeof nodeId === 'string' ? nodeId : contextNodeId; const store = useStoreApi(); const resizeControlRef = useRef(null); - const startValues = useRef({ - width: 0, - height: 0, - x: 0, - y: 0, - nodeX: 0, - nodeY: 0, - }); - const prevValues = useRef({ width: 0, height: 0, x: 0, y: 0 }); + const startValues = useRef(initStartValues); + const prevValues = useRef(initPrevValues); const getPointerPosition = useGetPointerPosition(); const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'; const controlPosition = position ?? defaultPosition; @@ -53,21 +53,18 @@ function ResizeControl({ const node = store.getState().nodeInternals.get(id); const { xSnapped, ySnapped } = getPointerPosition(event); - startValues.current = { - width: node?.width ?? 0, - height: node?.height ?? 0, - nodeX: node?.position.x ?? 0, - nodeY: node?.position.y ?? 0, - x: xSnapped, - y: ySnapped, - }; - prevValues.current = { width: node?.width ?? 0, height: node?.height ?? 0, x: node?.position.x ?? 0, y: node?.position.y ?? 0, }; + + startValues.current = { + ...prevValues.current, + pointerX: xSnapped, + pointerY: ySnapped, + }; }) .on('drag', (event: ResizeDragEvent) => { const { nodeInternals, triggerNodeChanges } = store.getState(); @@ -81,12 +78,12 @@ function ResizeControl({ if (node) { const changes: NodeChange[] = []; const { - x: startX, - y: startY, + pointerX: startX, + pointerY: startY, width: startWidth, height: startHeight, - nodeX: startNodeX, - nodeY: startNodeY, + x: startNodeX, + y: startNodeY, } = startValues.current; const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current; @@ -143,15 +140,13 @@ function ResizeControl({ } }) .on('end', () => { - const { triggerNodeChanges } = store.getState(); - const dimensionChange: NodeDimensionChange = { id: id, type: 'dimensions', resizing: true, }; - triggerNodeChanges([dimensionChange]); + store.getState().triggerNodeChanges([dimensionChange]); }); selection.call(dragHandler); From 8bb094551436d347c7187446ee7e4bb339a3923e Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 18:32:22 +0100 Subject: [PATCH 23/24] style(nodes): no outline for custom nodes --- packages/core/src/styles/style.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/core/src/styles/style.css b/packages/core/src/styles/style.css index 54b37f49..9850564d 100644 --- a/packages/core/src/styles/style.css +++ b/packages/core/src/styles/style.css @@ -13,6 +13,13 @@ } } +.react-flow__node.selectable { + &:focus, + &:focus-visible { + outline: none; + } +} + .react-flow__node-default, .react-flow__node-input, .react-flow__node-output, @@ -37,7 +44,6 @@ &:focus, &:focus-visible { box-shadow: 0 0 0 0.5px #1a192b; - outline: none; } } } From 7d7660f7355f2a31fabbd5e030b185e27ebb339d Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 6 Dec 2022 18:43:33 +0100 Subject: [PATCH 24/24] chore(packages): bump versions --- .changeset/lazy-pumpkins-remember.md | 6 ------ .changeset/mighty-needles-rush.md | 5 ----- .changeset/nasty-suns-hear.md | 5 ----- .changeset/perfect-pants-serve.md | 5 ----- .changeset/smooth-swans-exist.md | 5 ----- .changeset/tough-masks-teach.md | 5 ----- .changeset/twelve-icons-tan.md | 5 ----- examples/vite-app/package.json | 2 +- packages/background/CHANGELOG.md | 7 +++++++ packages/background/package.json | 2 +- packages/controls/CHANGELOG.md | 7 +++++++ packages/controls/package.json | 2 +- packages/core/CHANGELOG.md | 12 ++++++++++++ packages/core/package.json | 2 +- packages/minimap/CHANGELOG.md | 9 +++++++++ packages/minimap/package.json | 2 +- packages/node-resizer/CHANGELOG.md | 13 +++++++++++++ packages/node-resizer/package.json | 2 +- packages/node-toolbar/CHANGELOG.md | 9 +++++++++ packages/node-toolbar/package.json | 2 +- packages/reactflow/CHANGELOG.md | 13 +++++++++++++ packages/reactflow/package.json | 2 +- 22 files changed, 78 insertions(+), 44 deletions(-) delete mode 100644 .changeset/lazy-pumpkins-remember.md delete mode 100644 .changeset/mighty-needles-rush.md delete mode 100644 .changeset/nasty-suns-hear.md delete mode 100644 .changeset/perfect-pants-serve.md delete mode 100644 .changeset/smooth-swans-exist.md delete mode 100644 .changeset/tough-masks-teach.md delete mode 100644 .changeset/twelve-icons-tan.md diff --git a/.changeset/lazy-pumpkins-remember.md b/.changeset/lazy-pumpkins-remember.md deleted file mode 100644 index 8a5a3473..00000000 --- a/.changeset/lazy-pumpkins-remember.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@reactflow/core': patch -'@reactflow/minimap': patch ---- - -Fix getRectOfNodes diff --git a/.changeset/mighty-needles-rush.md b/.changeset/mighty-needles-rush.md deleted file mode 100644 index 0790ee82..00000000 --- a/.changeset/mighty-needles-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/core': patch ---- - -Allow middle mouse pan over edges diff --git a/.changeset/nasty-suns-hear.md b/.changeset/nasty-suns-hear.md deleted file mode 100644 index 1cc8c286..00000000 --- a/.changeset/nasty-suns-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/node-toolbar': patch ---- - -Get nodeId from React Flow context if it is not passed explicitly as prop diff --git a/.changeset/perfect-pants-serve.md b/.changeset/perfect-pants-serve.md deleted file mode 100644 index 851f79df..00000000 --- a/.changeset/perfect-pants-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/core': patch ---- - -Invalidate a Node trying to connect itself with the same handle diff --git a/.changeset/smooth-swans-exist.md b/.changeset/smooth-swans-exist.md deleted file mode 100644 index fcac6406..00000000 --- a/.changeset/smooth-swans-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/core': patch ---- - -Export the useNodeId hook, refactor how changes are applied and create a helper function diff --git a/.changeset/tough-masks-teach.md b/.changeset/tough-masks-teach.md deleted file mode 100644 index 433697f2..00000000 --- a/.changeset/tough-masks-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/core': patch ---- - -Ignore key events for nodes when input is focused diff --git a/.changeset/twelve-icons-tan.md b/.changeset/twelve-icons-tan.md deleted file mode 100644 index 2c56eab6..00000000 --- a/.changeset/twelve-icons-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reactflow/node-resizer': major ---- - -Add a node resizer component that can be used to resize a custom node diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 6554fc62..2a05d338 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -13,7 +13,7 @@ "test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'" }, "dependencies": { - "@reactflow/node-resizer": "workspace:^0.0.0", + "@reactflow/node-resizer": "workspace:^1.0.0", "classcat": "^5.0.3", "dagre": "^0.8.5", "localforage": "^1.10.0", diff --git a/packages/background/CHANGELOG.md b/packages/background/CHANGELOG.md index 1713e819..bdbef9c7 100644 --- a/packages/background/CHANGELOG.md +++ b/packages/background/CHANGELOG.md @@ -1,5 +1,12 @@ # @reactflow/background +## 11.0.7 + +### Patch Changes + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 + ## 11.0.6 ### Patch Changes diff --git a/packages/background/package.json b/packages/background/package.json index 7df6af43..5011906c 100644 --- a/packages/background/package.json +++ b/packages/background/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/background", - "version": "11.0.6", + "version": "11.0.7", "description": "Background component with different variants for React Flow", "keywords": [ "react", diff --git a/packages/controls/CHANGELOG.md b/packages/controls/CHANGELOG.md index 40de22b6..9df35ad9 100644 --- a/packages/controls/CHANGELOG.md +++ b/packages/controls/CHANGELOG.md @@ -1,5 +1,12 @@ # @reactflow/controls +## 11.0.7 + +### Patch Changes + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 + ## 11.0.6 ### Patch Changes diff --git a/packages/controls/package.json b/packages/controls/package.json index 91d19ade..7458ee83 100644 --- a/packages/controls/package.json +++ b/packages/controls/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/controls", - "version": "11.0.6", + "version": "11.0.7", "description": "Component to control the viewport of a React Flow instance", "keywords": [ "react", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 59a8cfca..b0b48f29 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,17 @@ # @reactflow/core +## 11.3.2 + +In this update we did some changes so that we could implement the new [``](https://reactflow.dev/docs/api/nodes/node-resizer/) component more smoothly. + +### Patch Changes + +- [#2646](https://github.com/wbkd/react-flow/pull/2646) [`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a) - Fix getRectOfNodes +- [#2648](https://github.com/wbkd/react-flow/pull/2648) [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae) - Allow middle mouse pan over edges +- [#2647](https://github.com/wbkd/react-flow/pull/2647) [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d) Thanks [@neo](https://github.com/neo)! - Invalidate node trying to connect itself with the same handle +- [#2626](https://github.com/wbkd/react-flow/pull/2626) [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18) - Export the useNodeId hook, refactor how changes are applied and create a helper function +- [#2642](https://github.com/wbkd/react-flow/pull/2642) [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6) - Ignore key events for nodes when input is focused + ## 11.3.1 ### Patch Changes diff --git a/packages/core/package.json b/packages/core/package.json index 58f31d51..9bd0f67b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/core", - "version": "11.3.1", + "version": "11.3.2", "description": "Core components and util functions of React Flow.", "keywords": [ "react", diff --git a/packages/minimap/CHANGELOG.md b/packages/minimap/CHANGELOG.md index b9cbb0be..a98b4072 100644 --- a/packages/minimap/CHANGELOG.md +++ b/packages/minimap/CHANGELOG.md @@ -1,5 +1,14 @@ # @reactflow/minimap +## 11.2.3 + +### Patch Changes + +- [#2646](https://github.com/wbkd/react-flow/pull/2646) [`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a) - Cleanup get node position with origin usage + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 + ## 11.2.2 ### Patch Changes diff --git a/packages/minimap/package.json b/packages/minimap/package.json index d6dc1331..ce4db650 100644 --- a/packages/minimap/package.json +++ b/packages/minimap/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/minimap", - "version": "11.2.2", + "version": "11.2.3", "description": "Minimap component for React Flow.", "keywords": [ "react", diff --git a/packages/node-resizer/CHANGELOG.md b/packages/node-resizer/CHANGELOG.md index a5e10d68..a6ead91f 100644 --- a/packages/node-resizer/CHANGELOG.md +++ b/packages/node-resizer/CHANGELOG.md @@ -1 +1,14 @@ # @reactflow/node-resizer + +## 1.0.0 + +This is a new package that exports components to build a UI for resizing a node 🎉 It exports a [``](https://reactflow.dev/docs/api/nodes/node-resizer/) component and a [``](https://reactflow.dev/docs/api/nodes/node-resizer/#noderesizecontrol--component) component. + +### Major Changes + +- [#2626](https://github.com/wbkd/react-flow/pull/2626) [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18) - Add a node resizer component that can be used to resize a custom node + +### Patch Changes + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json index 0a169f31..43b00744 100644 --- a/packages/node-resizer/package.json +++ b/packages/node-resizer/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/node-resizer", - "version": "0.0.0", + "version": "1.0.0", "description": "A helper component for resizing nodes.", "keywords": [ "react", diff --git a/packages/node-toolbar/CHANGELOG.md b/packages/node-toolbar/CHANGELOG.md index 7bad5ebf..624d1510 100644 --- a/packages/node-toolbar/CHANGELOG.md +++ b/packages/node-toolbar/CHANGELOG.md @@ -1,5 +1,14 @@ # @reactflow/node-toolbar +## 1.0.2 + +### Patch Changes + +- [#2626](https://github.com/wbkd/react-flow/pull/2626) [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18) - Get nodeId from React Flow context if it is not passed explicitly as prop + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 + ## 1.0.1 ### Patch Changes diff --git a/packages/node-toolbar/package.json b/packages/node-toolbar/package.json index 18f05350..2163d4ce 100644 --- a/packages/node-toolbar/package.json +++ b/packages/node-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@reactflow/node-toolbar", - "version": "1.0.1", + "version": "1.0.2", "description": "A toolbar component for React Flow that can be attached to a node.", "keywords": [ "react", diff --git a/packages/reactflow/CHANGELOG.md b/packages/reactflow/CHANGELOG.md index f3141ce1..dfeb78db 100644 --- a/packages/reactflow/CHANGELOG.md +++ b/packages/reactflow/CHANGELOG.md @@ -1,5 +1,18 @@ # reactflow +## 11.3.3 + +In this update we did some changes so that we could implement the new [``](https://reactflow.dev/docs/api/nodes/node-resizer/) component (not part of the `reactflow` package!) more smoothly. + +### Patch Changes + +- Updated dependencies [[`e6b5d90f`](https://github.com/wbkd/react-flow/commit/e6b5d90f61c8ee60e817bba232a162cae2ab3e2a), [`6ee44e07`](https://github.com/wbkd/react-flow/commit/6ee44e076eaa6908d07578a757a5187642b732ae), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`aa69c207`](https://github.com/wbkd/react-flow/commit/aa69c20765e6978f4f9c8cc63ed7110dbf6d9d9d), [`d29c401d`](https://github.com/wbkd/react-flow/commit/d29c401d598dbf2dcd5609b7adb8d029906a6f18), [`0df02f35`](https://github.com/wbkd/react-flow/commit/0df02f35f8d6c54dae36af18278feadc77acb2d6)]: + - @reactflow/core@11.3.2 + - @reactflow/minimap@11.2.3 + - @reactflow/node-toolbar@1.0.2 + - @reactflow/background@11.0.7 + - @reactflow/controls@11.0.7 + ## 11.3.2 ### Patch Changes diff --git a/packages/reactflow/package.json b/packages/reactflow/package.json index f84f948a..f1f3ecab 100644 --- a/packages/reactflow/package.json +++ b/packages/reactflow/package.json @@ -1,6 +1,6 @@ { "name": "reactflow", - "version": "11.3.2", + "version": "11.3.3", "description": "A highly customizable React library for building node-based editors and interactive flow charts", "keywords": [ "react",