From d057a1cd346d76f8168ea8bf5e9ce7e57bb08ce5 Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 9 Oct 2022 19:55:25 +0200 Subject: [PATCH 01/12] feat(packages): add interactive minimap --- examples/vite-app/package.json | 3 +- examples/vite-app/src/App/index.tsx | 6 + .../src/examples/InteractiveMinimap/index.tsx | 121 +++++++++++++++++ packages/interactive-minimap/.eslintrc.js | 4 + packages/interactive-minimap/CHANGELOG.md | 42 ++++++ packages/interactive-minimap/README.md | 10 ++ packages/interactive-minimap/package.json | 65 +++++++++ packages/interactive-minimap/src/MiniMap.tsx | 121 +++++++++++++++++ .../interactive-minimap/src/MiniMapNode.tsx | 53 ++++++++ packages/interactive-minimap/src/index.tsx | 2 + packages/interactive-minimap/src/style.css | 3 + packages/interactive-minimap/src/types.ts | 15 +++ packages/interactive-minimap/tsconfig.json | 6 + pnpm-lock.yaml | 125 +++++++++++------- 14 files changed, 527 insertions(+), 49 deletions(-) create mode 100644 examples/vite-app/src/examples/InteractiveMinimap/index.tsx create mode 100644 packages/interactive-minimap/.eslintrc.js create mode 100644 packages/interactive-minimap/CHANGELOG.md create mode 100644 packages/interactive-minimap/README.md create mode 100644 packages/interactive-minimap/package.json create mode 100644 packages/interactive-minimap/src/MiniMap.tsx create mode 100644 packages/interactive-minimap/src/MiniMapNode.tsx create mode 100644 packages/interactive-minimap/src/index.tsx create mode 100644 packages/interactive-minimap/src/style.css create mode 100644 packages/interactive-minimap/src/types.ts create mode 100644 packages/interactive-minimap/tsconfig.json diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 23bbc7fc..27ace109 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -19,7 +19,8 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.3.0", - "reactflow": "workspace:*" + "reactflow": "workspace:*", + "@reactflow/interactive-minimap": "workspace:*" }, "devDependencies": { "@cypress/skip-test": "^2.6.1", diff --git a/examples/vite-app/src/App/index.tsx b/examples/vite-app/src/App/index.tsx index 0ec2a021..71c4989d 100644 --- a/examples/vite-app/src/App/index.tsx +++ b/examples/vite-app/src/App/index.tsx @@ -36,6 +36,7 @@ import Validation from '../examples/Validation'; import UseKeyPress from '../examples/UseKeyPress'; import EdgeRouting from '../examples/EdgeRouting'; import CancelConnection from '../examples/CancelConnection'; +import InteractiveMinimap from '../examples/InteractiveMinimap'; interface IRoute { name: string; @@ -124,6 +125,11 @@ const routes: IRoute[] = [ path: '/interaction', component: Interaction, }, + { + name: 'Interactive Minimap', + path: '/interactive-minimap', + component: InteractiveMinimap, + }, { name: 'Layouting', path: '/layouting', diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx new file mode 100644 index 00000000..f80b2971 --- /dev/null +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -0,0 +1,121 @@ +import { MouseEvent } from 'react'; +import ReactFlow, { + Background, + BackgroundVariant, + Controls, + ReactFlowProvider, + Node, + Edge, + useReactFlow, +} from 'reactflow'; +import { MiniMap } from '@reactflow/interactive-minimap'; + +const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); +const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); +const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); + +const initialNodes: Node[] = [ + { + id: '1', + type: 'input', + data: { label: 'Node 1' }, + position: { x: 250, y: 5 }, + className: 'light', + }, + { + id: '2', + data: { label: 'Node 2' }, + position: { x: 100, y: 100 }, + className: 'light', + }, + { + id: '3', + data: { label: 'Node 3' }, + position: { x: 400, y: 100 }, + className: 'light', + }, + { + id: '4', + data: { label: 'Node 4' }, + position: { x: 400, y: 200 }, + className: 'light', + }, +]; + +const initialEdges: Edge[] = [ + { id: 'e1-2', source: '1', target: '2', animated: true }, + { id: 'e1-3', source: '1', target: '3' }, +]; + +const defaultEdgeOptions = { zIndex: 0 }; + +const BasicFlow = () => { + const instance = useReactFlow(); + + const updatePos = () => { + instance.setNodes((nodes) => + nodes.map((node) => { + node.position = { + x: Math.random() * 400, + y: Math.random() * 400, + }; + + return node; + }) + ); + }; + + const logToObject = () => console.log(instance.toObject()); + const resetTransform = () => instance.setViewport({ x: 0, y: 0, zoom: 1 }); + + const toggleClassnames = () => { + instance.setNodes((nodes) => + nodes.map((node) => { + node.className = node.className === 'light' ? 'dark' : 'light'; + + return node; + }) + ); + }; + + return ( + + + + + +
+ + + + +
+
+ ); +}; + +export default function App() { + return ( + + + + ); +} diff --git a/packages/interactive-minimap/.eslintrc.js b/packages/interactive-minimap/.eslintrc.js new file mode 100644 index 00000000..31cfbd11 --- /dev/null +++ b/packages/interactive-minimap/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['@reactflow/eslint-config'], +}; diff --git a/packages/interactive-minimap/CHANGELOG.md b/packages/interactive-minimap/CHANGELOG.md new file mode 100644 index 00000000..eb44ff49 --- /dev/null +++ b/packages/interactive-minimap/CHANGELOG.md @@ -0,0 +1,42 @@ +# @reactflow/minimap + +## 11.0.1 + +### Patch Changes + +- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]: + - @reactflow/core@11.1.0 + +## 11.0.0 + +### Major Changes + +- **Better [Accessibility](/docs/guides/accessibility)** + - Nodes and edges are focusable, selectable, moveable and deleteable with the keyboard. + - `aria-` default attributes for all elements and controllable via `ariaLabel` options + - Keyboard controls can be disabled with the new `disableKeyboardA11y` prop +- **Better selectable edges** via new edge option: `interactionWidth` - renders invisible edge that makes it easier to interact +- **Better routing for smoothstep and step edges**: https://twitter.com/reactflowdev/status/1567535405284614145 +- **Nicer edge updating behaviour**: https://twitter.com/reactflowdev/status/1564966917517021184 +- **Node origin**: The new `nodeOrigin` prop lets you control the origin of a node. Useful for layouting. +- **New background pattern**: `BackgroundVariant.Cross` variant +- **[`useOnViewportChange`](/docs/api/hooks/use-on-viewport-change) hook** - handle viewport changes within a component +- **[`useOnSelectionChange`](/docs/api/hooks/use-on-selection-change) hook** - handle selection changes within a component +- **[`useNodesInitialized`](/docs/api/hooks/use-nodes-initialized) hook** - returns true if all nodes are initialized and if there is more than one node +- **Deletable option** for Nodes and edges +- **New Event handlers**: `onPaneMouseEnter`, `onPaneMouseMove` and `onPaneMouseLeave` +- **Edge `pathOptions`** for `smoothstep` and `default` edges +- **Nicer cursor defaults**: Cursor is grabbing, while dragging a node or panning +- **Pane moveable** with middle mouse button +- **Pan over nodes** when they are not draggable (`draggable=false` or `nodesDraggable` false) +- **[``](/docs/api/edges/base-edge) component** that makes it easier to build custom edges +- **[Separately installable packages](/docs/overview/packages/)** + - @reactflow/core + - @reactflow/background + - @reactflow/controls + - @reactflow/minimap + +### Patch Changes + +- Updated dependencies: + - @reactflow/core@11.0.0 diff --git a/packages/interactive-minimap/README.md b/packages/interactive-minimap/README.md new file mode 100644 index 00000000..2dba17d9 --- /dev/null +++ b/packages/interactive-minimap/README.md @@ -0,0 +1,10 @@ +# @reactflow/minimap + +Mini map component for React Flow. + +## Installation + +```sh +npm install @reactflow/minimap +``` + diff --git a/packages/interactive-minimap/package.json b/packages/interactive-minimap/package.json new file mode 100644 index 00000000..e5409e1a --- /dev/null +++ b/packages/interactive-minimap/package.json @@ -0,0 +1,65 @@ +{ + "name": "@reactflow/interactive-minimap", + "version": "0.0.1-alpha.0", + "description": "Interactive Minimap component for React Flow.", + "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": false, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/wbkd/react-flow.git", + "directory": "packages/interactive-minimap" + }, + "scripts": { + "dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch", + "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" + }, + "dependencies": { + "@babel/runtime": "^7.18.9", + "@reactflow/core": "workspace:*", + "classcat": "^5.0.3", + "zustand": "^4.1.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + }, + "devDependencies": { + "@reactflow/eslint-config": "workspace:^0.0.0", + "@reactflow/rollup-config": "workspace:*", + "@reactflow/tsconfig": "workspace:*", + "@types/node": "^18.7.16", + "@types/react": "^18.0.19", + "react": "^18.2.0", + "typescript": "^4.8.3" + }, + "rollup": { + "globals": { + "zustand": "Zustand", + "zustand/shallow": "zustandShallow", + "classcat": "cc" + }, + "name": "ReactFlowMinimap" + } +} diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx new file mode 100644 index 00000000..b7ef604c --- /dev/null +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -0,0 +1,121 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { memo, useEffect, useRef } from 'react'; +import cc from 'classcat'; +import shallow from 'zustand/shallow'; +import { zoom, D3ZoomEvent } from 'd3-zoom'; +import { select } from 'd3-selection'; +import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects } from '@reactflow/core'; + +import MiniMapNode from './MiniMapNode'; +import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; + +declare const window: any; + +const defaultWidth = 200; +const defaultHeight = 150; + +const selector = (s: ReactFlowState) => { + const nodes = Array.from(s.nodeInternals.values()); + const viewBB: Rect = { + x: -s.transform[0] / s.transform[2], + y: -s.transform[1] / s.transform[2], + width: s.width / s.transform[2], + height: s.height / s.transform[2], + }; + + return { + nodes: nodes.filter((node) => !node.hidden && node.width && node.height), + viewBB, + boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, + rfId: s.rfId, + }; +}; + +const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); + +const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; + +function MiniMap({ + style, + className, + nodeStrokeColor = '#555', + nodeColor = '#fff', + nodeClassName = '', + nodeBorderRadius = 5, + nodeStrokeWidth = 2, + maskColor = 'rgb(240, 242, 243, 0.7)', + position = 'bottom-right', +}: MiniMapProps) { + const svg = useRef(null); + const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); + const elementWidth = (style?.width as number) ?? defaultWidth; + const elementHeight = (style?.height as number) ?? defaultHeight; + const nodeColorFunc = getAttrFunction(nodeColor); + const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); + const nodeClassNameFunc = getAttrFunction(nodeClassName); + const scaledWidth = boundingRect.width / elementWidth; + const scaledHeight = boundingRect.height / elementHeight; + const viewScale = Math.max(scaledWidth, scaledHeight); + const viewWidth = viewScale * elementWidth; + const viewHeight = viewScale * elementHeight; + const offset = 5 * viewScale; + const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; + const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; + const width = viewWidth + offset * 2; + const height = viewHeight + offset * 2; + const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; + const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; + + useEffect(() => { + const d3ZoomInstance = zoom(); + const selection = select(svg.current as Element).call(d3ZoomInstance); + + d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => { + console.log(event); + }); + }, []); + + return ( + + + React Flow mini map + {nodes.map((node) => { + return ( + + ); + })} + + + + ); +} + +MiniMap.displayName = 'MiniMap'; + +export default memo(MiniMap); diff --git a/packages/interactive-minimap/src/MiniMapNode.tsx b/packages/interactive-minimap/src/MiniMapNode.tsx new file mode 100644 index 00000000..3ea3bd33 --- /dev/null +++ b/packages/interactive-minimap/src/MiniMapNode.tsx @@ -0,0 +1,53 @@ +import { memo, CSSProperties } from 'react'; +import cc from 'classcat'; + +interface MiniMapNodeProps { + x: number; + y: number; + width: number; + height: number; + borderRadius: number; + className: string; + color: string; + shapeRendering: string; + strokeColor: string; + strokeWidth: number; + style?: CSSProperties; +} + +const MiniMapNode = ({ + x, + y, + width, + height, + style, + color, + strokeColor, + strokeWidth, + className, + borderRadius, + shapeRendering, +}: MiniMapNodeProps) => { + const { background, backgroundColor } = style || {}; + const fill = (color || background || backgroundColor) as string; + + return ( + + ); +}; + +MiniMapNode.displayName = 'MiniMapNode'; + +export default memo(MiniMapNode); diff --git a/packages/interactive-minimap/src/index.tsx b/packages/interactive-minimap/src/index.tsx new file mode 100644 index 00000000..7b91e95d --- /dev/null +++ b/packages/interactive-minimap/src/index.tsx @@ -0,0 +1,2 @@ +export { default as MiniMap } from './MiniMap'; +export * from './types'; diff --git a/packages/interactive-minimap/src/style.css b/packages/interactive-minimap/src/style.css new file mode 100644 index 00000000..dbb9a931 --- /dev/null +++ b/packages/interactive-minimap/src/style.css @@ -0,0 +1,3 @@ +.react-flow__minimap { + background-color: #fff; +} diff --git a/packages/interactive-minimap/src/types.ts b/packages/interactive-minimap/src/types.ts new file mode 100644 index 00000000..5f82824d --- /dev/null +++ b/packages/interactive-minimap/src/types.ts @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { HTMLAttributes } from 'react'; +import { Node, PanelPosition } from '@reactflow/core'; + +export type GetMiniMapNodeAttribute = (node: Node) => string; + +export interface MiniMapProps extends HTMLAttributes { + nodeColor?: string | GetMiniMapNodeAttribute; + nodeStrokeColor?: string | GetMiniMapNodeAttribute; + nodeClassName?: string | GetMiniMapNodeAttribute; + nodeBorderRadius?: number; + nodeStrokeWidth?: number; + maskColor?: string; + position?: PanelPosition; +} diff --git a/packages/interactive-minimap/tsconfig.json b/packages/interactive-minimap/tsconfig.json new file mode 100644 index 00000000..8a8e74ce --- /dev/null +++ b/packages/interactive-minimap/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "@reactflow/tsconfig/react.json", + "display": "@reactflow/minimap", + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26177a37..924aeef0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,8 +32,8 @@ importers: '@changesets/changelog-github': registry.npmjs.org/@changesets/changelog-github/0.4.6 '@changesets/cli': registry.npmjs.org/@changesets/cli/2.24.4 '@preconstruct/cli': registry.npmjs.org/@preconstruct/cli/2.2.1 - '@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.38.0_wsb62dxj2oqwgas4kadjymcmry - '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0_jdrczqv3ll7udvbunca43w7rz4 + '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha autoprefixer: registry.npmjs.org/autoprefixer/10.4.9_postcss@8.4.16 concurrently: registry.npmjs.org/concurrently/7.4.0 cypress: registry.npmjs.org/cypress/10.7.0 @@ -57,6 +57,7 @@ importers: examples/vite-app: specifiers: '@cypress/skip-test': ^2.6.1 + '@reactflow/interactive-minimap': workspace:* '@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/interactive-minimap': link:../../packages/interactive-minimap classcat: registry.npmjs.org/classcat/5.0.4 dagre: registry.npmjs.org/dagre/0.8.5 localforage: registry.npmjs.org/localforage/1.10.0 @@ -178,6 +180,33 @@ importers: react: registry.npmjs.org/react/18.2.0 typescript: registry.npmjs.org/typescript/4.8.3 + packages/interactive-minimap: + specifiers: + '@babel/runtime': ^7.18.9 + '@reactflow/core': workspace:* + '@reactflow/eslint-config': workspace:^0.0.0 + '@reactflow/rollup-config': workspace:* + '@reactflow/tsconfig': workspace:* + '@types/node': ^18.7.16 + '@types/react': ^18.0.19 + classcat: ^5.0.3 + 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 + 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/node': registry.npmjs.org/@types/node/18.7.16 + '@types/react': registry.npmjs.org/@types/react/18.0.19 + react: registry.npmjs.org/react/18.2.0 + typescript: registry.npmjs.org/typescript/4.8.3 + packages/minimap: specifiers: '@babel/runtime': ^7.18.9 @@ -1704,11 +1733,11 @@ packages: dev: true optional: true - registry.npmjs.org/@typescript-eslint/eslint-plugin/5.38.0_wsb62dxj2oqwgas4kadjymcmry: - resolution: {integrity: sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz} - id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.38.0 + registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0_jdrczqv3ll7udvbunca43w7rz4: + resolution: {integrity: sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz} + id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0 name: '@typescript-eslint/eslint-plugin' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -1718,10 +1747,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.38.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.38.0 - '@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.38.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 + '@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.39.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 ignore: registry.npmjs.org/ignore/5.2.0 @@ -1733,11 +1762,11 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/parser/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.0.tgz} - id: registry.npmjs.org/@typescript-eslint/parser/5.38.0 + registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz} + id: registry.npmjs.org/@typescript-eslint/parser/5.39.0 name: '@typescript-eslint/parser' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1746,9 +1775,9 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.38.0 - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.38.0 - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3 + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 typescript: registry.npmjs.org/typescript/4.8.3 @@ -1756,21 +1785,21 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/scope-manager/5.38.0: - resolution: {integrity: sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz} + registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0: + resolution: {integrity: sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz} name: '@typescript-eslint/scope-manager' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.38.0 - '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.38.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 + '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0 dev: true - registry.npmjs.org/@typescript-eslint/type-utils/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz} - id: registry.npmjs.org/@typescript-eslint/type-utils/5.38.0 + registry.npmjs.org/@typescript-eslint/type-utils/5.39.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz} + id: registry.npmjs.org/@typescript-eslint/type-utils/5.39.0 name: '@typescript-eslint/type-utils' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -1779,8 +1808,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3 - '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.38.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 + '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.8.3 @@ -1789,18 +1818,18 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/types/5.38.0: - resolution: {integrity: sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.0.tgz} + registry.npmjs.org/@typescript-eslint/types/5.39.0: + resolution: {integrity: sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz} name: '@typescript-eslint/types' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - registry.npmjs.org/@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3: - resolution: {integrity: sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz} - id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.38.0 + registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3: + resolution: {integrity: sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz} + id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0 name: '@typescript-eslint/typescript-estree' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -1808,8 +1837,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.38.0 - '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.38.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 + '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0 debug: registry.npmjs.org/debug/4.3.4 globby: registry.npmjs.org/globby/11.1.0 is-glob: registry.npmjs.org/is-glob/4.0.3 @@ -1820,19 +1849,19 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/utils/5.38.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.0.tgz} - id: registry.npmjs.org/@typescript-eslint/utils/5.38.0 + registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.39.0.tgz} + id: registry.npmjs.org/@typescript-eslint/utils/5.39.0 name: '@typescript-eslint/utils' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': registry.npmjs.org/@types/json-schema/7.0.11 - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.38.0 - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.38.0 - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.38.0_typescript@4.8.3 + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 eslint: registry.npmjs.org/eslint/8.23.1 eslint-scope: registry.npmjs.org/eslint-scope/5.1.1 eslint-utils: registry.npmjs.org/eslint-utils/3.0.0_eslint@8.23.1 @@ -1841,13 +1870,13 @@ packages: - typescript dev: true - registry.npmjs.org/@typescript-eslint/visitor-keys/5.38.0: - resolution: {integrity: sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz} + registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0: + resolution: {integrity: sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz} name: '@typescript-eslint/visitor-keys' - version: 5.38.0 + version: 5.39.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.38.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys/3.3.0 dev: true From 322ea0ebedd527d2ffc15d209ce0c5a1ef06fa4c Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 31 Oct 2022 17:55:32 +0100 Subject: [PATCH 02/12] feat(interactive-minimap): add zoom and pan --- .../src/examples/InteractiveMinimap/index.tsx | 10 +-- .../core/src/container/ZoomPane/index.tsx | 8 +- packages/interactive-minimap/src/MiniMap.tsx | 82 +++++++++++++++++-- 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index f80b2971..66cab6a0 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -19,25 +19,25 @@ const initialNodes: Node[] = [ id: '1', type: 'input', data: { label: 'Node 1' }, - position: { x: 250, y: 5 }, + position: { x: 0, y: 0 }, className: 'light', }, { id: '2', data: { label: 'Node 2' }, - position: { x: 100, y: 100 }, + position: { x: 0, y: 200 }, className: 'light', }, { id: '3', data: { label: 'Node 3' }, - position: { x: 400, y: 100 }, + position: { x: 200, y: 200 }, className: 'light', }, { id: '4', data: { label: 'Node 4' }, - position: { x: 400, y: 200 }, + position: { x: 200, y: 400 }, className: 'light', }, ]; @@ -88,9 +88,9 @@ const BasicFlow = () => { className="react-flow-basic-example" minZoom={0.2} maxZoom={4} - fitView defaultEdgeOptions={defaultEdgeOptions} selectNodesOnDrag={false} + fitView > diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index cde4ec47..a5783a7a 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -173,9 +173,12 @@ const ZoomPane = ({ useEffect(() => { if (d3Zoom) { d3Zoom.on('start', (event: D3ZoomEvent) => { + if (!event.sourceEvent) { + return null; + } + const { onViewportChangeStart } = store.getState(); isZoomingOrPanning.current = true; - if (event.sourceEvent?.type === 'mousedown') { store.setState({ paneDragging: true }); } @@ -194,6 +197,9 @@ const ZoomPane = ({ useEffect(() => { if (d3Zoom) { d3Zoom.on('end', (event: D3ZoomEvent) => { + if (!event.sourceEvent) { + return null; + } const { onViewportChangeEnd } = store.getState(); isZoomingOrPanning.current = false; diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index b7ef604c..e5a7927e 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -2,9 +2,19 @@ import { memo, useEffect, useRef } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; -import { zoom, D3ZoomEvent } from 'd3-zoom'; +import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; import { select } from 'd3-selection'; -import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects } from '@reactflow/core'; +import { + useStore, + getRectOfNodes, + ReactFlowState, + Rect, + Panel, + getBoundsOfRects, + useStoreApi, + useReactFlow, + XYPosition, +} from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; @@ -46,7 +56,9 @@ function MiniMap({ maskColor = 'rgb(240, 242, 243, 0.7)', position = 'bottom-right', }: MiniMapProps) { + const store = useStoreApi(); const svg = useRef(null); + const initialized = useRef(false); const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; @@ -65,15 +77,69 @@ function MiniMap({ const height = viewHeight + offset * 2; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; + const { setCenter } = useReactFlow(); + const startTransform = useRef({ x: 0, y: 0 }); + const viewScaleRef = useRef(0); + const viewScaleWRef = useRef(0); + const viewScaleHRef = useRef(0); + + viewScaleRef.current = viewScale; + viewScaleWRef.current = scaledWidth; + viewScaleHRef.current = scaledHeight; useEffect(() => { - const d3ZoomInstance = zoom(); - const selection = select(svg.current as Element).call(d3ZoomInstance); + if (!initialized.current && svg.current) { + const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 }; - d3ZoomInstance.on('zoom', (event: D3ZoomEvent) => { - console.log(event); - }); - }, []); + const selection = select(svg.current as Element); + + const zoomHandler = zoom() + .on('start', (event: D3ZoomEvent) => { + const { transform } = store.getState(); + + const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; + const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; + + startTransform.current = { + x: px - transform[0] / transform[2], + y: py - transform[1] / transform[2], + }; + }) + .on('zoom.wheel', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { + return; + } + + const pinchDelta = + -event.sourceEvent.deltaY * + (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * + 10; + const zoom = transform[2] * Math.pow(2, pinchDelta); + d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]); + }) + .on('zoom', (event: D3ZoomEvent) => { + if (event.sourceEvent.type !== 'mousemove') { + return; + } + + const { transform, d3Selection, d3Zoom } = store.getState(); + const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; + const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; + + const position = { + x: (px - startTransform.current.x) * transform[2], + y: (py - startTransform.current.y) * transform[2], + }; + + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + d3Zoom!.transform(d3Selection!, nextTransform); + }); + selection.call(zoomHandler); + initialized.current = true; + } + }, [setCenter]); return ( From f71ee4223f78c34633ced1cef7028d58c11d210e Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 31 Oct 2022 20:11:03 +0100 Subject: [PATCH 03/12] fix(interactive-mini): add deps --- packages/interactive-minimap/package.json | 2 ++ pnpm-lock.yaml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/interactive-minimap/package.json b/packages/interactive-minimap/package.json index e5409e1a..d3b50f6f 100644 --- a/packages/interactive-minimap/package.json +++ b/packages/interactive-minimap/package.json @@ -39,6 +39,8 @@ "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", "zustand": "^4.1.1" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aaf55e75..ca0b28de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,8 @@ importers: '@types/node': ^18.7.16 '@types/react': ^18.0.19 classcat: ^5.0.3 + d3-selection: ^3.0.0 + d3-zoom: ^3.0.0 react: ^18.2.0 typescript: ^4.8.3 zustand: ^4.1.1 @@ -197,6 +199,8 @@ importers: '@babel/runtime': registry.npmjs.org/@babel/runtime/7.19.0 '@reactflow/core': link:../core classcat: registry.npmjs.org/classcat/5.0.4 + d3-selection: registry.npmjs.org/d3-selection/3.0.0 + d3-zoom: registry.npmjs.org/d3-zoom/3.0.0 zustand: registry.npmjs.org/zustand/4.1.1_react@18.2.0 devDependencies: '@reactflow/eslint-config': link:../../tooling/eslint-config From 6eec913c89159b441e0c5044c258328426ffc978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Tue, 1 Nov 2022 13:37:53 +0100 Subject: [PATCH 04/12] feat(interactive-minimap): add type defs for d3 packages --- packages/interactive-minimap/package.json | 2 ++ pnpm-lock.yaml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/interactive-minimap/package.json b/packages/interactive-minimap/package.json index d3b50f6f..8dc4a82a 100644 --- a/packages/interactive-minimap/package.json +++ b/packages/interactive-minimap/package.json @@ -38,6 +38,8 @@ "dependencies": { "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca0b28de..91f51a8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -187,6 +187,8 @@ importers: '@reactflow/eslint-config': workspace:^0.0.0 '@reactflow/rollup-config': workspace:* '@reactflow/tsconfig': workspace:* + '@types/d3-selection': ^3.0.3 + '@types/d3-zoom': ^3.0.1 '@types/node': ^18.7.16 '@types/react': ^18.0.19 classcat: ^5.0.3 @@ -198,6 +200,8 @@ importers: dependencies: '@babel/runtime': registry.npmjs.org/@babel/runtime/7.19.0 '@reactflow/core': link:../core + '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 + '@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1 classcat: registry.npmjs.org/classcat/5.0.4 d3-selection: registry.npmjs.org/d3-selection/3.0.0 d3-zoom: registry.npmjs.org/d3-zoom/3.0.0 From 4098ced7d80ce7e2507754de169a1bdbc89d680a Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 14:12:31 +0100 Subject: [PATCH 05/12] refactor(interactive-minimap): drag mask instead of pane --- .../src/examples/InteractiveMinimap/index.tsx | 57 ++++-- packages/interactive-minimap/src/MiniMap.tsx | 64 +++--- .../interactive-minimap/src/MiniMapDrag.tsx | 187 ++++++++++++++++++ 3 files changed, 261 insertions(+), 47 deletions(-) create mode 100644 packages/interactive-minimap/src/MiniMapDrag.tsx diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index 66cab6a0..cde9b722 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -17,35 +17,70 @@ const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const initialNodes: Node[] = [ { id: '1', - type: 'input', data: { label: 'Node 1' }, position: { x: 0, y: 0 }, - className: 'light', }, { id: '2', data: { label: 'Node 2' }, position: { x: 0, y: 200 }, - className: 'light', }, { id: '3', data: { label: 'Node 3' }, - position: { x: 200, y: 200 }, - className: 'light', + position: { x: 200, y: 0 }, }, + { id: '4', data: { label: 'Node 4' }, - position: { x: 200, y: 400 }, - className: 'light', + position: { x: 1000, y: 0 }, + }, + { + id: '5', + data: { label: 'Node 5' }, + position: { x: 1000, y: 200 }, + }, + { + id: '6', + data: { label: 'Node 6' }, + position: { x: 800, y: 0 }, + }, + + { + id: '7', + data: { label: 'Node 4' }, + position: { x: 0, y: 1000 }, + }, + { + id: '8', + data: { label: 'Node 5' }, + position: { x: 0, y: 800 }, + }, + { + id: '9', + data: { label: 'Node 6' }, + position: { x: 200, y: 1000 }, + }, + + { + id: '10', + data: { label: 'Node 4' }, + position: { x: 1000, y: 1000 }, + }, + { + id: '11', + data: { label: 'Node 5' }, + position: { x: 800, y: 1000 }, + }, + { + id: '12', + data: { label: 'Node 6' }, + position: { x: 1000, y: 800 }, }, ]; -const initialEdges: Edge[] = [ - { id: 'e1-2', source: '1', target: '2', animated: true }, - { id: 'e1-3', source: '1', target: '3' }, -]; +const initialEdges: Edge[] = []; const defaultEdgeOptions = { zIndex: 0 }; diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index e5a7927e..a8cbd404 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useEffect, useRef } from 'react'; +import { memo, MouseEvent, useEffect, useRef } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; -import { select } from 'd3-selection'; +import { select, pointer } from 'd3-selection'; import { useStore, getRectOfNodes, @@ -13,7 +13,6 @@ import { getBoundsOfRects, useStoreApi, useReactFlow, - XYPosition, } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; @@ -38,6 +37,8 @@ const selector = (s: ReactFlowState) => { viewBB, boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, rfId: s.rfId, + width: s.width, + height: s.height, }; }; @@ -49,17 +50,16 @@ function MiniMap({ style, className, nodeStrokeColor = '#555', - nodeColor = '#fff', + nodeColor = '#999', nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth = 2, - maskColor = 'rgb(240, 242, 243, 0.7)', + maskColor = 'rgb(200, 200, 200, 0.9)', position = 'bottom-right', }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); - const initialized = useRef(false); - const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); + const { boundingRect, viewBB, nodes, rfId, width: w, height: h } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; const nodeColorFunc = getAttrFunction(nodeColor); @@ -77,33 +77,27 @@ function MiniMap({ const height = viewHeight + offset * 2; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; - const { setCenter } = useReactFlow(); - const startTransform = useRef({ x: 0, y: 0 }); + const { setCenter, setViewport, project } = useReactFlow(); + const startTransform = useRef<[number, number]>([0, 0]); const viewScaleRef = useRef(0); - const viewScaleWRef = useRef(0); - const viewScaleHRef = useRef(0); - viewScaleRef.current = viewScale; - viewScaleWRef.current = scaledWidth; - viewScaleHRef.current = scaledHeight; + viewScaleRef.current = Math.max(w / elementWidth, h / elementHeight); + + const onClick = (event: MouseEvent) => { + const rfCoord = pointer(event); + + console.log(rfCoord); + }; useEffect(() => { - if (!initialized.current && svg.current) { - const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 }; - + if (svg.current) { const selection = select(svg.current as Element); const zoomHandler = zoom() .on('start', (event: D3ZoomEvent) => { - const { transform } = store.getState(); + const rfCoord = pointer(event); - const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; - const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; - - startTransform.current = { - x: px - transform[0] / transform[2], - y: py - transform[1] / transform[2], - }; + startTransform.current = rfCoord; }) .on('zoom.wheel', (event: D3ZoomEvent) => { const { transform, d3Selection, d3Zoom } = store.getState(); @@ -117,29 +111,26 @@ function MiniMap({ (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * 10; const zoom = transform[2] * Math.pow(2, pinchDelta); - d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]); + + d3Zoom.scaleTo(d3Selection, zoom, startTransform.current); }) .on('zoom', (event: D3ZoomEvent) => { - if (event.sourceEvent.type !== 'mousemove') { + const { transform, d3Selection, d3Zoom } = store.getState(); + if (event.sourceEvent.type !== 'mousemove' || !d3Selection || !d3Zoom) { return; } - const { transform, d3Selection, d3Zoom } = store.getState(); - const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; - const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; - const position = { - x: (px - startTransform.current.x) * transform[2], - y: (py - startTransform.current.y) * transform[2], + x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * transform[2], + y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * transform[2], }; const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); - d3Zoom!.transform(d3Selection!, nextTransform); + d3Zoom.transform(d3Selection, nextTransform); }); selection.call(zoomHandler); - initialized.current = true; } - }, [setCenter]); + }, [setCenter, setViewport, project]); return ( @@ -150,6 +141,7 @@ function MiniMap({ role="img" aria-labelledby={labelledBy} ref={svg} + onClick={onClick} > React Flow mini map {nodes.map((node) => { diff --git a/packages/interactive-minimap/src/MiniMapDrag.tsx b/packages/interactive-minimap/src/MiniMapDrag.tsx new file mode 100644 index 00000000..e5a7927e --- /dev/null +++ b/packages/interactive-minimap/src/MiniMapDrag.tsx @@ -0,0 +1,187 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { memo, useEffect, useRef } from 'react'; +import cc from 'classcat'; +import shallow from 'zustand/shallow'; +import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; +import { select } from 'd3-selection'; +import { + useStore, + getRectOfNodes, + ReactFlowState, + Rect, + Panel, + getBoundsOfRects, + useStoreApi, + useReactFlow, + XYPosition, +} from '@reactflow/core'; + +import MiniMapNode from './MiniMapNode'; +import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; + +declare const window: any; + +const defaultWidth = 200; +const defaultHeight = 150; + +const selector = (s: ReactFlowState) => { + const nodes = Array.from(s.nodeInternals.values()); + const viewBB: Rect = { + x: -s.transform[0] / s.transform[2], + y: -s.transform[1] / s.transform[2], + width: s.width / s.transform[2], + height: s.height / s.transform[2], + }; + + return { + nodes: nodes.filter((node) => !node.hidden && node.width && node.height), + viewBB, + boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, + rfId: s.rfId, + }; +}; + +const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); + +const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; + +function MiniMap({ + style, + className, + nodeStrokeColor = '#555', + nodeColor = '#fff', + nodeClassName = '', + nodeBorderRadius = 5, + nodeStrokeWidth = 2, + maskColor = 'rgb(240, 242, 243, 0.7)', + position = 'bottom-right', +}: MiniMapProps) { + const store = useStoreApi(); + const svg = useRef(null); + const initialized = useRef(false); + const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); + const elementWidth = (style?.width as number) ?? defaultWidth; + const elementHeight = (style?.height as number) ?? defaultHeight; + const nodeColorFunc = getAttrFunction(nodeColor); + const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); + const nodeClassNameFunc = getAttrFunction(nodeClassName); + const scaledWidth = boundingRect.width / elementWidth; + const scaledHeight = boundingRect.height / elementHeight; + const viewScale = Math.max(scaledWidth, scaledHeight); + const viewWidth = viewScale * elementWidth; + const viewHeight = viewScale * elementHeight; + const offset = 5 * viewScale; + const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; + const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; + const width = viewWidth + offset * 2; + const height = viewHeight + offset * 2; + const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; + const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; + const { setCenter } = useReactFlow(); + const startTransform = useRef({ x: 0, y: 0 }); + const viewScaleRef = useRef(0); + const viewScaleWRef = useRef(0); + const viewScaleHRef = useRef(0); + + viewScaleRef.current = viewScale; + viewScaleWRef.current = scaledWidth; + viewScaleHRef.current = scaledHeight; + + useEffect(() => { + if (!initialized.current && svg.current) { + const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 }; + + const selection = select(svg.current as Element); + + const zoomHandler = zoom() + .on('start', (event: D3ZoomEvent) => { + const { transform } = store.getState(); + + const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; + const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; + + startTransform.current = { + x: px - transform[0] / transform[2], + y: py - transform[1] / transform[2], + }; + }) + .on('zoom.wheel', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { + return; + } + + const pinchDelta = + -event.sourceEvent.deltaY * + (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * + 10; + const zoom = transform[2] * Math.pow(2, pinchDelta); + d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]); + }) + .on('zoom', (event: D3ZoomEvent) => { + if (event.sourceEvent.type !== 'mousemove') { + return; + } + + const { transform, d3Selection, d3Zoom } = store.getState(); + const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; + const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; + + const position = { + x: (px - startTransform.current.x) * transform[2], + y: (py - startTransform.current.y) * transform[2], + }; + + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + d3Zoom!.transform(d3Selection!, nextTransform); + }); + selection.call(zoomHandler); + initialized.current = true; + } + }, [setCenter]); + + return ( + + + React Flow mini map + {nodes.map((node) => { + return ( + + ); + })} + + + + ); +} + +MiniMap.displayName = 'MiniMap'; + +export default memo(MiniMap); From b4658d467686305d5179f2e26943f5c90d0b5c1f Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 16:10:23 +0100 Subject: [PATCH 06/12] chore(interactive-minimap): cleanup --- .../src/examples/InteractiveMinimap/index.tsx | 9 ++++- packages/interactive-minimap/src/MiniMap.tsx | 39 +++++++------------ packages/interactive-minimap/src/types.ts | 9 +++-- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index cde9b722..fa66d209 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -1,4 +1,4 @@ -import { MouseEvent } from 'react'; +import { MouseEvent, useCallback } from 'react'; import ReactFlow, { Background, BackgroundVariant, @@ -7,6 +7,7 @@ import ReactFlow, { Node, Edge, useReactFlow, + XYPosition, } from 'reactflow'; import { MiniMap } from '@reactflow/interactive-minimap'; @@ -113,6 +114,10 @@ const BasicFlow = () => { ); }; + const onMiniMapClick = useCallback((event: MouseEvent, pos: XYPosition) => { + console.log(pos); + }, []); + return ( { fitView > - +
diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index a8cbd404..d8c8e77e 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -1,19 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, MouseEvent, useEffect, useRef } from 'react'; +import { memo, useEffect, useRef, MouseEvent } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; import { select, pointer } from 'd3-selection'; -import { - useStore, - getRectOfNodes, - ReactFlowState, - Rect, - Panel, - getBoundsOfRects, - useStoreApi, - useReactFlow, -} from '@reactflow/core'; + +import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects, useStoreApi } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; @@ -56,6 +48,7 @@ function MiniMap({ nodeStrokeWidth = 2, maskColor = 'rgb(200, 200, 200, 0.9)', position = 'bottom-right', + onClick, }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); @@ -77,16 +70,13 @@ function MiniMap({ const height = viewHeight + offset * 2; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; - const { setCenter, setViewport, project } = useReactFlow(); - const startTransform = useRef<[number, number]>([0, 0]); const viewScaleRef = useRef(0); viewScaleRef.current = Math.max(w / elementWidth, h / elementHeight); - const onClick = (event: MouseEvent) => { + const onSvgClick = (event: MouseEvent) => { const rfCoord = pointer(event); - - console.log(rfCoord); + onClick?.(event, { x: rfCoord[0], y: rfCoord[1] }); }; useEffect(() => { @@ -94,11 +84,6 @@ function MiniMap({ const selection = select(svg.current as Element); const zoomHandler = zoom() - .on('start', (event: D3ZoomEvent) => { - const rfCoord = pointer(event); - - startTransform.current = rfCoord; - }) .on('zoom.wheel', (event: D3ZoomEvent) => { const { transform, d3Selection, d3Zoom } = store.getState(); @@ -112,25 +97,29 @@ function MiniMap({ 10; const zoom = transform[2] * Math.pow(2, pinchDelta); - d3Zoom.scaleTo(d3Selection, zoom, startTransform.current); + d3Zoom.scaleTo(d3Selection, zoom); }) .on('zoom', (event: D3ZoomEvent) => { const { transform, d3Selection, d3Zoom } = store.getState(); + if (event.sourceEvent.type !== 'mousemove' || !d3Selection || !d3Zoom) { return; } + let nextTransform = null; + const position = { x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * transform[2], y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * transform[2], }; - const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + d3Zoom.transform(d3Selection, nextTransform); }); selection.call(zoomHandler); } - }, [setCenter, setViewport, project]); + }, []); return ( @@ -141,7 +130,7 @@ function MiniMap({ role="img" aria-labelledby={labelledBy} ref={svg} - onClick={onClick} + onClick={onSvgClick} > React Flow mini map {nodes.map((node) => { diff --git a/packages/interactive-minimap/src/types.ts b/packages/interactive-minimap/src/types.ts index 5f82824d..aff2aeab 100644 --- a/packages/interactive-minimap/src/types.ts +++ b/packages/interactive-minimap/src/types.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { HTMLAttributes } from 'react'; -import { Node, PanelPosition } from '@reactflow/core'; +import { HTMLAttributes, MouseEvent } from 'react'; +import { Node, PanelPosition, XYPosition } from '@reactflow/core'; export type GetMiniMapNodeAttribute = (node: Node) => string; -export interface MiniMapProps extends HTMLAttributes { +export type MiniMapProps = Omit, 'onClick'> & { nodeColor?: string | GetMiniMapNodeAttribute; nodeStrokeColor?: string | GetMiniMapNodeAttribute; nodeClassName?: string | GetMiniMapNodeAttribute; @@ -12,4 +12,5 @@ export interface MiniMapProps extends HTMLAttributes void; +}; From d650609cd9042802af2f7eeb51966758996f5fd1 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 17:27:18 +0100 Subject: [PATCH 07/12] chore(interactive-minimap): remove unused file --- packages/interactive-minimap/src/MiniMap.tsx | 12 +- .../interactive-minimap/src/MiniMapDrag.tsx | 187 ------------------ 2 files changed, 4 insertions(+), 195 deletions(-) delete mode 100644 packages/interactive-minimap/src/MiniMapDrag.tsx diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index d8c8e77e..57444d62 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -5,7 +5,7 @@ import shallow from 'zustand/shallow'; import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; import { select, pointer } from 'd3-selection'; -import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects, useStoreApi } from '@reactflow/core'; +import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi, ReactFlowState, Rect } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; @@ -29,8 +29,6 @@ const selector = (s: ReactFlowState) => { viewBB, boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, rfId: s.rfId, - width: s.width, - height: s.height, }; }; @@ -52,7 +50,7 @@ function MiniMap({ }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); - const { boundingRect, viewBB, nodes, rfId, width: w, height: h } = useStore(selector, shallow); + const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; const nodeColorFunc = getAttrFunction(nodeColor); @@ -72,7 +70,7 @@ function MiniMap({ const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; const viewScaleRef = useRef(0); - viewScaleRef.current = Math.max(w / elementWidth, h / elementHeight); + viewScaleRef.current = viewScale; const onSvgClick = (event: MouseEvent) => { const rfCoord = pointer(event); @@ -106,14 +104,12 @@ function MiniMap({ return; } - let nextTransform = null; - const position = { x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * transform[2], y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * transform[2], }; - nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); d3Zoom.transform(d3Selection, nextTransform); }); diff --git a/packages/interactive-minimap/src/MiniMapDrag.tsx b/packages/interactive-minimap/src/MiniMapDrag.tsx deleted file mode 100644 index e5a7927e..00000000 --- a/packages/interactive-minimap/src/MiniMapDrag.tsx +++ /dev/null @@ -1,187 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useEffect, useRef } from 'react'; -import cc from 'classcat'; -import shallow from 'zustand/shallow'; -import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; -import { select } from 'd3-selection'; -import { - useStore, - getRectOfNodes, - ReactFlowState, - Rect, - Panel, - getBoundsOfRects, - useStoreApi, - useReactFlow, - XYPosition, -} from '@reactflow/core'; - -import MiniMapNode from './MiniMapNode'; -import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; - -declare const window: any; - -const defaultWidth = 200; -const defaultHeight = 150; - -const selector = (s: ReactFlowState) => { - const nodes = Array.from(s.nodeInternals.values()); - const viewBB: Rect = { - x: -s.transform[0] / s.transform[2], - y: -s.transform[1] / s.transform[2], - width: s.width / s.transform[2], - height: s.height / s.transform[2], - }; - - return { - nodes: nodes.filter((node) => !node.hidden && node.width && node.height), - viewBB, - boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, - rfId: s.rfId, - }; -}; - -const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); - -const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; - -function MiniMap({ - style, - className, - nodeStrokeColor = '#555', - nodeColor = '#fff', - nodeClassName = '', - nodeBorderRadius = 5, - nodeStrokeWidth = 2, - maskColor = 'rgb(240, 242, 243, 0.7)', - position = 'bottom-right', -}: MiniMapProps) { - const store = useStoreApi(); - const svg = useRef(null); - const initialized = useRef(false); - const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); - const elementWidth = (style?.width as number) ?? defaultWidth; - const elementHeight = (style?.height as number) ?? defaultHeight; - const nodeColorFunc = getAttrFunction(nodeColor); - const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); - const nodeClassNameFunc = getAttrFunction(nodeClassName); - const scaledWidth = boundingRect.width / elementWidth; - const scaledHeight = boundingRect.height / elementHeight; - const viewScale = Math.max(scaledWidth, scaledHeight); - const viewWidth = viewScale * elementWidth; - const viewHeight = viewScale * elementHeight; - const offset = 5 * viewScale; - const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; - const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; - const width = viewWidth + offset * 2; - const height = viewHeight + offset * 2; - const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; - const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; - const { setCenter } = useReactFlow(); - const startTransform = useRef({ x: 0, y: 0 }); - const viewScaleRef = useRef(0); - const viewScaleWRef = useRef(0); - const viewScaleHRef = useRef(0); - - viewScaleRef.current = viewScale; - viewScaleWRef.current = scaledWidth; - viewScaleHRef.current = scaledHeight; - - useEffect(() => { - if (!initialized.current && svg.current) { - const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 }; - - const selection = select(svg.current as Element); - - const zoomHandler = zoom() - .on('start', (event: D3ZoomEvent) => { - const { transform } = store.getState(); - - const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; - const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; - - startTransform.current = { - x: px - transform[0] / transform[2], - y: py - transform[1] / transform[2], - }; - }) - .on('zoom.wheel', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); - - if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { - return; - } - - const pinchDelta = - -event.sourceEvent.deltaY * - (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * - 10; - const zoom = transform[2] * Math.pow(2, pinchDelta); - d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]); - }) - .on('zoom', (event: D3ZoomEvent) => { - if (event.sourceEvent.type !== 'mousemove') { - return; - } - - const { transform, d3Selection, d3Zoom } = store.getState(); - const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current; - const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current; - - const position = { - x: (px - startTransform.current.x) * transform[2], - y: (py - startTransform.current.y) * transform[2], - }; - - const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); - d3Zoom!.transform(d3Selection!, nextTransform); - }); - selection.call(zoomHandler); - initialized.current = true; - } - }, [setCenter]); - - return ( - - - React Flow mini map - {nodes.map((node) => { - return ( - - ); - })} - - - - ); -} - -MiniMap.displayName = 'MiniMap'; - -export default memo(MiniMap); From 0f855308ccb6700b64b8855091cd3a8867f2677b Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 18:04:29 +0100 Subject: [PATCH 08/12] feat(interactive-minimap): add node click handler --- .../src/examples/InteractiveMinimap/index.tsx | 6 +++++- packages/interactive-minimap/src/MiniMap.tsx | 14 ++++++++++++++ packages/interactive-minimap/src/MiniMapNode.tsx | 7 ++++++- packages/interactive-minimap/src/types.ts | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index fa66d209..db8d754f 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -118,6 +118,10 @@ const BasicFlow = () => { console.log(pos); }, []); + const onMiniMapNodeClick = useCallback((event: MouseEvent, node: Node) => { + console.log(node); + }, []); + return ( { fitView > - +
diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index 57444d62..46b0ec9f 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -47,6 +47,7 @@ function MiniMap({ maskColor = 'rgb(200, 200, 200, 0.9)', position = 'bottom-right', onClick, + onNodeClick, }: MiniMapProps) { const store = useStoreApi(); const svg = useRef(null); @@ -114,9 +115,20 @@ function MiniMap({ d3Zoom.transform(d3Selection, nextTransform); }); selection.call(zoomHandler); + + return () => { + selection.on('.zoom', null); + }; } }, []); + const onSvgNodeClick = onNodeClick + ? (event: MouseEvent, nodeId: string) => { + const node = store.getState().nodeInternals.get(nodeId)!; + onNodeClick(event, node); + } + : undefined; + return ( ); })} diff --git a/packages/interactive-minimap/src/MiniMapNode.tsx b/packages/interactive-minimap/src/MiniMapNode.tsx index 3ea3bd33..15a5ba3f 100644 --- a/packages/interactive-minimap/src/MiniMapNode.tsx +++ b/packages/interactive-minimap/src/MiniMapNode.tsx @@ -1,7 +1,8 @@ -import { memo, CSSProperties } from 'react'; +import { memo, CSSProperties, MouseEvent } from 'react'; import cc from 'classcat'; interface MiniMapNodeProps { + id: string; x: number; y: number; width: number; @@ -13,9 +14,11 @@ interface MiniMapNodeProps { strokeColor: string; strokeWidth: number; style?: CSSProperties; + onClick?: (event: MouseEvent, id: string) => void; } const MiniMapNode = ({ + id, x, y, width, @@ -27,6 +30,7 @@ const MiniMapNode = ({ className, borderRadius, shapeRendering, + onClick, }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; @@ -44,6 +48,7 @@ const MiniMapNode = ({ stroke={strokeColor} strokeWidth={strokeWidth} shapeRendering={shapeRendering} + onClick={onClick ? (event) => onClick(event, id) : undefined} /> ); }; diff --git a/packages/interactive-minimap/src/types.ts b/packages/interactive-minimap/src/types.ts index aff2aeab..80534fc3 100644 --- a/packages/interactive-minimap/src/types.ts +++ b/packages/interactive-minimap/src/types.ts @@ -13,4 +13,5 @@ export type MiniMapProps = Omit, ' maskColor?: string; position?: PanelPosition; onClick?: (event: MouseEvent, position: XYPosition) => void; + onNodeClick?: (event: MouseEvent, node: Node) => void; }; From a96044b2a2a2005c0deda3f71e79633b5cd3eca8 Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 22:45:08 +0100 Subject: [PATCH 09/12] feat(minimap): add pannable and zoomable props, remove interactive-minimap package --- .../src/examples/InteractiveMinimap/index.tsx | 4 +- .../src/examples/NestedNodes/index.tsx | 2 +- packages/interactive-minimap/.eslintrc.js | 4 - packages/interactive-minimap/CHANGELOG.md | 42 ----- packages/interactive-minimap/README.md | 10 - packages/interactive-minimap/package.json | 69 ------- packages/interactive-minimap/src/MiniMap.tsx | 178 ------------------ .../interactive-minimap/src/MiniMapNode.tsx | 58 ------ packages/interactive-minimap/src/index.tsx | 2 - packages/interactive-minimap/src/style.css | 3 - packages/interactive-minimap/src/types.ts | 17 -- packages/interactive-minimap/tsconfig.json | 6 - packages/minimap/package.json | 4 + packages/minimap/src/MiniMap.tsx | 80 +++++++- packages/minimap/src/MiniMapNode.tsx | 7 +- packages/minimap/src/types.ts | 10 +- pnpm-lock.yaml | 8 + 17 files changed, 106 insertions(+), 398 deletions(-) delete mode 100644 packages/interactive-minimap/.eslintrc.js delete mode 100644 packages/interactive-minimap/CHANGELOG.md delete mode 100644 packages/interactive-minimap/README.md delete mode 100644 packages/interactive-minimap/package.json delete mode 100644 packages/interactive-minimap/src/MiniMap.tsx delete mode 100644 packages/interactive-minimap/src/MiniMapNode.tsx delete mode 100644 packages/interactive-minimap/src/index.tsx delete mode 100644 packages/interactive-minimap/src/style.css delete mode 100644 packages/interactive-minimap/src/types.ts delete mode 100644 packages/interactive-minimap/tsconfig.json diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index db8d754f..18fa854a 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -1,5 +1,6 @@ import { MouseEvent, useCallback } from 'react'; import ReactFlow, { + MiniMap, Background, BackgroundVariant, Controls, @@ -9,7 +10,6 @@ import ReactFlow, { useReactFlow, XYPosition, } from 'reactflow'; -import { MiniMap } from '@reactflow/interactive-minimap'; const onNodeDrag = (_: MouseEvent, node: Node) => console.log('drag', node); const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node); @@ -137,7 +137,7 @@ const BasicFlow = () => { fitView > - +
diff --git a/examples/vite-app/src/examples/NestedNodes/index.tsx b/examples/vite-app/src/examples/NestedNodes/index.tsx index 5e9c80f1..a6c083ce 100644 --- a/examples/vite-app/src/examples/NestedNodes/index.tsx +++ b/examples/vite-app/src/examples/NestedNodes/index.tsx @@ -160,7 +160,7 @@ const NestedFlow = () => { maxZoom={4} onlyRenderVisibleElements={false} > - + diff --git a/packages/interactive-minimap/.eslintrc.js b/packages/interactive-minimap/.eslintrc.js deleted file mode 100644 index 31cfbd11..00000000 --- a/packages/interactive-minimap/.eslintrc.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - root: true, - extends: ['@reactflow/eslint-config'], -}; diff --git a/packages/interactive-minimap/CHANGELOG.md b/packages/interactive-minimap/CHANGELOG.md deleted file mode 100644 index eb44ff49..00000000 --- a/packages/interactive-minimap/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# @reactflow/minimap - -## 11.0.1 - -### Patch Changes - -- Updated dependencies [[`def11008`](https://github.com/wbkd/react-flow/commit/def11008d88749fec40e6fcba8bc41eea2511bab), [`d00faa6b`](https://github.com/wbkd/react-flow/commit/d00faa6b3e77388bfd655d4c02e9a5375bc515e4)]: - - @reactflow/core@11.1.0 - -## 11.0.0 - -### Major Changes - -- **Better [Accessibility](/docs/guides/accessibility)** - - Nodes and edges are focusable, selectable, moveable and deleteable with the keyboard. - - `aria-` default attributes for all elements and controllable via `ariaLabel` options - - Keyboard controls can be disabled with the new `disableKeyboardA11y` prop -- **Better selectable edges** via new edge option: `interactionWidth` - renders invisible edge that makes it easier to interact -- **Better routing for smoothstep and step edges**: https://twitter.com/reactflowdev/status/1567535405284614145 -- **Nicer edge updating behaviour**: https://twitter.com/reactflowdev/status/1564966917517021184 -- **Node origin**: The new `nodeOrigin` prop lets you control the origin of a node. Useful for layouting. -- **New background pattern**: `BackgroundVariant.Cross` variant -- **[`useOnViewportChange`](/docs/api/hooks/use-on-viewport-change) hook** - handle viewport changes within a component -- **[`useOnSelectionChange`](/docs/api/hooks/use-on-selection-change) hook** - handle selection changes within a component -- **[`useNodesInitialized`](/docs/api/hooks/use-nodes-initialized) hook** - returns true if all nodes are initialized and if there is more than one node -- **Deletable option** for Nodes and edges -- **New Event handlers**: `onPaneMouseEnter`, `onPaneMouseMove` and `onPaneMouseLeave` -- **Edge `pathOptions`** for `smoothstep` and `default` edges -- **Nicer cursor defaults**: Cursor is grabbing, while dragging a node or panning -- **Pane moveable** with middle mouse button -- **Pan over nodes** when they are not draggable (`draggable=false` or `nodesDraggable` false) -- **[``](/docs/api/edges/base-edge) component** that makes it easier to build custom edges -- **[Separately installable packages](/docs/overview/packages/)** - - @reactflow/core - - @reactflow/background - - @reactflow/controls - - @reactflow/minimap - -### Patch Changes - -- Updated dependencies: - - @reactflow/core@11.0.0 diff --git a/packages/interactive-minimap/README.md b/packages/interactive-minimap/README.md deleted file mode 100644 index 2dba17d9..00000000 --- a/packages/interactive-minimap/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# @reactflow/minimap - -Mini map component for React Flow. - -## Installation - -```sh -npm install @reactflow/minimap -``` - diff --git a/packages/interactive-minimap/package.json b/packages/interactive-minimap/package.json deleted file mode 100644 index 8dc4a82a..00000000 --- a/packages/interactive-minimap/package.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@reactflow/interactive-minimap", - "version": "0.0.1-alpha.0", - "description": "Interactive Minimap component for React Flow.", - "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": false, - "publishConfig": { - "access": "public" - }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/wbkd/react-flow.git", - "directory": "packages/interactive-minimap" - }, - "scripts": { - "dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch", - "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" - }, - "dependencies": { - "@babel/runtime": "^7.18.9", - "@reactflow/core": "workspace:*", - "@types/d3-selection": "^3.0.3", - "@types/d3-zoom": "^3.0.1", - "classcat": "^5.0.3", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0", - "zustand": "^4.1.1" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - }, - "devDependencies": { - "@reactflow/eslint-config": "workspace:^0.0.0", - "@reactflow/rollup-config": "workspace:*", - "@reactflow/tsconfig": "workspace:*", - "@types/node": "^18.7.16", - "@types/react": "^18.0.19", - "react": "^18.2.0", - "typescript": "^4.8.3" - }, - "rollup": { - "globals": { - "zustand": "Zustand", - "zustand/shallow": "zustandShallow", - "classcat": "cc" - }, - "name": "ReactFlowMinimap" - } -} diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx deleted file mode 100644 index 46b0ec9f..00000000 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo, useEffect, useRef, MouseEvent } from 'react'; -import cc from 'classcat'; -import shallow from 'zustand/shallow'; -import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom'; -import { select, pointer } from 'd3-selection'; - -import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi, ReactFlowState, Rect } from '@reactflow/core'; - -import MiniMapNode from './MiniMapNode'; -import { MiniMapProps, GetMiniMapNodeAttribute } from './types'; - -declare const window: any; - -const defaultWidth = 200; -const defaultHeight = 150; - -const selector = (s: ReactFlowState) => { - const nodes = Array.from(s.nodeInternals.values()); - const viewBB: Rect = { - x: -s.transform[0] / s.transform[2], - y: -s.transform[1] / s.transform[2], - width: s.width / s.transform[2], - height: s.height / s.transform[2], - }; - - return { - nodes: nodes.filter((node) => !node.hidden && node.width && node.height), - viewBB, - boundingRect: nodes.length > 0 ? getBoundsOfRects(getRectOfNodes(nodes), viewBB) : viewBB, - rfId: s.rfId, - }; -}; - -const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func); - -const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; - -function MiniMap({ - style, - className, - nodeStrokeColor = '#555', - nodeColor = '#999', - nodeClassName = '', - nodeBorderRadius = 5, - nodeStrokeWidth = 2, - maskColor = 'rgb(200, 200, 200, 0.9)', - position = 'bottom-right', - onClick, - onNodeClick, -}: MiniMapProps) { - const store = useStoreApi(); - const svg = useRef(null); - const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); - const elementWidth = (style?.width as number) ?? defaultWidth; - const elementHeight = (style?.height as number) ?? defaultHeight; - const nodeColorFunc = getAttrFunction(nodeColor); - const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); - const nodeClassNameFunc = getAttrFunction(nodeClassName); - const scaledWidth = boundingRect.width / elementWidth; - const scaledHeight = boundingRect.height / elementHeight; - const viewScale = Math.max(scaledWidth, scaledHeight); - const viewWidth = viewScale * elementWidth; - const viewHeight = viewScale * elementHeight; - const offset = 5 * viewScale; - const x = boundingRect.x - (viewWidth - boundingRect.width) / 2 - offset; - const y = boundingRect.y - (viewHeight - boundingRect.height) / 2 - offset; - const width = viewWidth + offset * 2; - const height = viewHeight + offset * 2; - const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; - const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; - const viewScaleRef = useRef(0); - - viewScaleRef.current = viewScale; - - const onSvgClick = (event: MouseEvent) => { - const rfCoord = pointer(event); - onClick?.(event, { x: rfCoord[0], y: rfCoord[1] }); - }; - - useEffect(() => { - if (svg.current) { - const selection = select(svg.current as Element); - - const zoomHandler = zoom() - .on('zoom.wheel', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); - - if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { - return; - } - - const pinchDelta = - -event.sourceEvent.deltaY * - (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * - 10; - const zoom = transform[2] * Math.pow(2, pinchDelta); - - d3Zoom.scaleTo(d3Selection, zoom); - }) - .on('zoom', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); - - if (event.sourceEvent.type !== 'mousemove' || !d3Selection || !d3Zoom) { - return; - } - - const position = { - x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * transform[2], - y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * transform[2], - }; - - const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); - - d3Zoom.transform(d3Selection, nextTransform); - }); - selection.call(zoomHandler); - - return () => { - selection.on('.zoom', null); - }; - } - }, []); - - const onSvgNodeClick = onNodeClick - ? (event: MouseEvent, nodeId: string) => { - const node = store.getState().nodeInternals.get(nodeId)!; - onNodeClick(event, node); - } - : undefined; - - return ( - - - React Flow mini map - {nodes.map((node) => { - return ( - - ); - })} - - - - ); -} - -MiniMap.displayName = 'MiniMap'; - -export default memo(MiniMap); diff --git a/packages/interactive-minimap/src/MiniMapNode.tsx b/packages/interactive-minimap/src/MiniMapNode.tsx deleted file mode 100644 index 15a5ba3f..00000000 --- a/packages/interactive-minimap/src/MiniMapNode.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { memo, CSSProperties, MouseEvent } from 'react'; -import cc from 'classcat'; - -interface MiniMapNodeProps { - id: string; - x: number; - y: number; - width: number; - height: number; - borderRadius: number; - className: string; - color: string; - shapeRendering: string; - strokeColor: string; - strokeWidth: number; - style?: CSSProperties; - onClick?: (event: MouseEvent, id: string) => void; -} - -const MiniMapNode = ({ - id, - x, - y, - width, - height, - style, - color, - strokeColor, - strokeWidth, - className, - borderRadius, - shapeRendering, - onClick, -}: MiniMapNodeProps) => { - const { background, backgroundColor } = style || {}; - const fill = (color || background || backgroundColor) as string; - - return ( - onClick(event, id) : undefined} - /> - ); -}; - -MiniMapNode.displayName = 'MiniMapNode'; - -export default memo(MiniMapNode); diff --git a/packages/interactive-minimap/src/index.tsx b/packages/interactive-minimap/src/index.tsx deleted file mode 100644 index 7b91e95d..00000000 --- a/packages/interactive-minimap/src/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export { default as MiniMap } from './MiniMap'; -export * from './types'; diff --git a/packages/interactive-minimap/src/style.css b/packages/interactive-minimap/src/style.css deleted file mode 100644 index dbb9a931..00000000 --- a/packages/interactive-minimap/src/style.css +++ /dev/null @@ -1,3 +0,0 @@ -.react-flow__minimap { - background-color: #fff; -} diff --git a/packages/interactive-minimap/src/types.ts b/packages/interactive-minimap/src/types.ts deleted file mode 100644 index 80534fc3..00000000 --- a/packages/interactive-minimap/src/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { HTMLAttributes, MouseEvent } from 'react'; -import { Node, PanelPosition, XYPosition } from '@reactflow/core'; - -export type GetMiniMapNodeAttribute = (node: Node) => string; - -export type MiniMapProps = Omit, 'onClick'> & { - nodeColor?: string | GetMiniMapNodeAttribute; - nodeStrokeColor?: string | GetMiniMapNodeAttribute; - nodeClassName?: string | GetMiniMapNodeAttribute; - nodeBorderRadius?: number; - nodeStrokeWidth?: number; - maskColor?: string; - position?: PanelPosition; - onClick?: (event: MouseEvent, position: XYPosition) => void; - onNodeClick?: (event: MouseEvent, node: Node) => void; -}; diff --git a/packages/interactive-minimap/tsconfig.json b/packages/interactive-minimap/tsconfig.json deleted file mode 100644 index 8a8e74ce..00000000 --- a/packages/interactive-minimap/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "@reactflow/tsconfig/react.json", - "display": "@reactflow/minimap", - "include": ["**/*.ts", "**/*.tsx"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/minimap/package.json b/packages/minimap/package.json index 527aab38..100a12b0 100644 --- a/packages/minimap/package.json +++ b/packages/minimap/package.json @@ -40,7 +40,11 @@ "dependencies": { "@babel/runtime": "^7.18.9", "@reactflow/core": "workspace:*", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", "zustand": "^4.1.1" }, "peerDependencies": { diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 05cd8771..4fd639da 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -1,8 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { memo } from 'react'; +import { memo, useEffect, useRef } from 'react'; +import type { MouseEvent } from 'react'; import cc from 'classcat'; import shallow from 'zustand/shallow'; -import { useStore, getRectOfNodes, getBoundsOfRects, Panel } from '@reactflow/core'; +import { zoom, zoomIdentity } from 'd3-zoom'; +import type { D3ZoomEvent } from 'd3-zoom'; +import { select, pointer } from 'd3-selection'; +import { useStore, getRectOfNodes, Panel, getBoundsOfRects, useStoreApi } from '@reactflow/core'; import type { ReactFlowState, Rect } from '@reactflow/core'; import MiniMapNode from './MiniMapNode'; @@ -44,7 +48,13 @@ function MiniMap({ nodeStrokeWidth = 2, maskColor = 'rgb(240, 242, 243, 0.7)', position = 'bottom-right', + onClick, + onNodeClick, + pannable = false, + zoomable = false, }: MiniMapProps) { + const store = useStoreApi(); + const svg = useRef(null); const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow); const elementWidth = (style?.width as number) ?? defaultWidth; const elementHeight = (style?.height as number) ?? defaultHeight; @@ -63,6 +73,68 @@ function MiniMap({ const height = viewHeight + offset * 2; const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'; const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`; + const viewScaleRef = useRef(0); + + viewScaleRef.current = viewScale; + + useEffect(() => { + if (svg.current) { + const selection = select(svg.current as Element); + + const zoomHandler = zoom() + .on('zoom.wheel', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'wheel' || !zoomable || !d3Selection || !d3Zoom) { + return; + } + + const pinchDelta = + -event.sourceEvent.deltaY * + (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * + 10; + const zoom = transform[2] * Math.pow(2, pinchDelta); + + d3Zoom.scaleTo(d3Selection, zoom); + }) + .on('zoom', (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); + + if (event.sourceEvent.type !== 'mousemove' || !pannable || !d3Selection || !d3Zoom) { + return; + } + + // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround. + const position = { + x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * Math.max(1, transform[2]), + y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * Math.max(1, transform[2]), + }; + + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + + d3Zoom.transform(d3Selection, nextTransform); + }); + selection.call(zoomHandler); + + return () => { + selection.on('.zoom', null); + }; + } + }, [pannable, zoomable]); + + const onSvgClick = onClick + ? (event: MouseEvent) => { + const rfCoord = pointer(event); + onClick(event, { x: rfCoord[0], y: rfCoord[1] }); + } + : undefined; + + const onSvgNodeClick = onNodeClick + ? (event: MouseEvent, nodeId: string) => { + const node = store.getState().nodeInternals.get(nodeId)!; + onNodeClick(event, node); + } + : undefined; return ( @@ -72,6 +144,8 @@ function MiniMap({ viewBox={`${x} ${y} ${width} ${height}`} role="img" aria-labelledby={labelledBy} + ref={svg} + onClick={onSvgClick} > React Flow mini map {nodes.map((node) => { @@ -89,6 +163,8 @@ function MiniMap({ strokeColor={nodeStrokeColorFunc(node)} strokeWidth={nodeStrokeWidth} shapeRendering={shapeRendering} + onClick={onSvgNodeClick} + id={node.id} /> ); })} diff --git a/packages/minimap/src/MiniMapNode.tsx b/packages/minimap/src/MiniMapNode.tsx index 111c7ee0..ae000d02 100644 --- a/packages/minimap/src/MiniMapNode.tsx +++ b/packages/minimap/src/MiniMapNode.tsx @@ -1,8 +1,9 @@ import { memo } from 'react'; -import type { CSSProperties } from 'react'; +import type { CSSProperties, MouseEvent } from 'react'; import cc from 'classcat'; interface MiniMapNodeProps { + id: string; x: number; y: number; width: number; @@ -14,9 +15,11 @@ interface MiniMapNodeProps { strokeColor: string; strokeWidth: number; style?: CSSProperties; + onClick?: (event: MouseEvent, id: string) => void; } const MiniMapNode = ({ + id, x, y, width, @@ -28,6 +31,7 @@ const MiniMapNode = ({ className, borderRadius, shapeRendering, + onClick, }: MiniMapNodeProps) => { const { background, backgroundColor } = style || {}; const fill = (color || background || backgroundColor) as string; @@ -45,6 +49,7 @@ const MiniMapNode = ({ stroke={strokeColor} strokeWidth={strokeWidth} shapeRendering={shapeRendering} + onClick={onClick ? (event) => onClick(event, id) : undefined} /> ); }; diff --git a/packages/minimap/src/types.ts b/packages/minimap/src/types.ts index 4fed29ba..63d8c6d9 100644 --- a/packages/minimap/src/types.ts +++ b/packages/minimap/src/types.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { HTMLAttributes } from 'react'; -import type { Node, PanelPosition } from '@reactflow/core'; +import type { HTMLAttributes, MouseEvent } from 'react'; +import type { Node, PanelPosition, XYPosition } from '@reactflow/core'; export type GetMiniMapNodeAttribute = (node: Node) => string; -export type MiniMapProps = HTMLAttributes & { +export type MiniMapProps = Omit, 'onClick'> & { nodeColor?: string | GetMiniMapNodeAttribute; nodeStrokeColor?: string | GetMiniMapNodeAttribute; nodeClassName?: string | GetMiniMapNodeAttribute; @@ -12,4 +12,8 @@ export type MiniMapProps = HTMLAttributes & { nodeStrokeWidth?: number; maskColor?: string; position?: PanelPosition; + onClick?: (event: MouseEvent, position: XYPosition) => void; + onNodeClick?: (event: MouseEvent, node: Node) => void; + pannable?: boolean; + zoomable?: boolean; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91f51a8d..56aff557 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,16 +222,24 @@ importers: '@reactflow/eslint-config': workspace:^0.0.0 '@reactflow/rollup-config': workspace:* '@reactflow/tsconfig': workspace:* + '@types/d3-selection': ^3.0.3 + '@types/d3-zoom': ^3.0.1 '@types/node': ^18.7.16 '@types/react': ^18.0.19 classcat: ^5.0.3 + d3-selection: ^3.0.0 + d3-zoom: ^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 + '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 + '@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1 classcat: registry.npmjs.org/classcat/5.0.4 + d3-selection: registry.npmjs.org/d3-selection/3.0.0 + d3-zoom: registry.npmjs.org/d3-zoom/3.0.0 zustand: registry.npmjs.org/zustand/4.1.1_react@18.2.0 devDependencies: '@reactflow/eslint-config': link:../../tooling/eslint-config From e85e29343c1ddd4ce3dc2bf6b2e190bdb5847eed Mon Sep 17 00:00:00 2001 From: moklick Date: Tue, 1 Nov 2022 23:28:21 +0100 Subject: [PATCH 10/12] chore(examples): remove interactive minimap dep --- examples/vite-app/package.json | 3 +- pnpm-lock.yaml | 186 ++++++++++++--------------------- 2 files changed, 70 insertions(+), 119 deletions(-) diff --git a/examples/vite-app/package.json b/examples/vite-app/package.json index 27ace109..23bbc7fc 100644 --- a/examples/vite-app/package.json +++ b/examples/vite-app/package.json @@ -19,8 +19,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.3.0", - "reactflow": "workspace:*", - "@reactflow/interactive-minimap": "workspace:*" + "reactflow": "workspace:*" }, "devDependencies": { "@cypress/skip-test": "^2.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56aff557..7220fd5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,15 +32,15 @@ importers: '@changesets/changelog-github': registry.npmjs.org/@changesets/changelog-github/0.4.7 '@changesets/cli': registry.npmjs.org/@changesets/cli/2.25.0 '@preconstruct/cli': registry.npmjs.org/@preconstruct/cli/2.2.1 - '@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0_jdrczqv3ll7udvbunca43w7rz4 - '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/eslint-plugin': registry.npmjs.org/@typescript-eslint/eslint-plugin/5.42.0_dcn2ddfkdgyby36a3kmqplwxkq + '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.42.0_irgkl5vooow2ydyo6aokmferha autoprefixer: registry.npmjs.org/autoprefixer/10.4.9_postcss@8.4.16 concurrently: registry.npmjs.org/concurrently/7.4.0 cypress: registry.npmjs.org/cypress/10.7.0 eslint: registry.npmjs.org/eslint/8.23.1 eslint-config-prettier: registry.npmjs.org/eslint-config-prettier/8.5.0_eslint@8.23.1 eslint-plugin-prettier: registry.npmjs.org/eslint-plugin-prettier/4.2.1_cabrci5exjdaojcvd6xoxgeowu - eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.31.9_eslint@8.23.1 + eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.31.10_eslint@8.23.1 postcss: registry.npmjs.org/postcss/8.4.16 postcss-cli: registry.npmjs.org/postcss-cli/10.0.0_postcss@8.4.16 postcss-combine-duplicated-selectors: registry.npmjs.org/postcss-combine-duplicated-selectors/10.0.3_postcss@8.4.16 @@ -57,7 +57,6 @@ importers: examples/vite-app: specifiers: '@cypress/skip-test': ^2.6.1 - '@reactflow/interactive-minimap': workspace:* '@types/react': ^18.0.17 '@types/react-dom': ^18.0.6 '@vitejs/plugin-react': ^2.1.0 @@ -74,7 +73,6 @@ importers: typescript: ^4.8.3 vite: ^3.1.0 dependencies: - '@reactflow/interactive-minimap': link:../../packages/interactive-minimap classcat: registry.npmjs.org/classcat/5.0.4 dagre: registry.npmjs.org/dagre/0.8.5 localforage: registry.npmjs.org/localforage/1.10.0 @@ -180,41 +178,6 @@ importers: react: registry.npmjs.org/react/18.2.0 typescript: registry.npmjs.org/typescript/4.8.3 - packages/interactive-minimap: - specifiers: - '@babel/runtime': ^7.18.9 - '@reactflow/core': workspace:* - '@reactflow/eslint-config': workspace:^0.0.0 - '@reactflow/rollup-config': workspace:* - '@reactflow/tsconfig': workspace:* - '@types/d3-selection': ^3.0.3 - '@types/d3-zoom': ^3.0.1 - '@types/node': ^18.7.16 - '@types/react': ^18.0.19 - classcat: ^5.0.3 - d3-selection: ^3.0.0 - d3-zoom: ^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 - '@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3 - '@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1 - classcat: registry.npmjs.org/classcat/5.0.4 - d3-selection: registry.npmjs.org/d3-selection/3.0.0 - d3-zoom: registry.npmjs.org/d3-zoom/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/node': registry.npmjs.org/@types/node/18.7.16 - '@types/react': registry.npmjs.org/@types/react/18.0.19 - react: registry.npmjs.org/react/18.2.0 - typescript: registry.npmjs.org/typescript/4.8.3 - packages/minimap: specifiers: '@babel/runtime': ^7.18.9 @@ -287,7 +250,7 @@ importers: eslint: registry.npmjs.org/eslint/8.23.1 eslint-config-prettier: registry.npmjs.org/eslint-config-prettier/8.5.0_eslint@8.23.1 eslint-config-turbo: registry.npmjs.org/eslint-config-turbo/0.0.4_eslint@8.23.1 - eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.31.8_eslint@8.23.1 + eslint-plugin-react: registry.npmjs.org/eslint-plugin-react/7.31.10_eslint@8.23.1 tooling/rollup-config: specifiers: @@ -1727,6 +1690,12 @@ packages: version: 6.2.3 dev: true + registry.npmjs.org/@types/semver/7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz} + name: '@types/semver' + version: 7.3.13 + dev: true + registry.npmjs.org/@types/sinonjs__fake-timers/8.1.1: resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz} name: '@types/sinonjs__fake-timers' @@ -1749,11 +1718,11 @@ packages: dev: true optional: true - registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0_jdrczqv3ll7udvbunca43w7rz4: - resolution: {integrity: sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.39.0.tgz} - id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.39.0 + registry.npmjs.org/@typescript-eslint/eslint-plugin/5.42.0_dcn2ddfkdgyby36a3kmqplwxkq: + resolution: {integrity: sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz} + id: registry.npmjs.org/@typescript-eslint/eslint-plugin/5.42.0 name: '@typescript-eslint/eslint-plugin' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: '@typescript-eslint/parser': ^5.0.0 @@ -1763,13 +1732,14 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 - '@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.39.0_irgkl5vooow2ydyo6aokmferha - '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/parser': registry.npmjs.org/@typescript-eslint/parser/5.42.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.42.0 + '@typescript-eslint/type-utils': registry.npmjs.org/@typescript-eslint/type-utils/5.42.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.42.0_irgkl5vooow2ydyo6aokmferha debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 ignore: registry.npmjs.org/ignore/5.2.0 + natural-compare-lite: registry.npmjs.org/natural-compare-lite/1.4.0 regexpp: registry.npmjs.org/regexpp/3.2.0 semver: registry.npmjs.org/semver/7.3.8 tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.8.3 @@ -1778,11 +1748,11 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/parser/5.39.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.39.0.tgz} - id: registry.npmjs.org/@typescript-eslint/parser/5.39.0 + registry.npmjs.org/@typescript-eslint/parser/5.42.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz} + id: registry.npmjs.org/@typescript-eslint/parser/5.42.0 name: '@typescript-eslint/parser' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -1791,9 +1761,9 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.42.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.42.0 + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.42.0_typescript@4.8.3 debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 typescript: registry.npmjs.org/typescript/4.8.3 @@ -1801,21 +1771,21 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0: - resolution: {integrity: sha512-/I13vAqmG3dyqMVSZPjsbuNQlYS082Y7OMkwhCfLXYsmlI0ca4nkL7wJ/4gjX70LD4P8Hnw1JywUVVAwepURBw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.39.0.tgz} + registry.npmjs.org/@typescript-eslint/scope-manager/5.42.0: + resolution: {integrity: sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz} name: '@typescript-eslint/scope-manager' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 - '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.42.0 + '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.42.0 dev: true - registry.npmjs.org/@typescript-eslint/type-utils/5.39.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.39.0.tgz} - id: registry.npmjs.org/@typescript-eslint/type-utils/5.39.0 + registry.npmjs.org/@typescript-eslint/type-utils/5.42.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz} + id: registry.npmjs.org/@typescript-eslint/type-utils/5.42.0 name: '@typescript-eslint/type-utils' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: '*' @@ -1824,8 +1794,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 - '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.42.0_typescript@4.8.3 + '@typescript-eslint/utils': registry.npmjs.org/@typescript-eslint/utils/5.42.0_irgkl5vooow2ydyo6aokmferha debug: registry.npmjs.org/debug/4.3.4 eslint: registry.npmjs.org/eslint/8.23.1 tsutils: registry.npmjs.org/tsutils/3.21.0_typescript@4.8.3 @@ -1834,18 +1804,18 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/types/5.39.0: - resolution: {integrity: sha512-gQMZrnfEBFXK38hYqt8Lkwt8f4U6yq+2H5VDSgP/qiTzC8Nw8JO3OuSUOQ2qW37S/dlwdkHDntkZM6SQhKyPhw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.39.0.tgz} + registry.npmjs.org/@typescript-eslint/types/5.42.0: + resolution: {integrity: sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz} name: '@typescript-eslint/types' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3: - resolution: {integrity: sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.39.0.tgz} - id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0 + registry.npmjs.org/@typescript-eslint/typescript-estree/5.42.0_typescript@4.8.3: + resolution: {integrity: sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz} + id: registry.npmjs.org/@typescript-eslint/typescript-estree/5.42.0 name: '@typescript-eslint/typescript-estree' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: typescript: '*' @@ -1853,8 +1823,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 - '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.42.0 + '@typescript-eslint/visitor-keys': registry.npmjs.org/@typescript-eslint/visitor-keys/5.42.0 debug: registry.npmjs.org/debug/4.3.4 globby: registry.npmjs.org/globby/11.1.0 is-glob: registry.npmjs.org/is-glob/4.0.3 @@ -1865,34 +1835,36 @@ packages: - supports-color dev: true - registry.npmjs.org/@typescript-eslint/utils/5.39.0_irgkl5vooow2ydyo6aokmferha: - resolution: {integrity: sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.39.0.tgz} - id: registry.npmjs.org/@typescript-eslint/utils/5.39.0 + registry.npmjs.org/@typescript-eslint/utils/5.42.0_irgkl5vooow2ydyo6aokmferha: + resolution: {integrity: sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz} + id: registry.npmjs.org/@typescript-eslint/utils/5.42.0 name: '@typescript-eslint/utils' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: '@types/json-schema': registry.npmjs.org/@types/json-schema/7.0.11 - '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.39.0 - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 - '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.39.0_typescript@4.8.3 + '@types/semver': registry.npmjs.org/@types/semver/7.3.13 + '@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager/5.42.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.42.0 + '@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree/5.42.0_typescript@4.8.3 eslint: registry.npmjs.org/eslint/8.23.1 eslint-scope: registry.npmjs.org/eslint-scope/5.1.1 eslint-utils: registry.npmjs.org/eslint-utils/3.0.0_eslint@8.23.1 + semver: registry.npmjs.org/semver/7.3.8 transitivePeerDependencies: - supports-color - typescript dev: true - registry.npmjs.org/@typescript-eslint/visitor-keys/5.39.0: - resolution: {integrity: sha512-yyE3RPwOG+XJBLrhvsxAidUgybJVQ/hG8BhiJo0k8JSAYfk/CshVcxf0HwP4Jt7WZZ6vLmxdo1p6EyN3tzFTkg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.39.0.tgz} + registry.npmjs.org/@typescript-eslint/visitor-keys/5.42.0: + resolution: {integrity: sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz} name: '@typescript-eslint/visitor-keys' - version: 5.39.0 + version: 5.42.0 engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.39.0 + '@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types/5.42.0 eslint-visitor-keys: registry.npmjs.org/eslint-visitor-keys/3.3.0 dev: true @@ -3415,37 +3387,11 @@ packages: prettier-linter-helpers: registry.npmjs.org/prettier-linter-helpers/1.0.0 dev: true - registry.npmjs.org/eslint-plugin-react/7.31.8_eslint@8.23.1: - resolution: {integrity: sha512-5lBTZmgQmARLLSYiwI71tiGVTLUuqXantZM6vlSY39OaDSV0M7+32K5DnLkmFrwTe+Ksz0ffuLUC91RUviVZfw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.8.tgz} - id: registry.npmjs.org/eslint-plugin-react/7.31.8 + registry.npmjs.org/eslint-plugin-react/7.31.10_eslint@8.23.1: + resolution: {integrity: sha512-e4N/nc6AAlg4UKW/mXeYWd3R++qUano5/o+t+wnWxIf+bLsOaH3a4q74kX3nDjYym3VBN4HyO9nEn1GcAqgQOA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.10.tgz} + id: registry.npmjs.org/eslint-plugin-react/7.31.10 name: eslint-plugin-react - version: 7.31.8 - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - dependencies: - array-includes: registry.npmjs.org/array-includes/3.1.5 - array.prototype.flatmap: registry.npmjs.org/array.prototype.flatmap/1.3.0 - doctrine: registry.npmjs.org/doctrine/2.1.0 - eslint: registry.npmjs.org/eslint/8.23.1 - estraverse: registry.npmjs.org/estraverse/5.3.0 - jsx-ast-utils: registry.npmjs.org/jsx-ast-utils/3.3.3 - minimatch: registry.npmjs.org/minimatch/3.1.2 - object.entries: registry.npmjs.org/object.entries/1.1.5 - object.fromentries: registry.npmjs.org/object.fromentries/2.0.5 - object.hasown: registry.npmjs.org/object.hasown/1.1.1 - object.values: registry.npmjs.org/object.values/1.1.5 - prop-types: registry.npmjs.org/prop-types/15.8.1 - resolve: registry.npmjs.org/resolve/2.0.0-next.4 - semver: registry.npmjs.org/semver/6.3.0 - string.prototype.matchall: registry.npmjs.org/string.prototype.matchall/4.0.7 - dev: true - - registry.npmjs.org/eslint-plugin-react/7.31.9_eslint@8.23.1: - resolution: {integrity: sha512-vrVJwusIw4L99lyfXjtCw8HWdloajsiYslMavogrBe2Gl8gr95TJsJnOMRasN4b4N24I3XuJf6aAV6MhyGmjqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.9.tgz} - id: registry.npmjs.org/eslint-plugin-react/7.31.9 - name: eslint-plugin-react - version: 7.31.9 + version: 7.31.10 engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 @@ -5219,6 +5165,12 @@ packages: hasBin: true dev: true + registry.npmjs.org/natural-compare-lite/1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz} + name: natural-compare-lite + version: 1.4.0 + dev: true + registry.npmjs.org/natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} name: natural-compare From a246f720e099180dcce1770926607dde6153b140 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 2 Nov 2022 11:30:48 +0100 Subject: [PATCH 11/12] refactor(minimap): cleanup events when not pannable or zoomable --- packages/minimap/src/MiniMap.tsx | 70 ++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 4fd639da..f2487f18 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { memo, useEffect, useRef } from 'react'; import type { MouseEvent } from 'react'; @@ -41,12 +42,12 @@ const ARIA_LABEL_KEY = 'react-flow__minimap-desc'; function MiniMap({ style, className, - nodeStrokeColor = '#555', - nodeColor = '#fff', + nodeStrokeColor = 'transparent', + nodeColor = '#e2e2e2', nodeClassName = '', nodeBorderRadius = 5, nodeStrokeWidth = 2, - maskColor = 'rgb(240, 242, 243, 0.7)', + maskColor = 'rgb(240, 240, 240, 0.6)', position = 'bottom-right', onClick, onNodeClick, @@ -81,43 +82,50 @@ function MiniMap({ if (svg.current) { const selection = select(svg.current as Element); - const zoomHandler = zoom() - .on('zoom.wheel', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); + const zoomHandler = (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); - if (event.sourceEvent.type !== 'wheel' || !zoomable || !d3Selection || !d3Zoom) { - return; - } + if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) { + return; + } - const pinchDelta = - -event.sourceEvent.deltaY * - (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * - 10; - const zoom = transform[2] * Math.pow(2, pinchDelta); + const pinchDelta = + -event.sourceEvent.deltaY * + (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * + 10; + const zoom = transform[2] * Math.pow(2, pinchDelta); - d3Zoom.scaleTo(d3Selection, zoom); - }) - .on('zoom', (event: D3ZoomEvent) => { - const { transform, d3Selection, d3Zoom } = store.getState(); + d3Zoom.scaleTo(d3Selection, zoom); + }; - if (event.sourceEvent.type !== 'mousemove' || !pannable || !d3Selection || !d3Zoom) { - return; - } + const panHandler = (event: D3ZoomEvent) => { + const { transform, d3Selection, d3Zoom } = store.getState(); - // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround. - const position = { - x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * Math.max(1, transform[2]), - y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * Math.max(1, transform[2]), - }; + if (event.sourceEvent.type !== 'mousemove' || !d3Selection || !d3Zoom) { + return; + } - const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + // @TODO: how to calculate the correct next position? Math.max(1, transform[2]) is a workaround. + const position = { + x: transform[0] - event.sourceEvent.movementX * viewScaleRef.current * Math.max(1, transform[2]), + y: transform[1] - event.sourceEvent.movementY * viewScaleRef.current * Math.max(1, transform[2]), + }; - d3Zoom.transform(d3Selection, nextTransform); - }); - selection.call(zoomHandler); + const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]); + + d3Zoom.transform(d3Selection, nextTransform); + }; + + const zoomAndPanHandler = zoom() + // @ts-ignore + .on('zoom', pannable ? panHandler : null) + // @ts-ignore + .on('zoom.wheel', zoomable ? zoomHandler : null); + + selection.call(zoomAndPanHandler); return () => { - selection.on('.zoom', null); + selection.on('zoom', null); }; } }, [pannable, zoomable]); From 8ba4dd5d1d4b2e6f107c148de62aec0b688d8b21 Mon Sep 17 00:00:00 2001 From: moklick Date: Wed, 2 Nov 2022 11:34:41 +0100 Subject: [PATCH 12/12] chore(changeset): add --- .changeset/twenty-items-invite.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/twenty-items-invite.md diff --git a/.changeset/twenty-items-invite.md b/.changeset/twenty-items-invite.md new file mode 100644 index 00000000..7252cb31 --- /dev/null +++ b/.changeset/twenty-items-invite.md @@ -0,0 +1,8 @@ +--- +'@reactflow/examples': minor +'@reactflow/core': minor +'@reactflow/minimap': minor +'reactflow': minor +--- + +Feat: Add pan and zoom to mini map