diff --git a/README.md b/README.md index 2f3f6e7f..802887c5 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ React Flow is a library for building node-based graphs. You can easily implement ## Key Features * **Easy to use:** Seamless zooming & panning behaviour and single and multi-selections of elements -* **Customizable:** Different [node](#node-types--custom-nodes) and [edge types](#edge-types--custom-edges) and support for custom nodes with multiple handles and edges +* **Customizable:** Different [node](#node-types--custom-nodes) and [edge types](#edge-types--custom-edges) and support for custom nodes with multiple handles and custom edges * **Fast rendering:** Only nodes that have changed are re-rendered and only those that are in the viewport are displayed -* **Utils:** Snap-to-grid, background styles and graph [helper functions](#helper-functions) -* **Components:** [Background, Minimap and graph controls](#components) +* **Utils:** Snap-to-grid and graph [helper functions](#helper-functions) +* **Components:** [Background, Minimap and Controls](#components) * **Reliable**: Written in [Typescript](https://www.typescriptlang.org/) and tested with [cypress](https://www.cypress.io/) In order to make this library as flexible as possible we don’t do any state updates besides the positions. This means that you need to pass the functions to remove an element or connect nodes by yourself. You can implement your own ones or use the helper functions that come with the library. @@ -78,9 +78,10 @@ const BasicFlow = () => ; - `snapToGrid`: default: `false` - `snapGrid`: [x, y] array - default: `[16, 16]` - `onlyRenderVisibleNodes`: default: `true` +- `translateExtent`: [default `[[-∞, -∞], [+∞, +∞]]`](https://github.com/d3/d3-zoom#zoom_translateExtent) #### Event Handlers -- `onElementClick(event: MouseEvent, element: Node | Edge)`: called when user clicks node or edge click +- `onElementClick(event: MouseEvent, element: Node | Edge)`: called when user clicks node or edge - `onElementsRemove(elements: Elements)`: called when user removes node or edge - `onNodeDragStart(event: MouseEvent, node: Node)`: node drag start - `onNodeDragStop(event: MouseEvent, node: Node)`: node drag stop @@ -93,21 +94,21 @@ const BasicFlow = () => ; - `onConnectStop(event: MouseEvent)`: called when user stops to drag connection line - `onConnectEnd(event: MouseEvent)`: called after user stops or connects nodes - `onLoad(reactFlowInstance)`: called after flow is initialized -- `onMove()`: called when user is panning or zooming -- `onMoveStart()`: called when user starts panning or zooming -- `onMoveEnd()`: called when user ends panning or zooming +- `onMove(flowTransform: FlowTransform)`: called when user is panning or zooming +- `onMoveStart(flowTransform: FlowTransform)`: called when user starts panning or zooming +- `onMoveEnd(flowTransform: FlowTransform)`: called when user ends panning or zooming - `onSelectionChange(elements: Elements)`: called when user selects one or multiple elements - `onSelectionDragStart(evt: MouseEvent, nodes: Node[])`: called when user starts to drag a selection - `onSelectionDrag(evt: MouseEvent, nodes: Node[])`: called when user drags a selection - `onSelectionDragStop(evt: MouseEvent, nodes: Node[])`: called when user stops to drag a selection - `onPaneClick(event: MouseEvent)`: called when user clicks directly on the canvas - `onPaneContextMenu(event: MouseEvent)`: called when user does a right-click on the canvas -- `onPaneScroll(event: WheelEvent)`: called when user scrolls pane (only works when `zoomOnScroll` is set to `false) +- `onPaneScroll(event: WheelEvent)`: called when user scrolls pane (only works when `zoomOnScroll` is set to `false) #### Interaction -- `nodesDraggable`: default: `true`. This applies to all nodes. You can also change the behavior of a specific node with the `draggable` node option. -- `nodesConnectable`: default: `true`. This applies to all nodes. You can also change the behavior of a specific node with the `connectable` node option. -- `elementsSelectable`: default: `true`. This applies to all elements. You can also change the behavior of a specific node with the `selectable` node option. +- `nodesDraggable`: default: `true`. This applies to all nodes. You can also change the behavior of a specific node with the `draggable` node option +- `nodesConnectable`: default: `true`. This applies to all nodes. You can also change the behavior of a specific node with the `connectable` node option +- `elementsSelectable`: default: `true`. This applies to all elements. You can also change the behavior of a specific node with the `selectable` node option - `zoomOnScroll`: default: `true` - `zoomOnDoubleClick`: default: `true` - `selectNodesOnDrag`: default: `true` @@ -145,13 +146,14 @@ const BasicFlow = () => ; ### project -Transforms pixel coordinates to the internal React Flow coordinate system +Transforms pixel coordinates to the internal ReactFlow coordinate system. +This can be used when you drag nodes (from a side bar for example) and need the position on the pane. `project = (position: XYPosition): XYPosition` ### fitView -Fits view port so that all nodes are visible +Fits view port so that all nodes are inside the view port. `fitView = ({ padding }): void` @@ -173,7 +175,7 @@ Fits view port so that all nodes are visible ### setTransform -Sets position and zoom of the pane +Sets position and zoom of the pane. `setTransform = (transform: FlowTransform): void` @@ -194,9 +196,9 @@ Node example: `{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { - `targetPosition`: 'left' | 'right' | 'top' | 'bottom' handle position - default: 'top' - `sourcePosition`: 'left' | 'right' | 'top' | 'bottom' handle position - default: 'bottom' - `isHidden`: if `true`, the node will not be rendered -- `draggable`: boolean - if option is not set, the node is draggable (overwrites generel `nodesDraggable` option) -- `connectable`: boolean - if option is not set, the node is connectable (overwrites generel `nodesConnectable` option) -- `selectable`: boolean - if option is not set, the node is selectable (overwrites generel `elementsSelectable` option) +- `draggable`: boolean - if option is not set, the node is draggable (overwrites general `nodesDraggable` option) +- `connectable`: boolean - if option is not set, the node is connectable (overwrites general `nodesConnectable` option) +- `selectable`: boolean - if option is not set, the node is selectable (overwrites general `elementsSelectable` option) ## Node Types & Custom Nodes @@ -236,7 +238,7 @@ Your custom nodes are wrapped so that the basic functions like dragging or selec ### Prevent dragging -If you have controls inside your custom node that should not drag the node you can add the class name `nodrag`. +If you have controls or other elements inside your custom node that should not drag the node you can add the class name `nodrag`. ## Handle Component @@ -531,10 +533,12 @@ The React Flow wrapper has the className `react-flow`. If you want to change the You could achieve the same effect by passing a style prop to the `ReactFlow` component: ```javascript +const style = { background: 'red', width: '100%' height: '300px' }; + const FlowWithRedBg = ( ); ``` @@ -549,37 +553,37 @@ import ReactFlow, { isNode, isEdge, removeElements, addEdge } from 'react-flow-r ### isEdge -Returns true if element is an edge +Returns `true` if the passed element is an edge. `isEdge = (element: Node | Edge): element is Edge` ### isNode -Returns true if element is a node +Returns `true` if the passed element is a node. `isNode = (element: Node | Edge): element is Node` ### removeElements -Returns an array of elements without the ones from `elementsToRemove`. It also removes all incoming/outgoing edges if you just pass one or multiple nodes +Returns an array of elements without the ones from `elementsToRemove`. It also removes all incoming/outgoing edges if you just pass one or multiple nodes. `removeElements = (elementsToRemove: Elements, elements: Elements): Elements` ### addEdge -Returns elements array with added edge +Returns an array with elements with the added edge. `addEdge = (edgeParams: Edge, elements: Elements): Elements` ### getOutgoers -Returns all direct child nodes of the passed node +Returns all direct child nodes of the passed node. `getOutgoers = (node: Node, elements: Elements): Node[]` ### getConnectedEdges -Returns all edges that are connected to the passed nodes +Returns all edges that are connected to the passed nodes. `getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[]` @@ -657,7 +661,7 @@ This serves the content of the `example` folder and watches changes inside the ` # Testing -Testing is done with cypress. You can find all tests in the [`integration/flow`](/cypress/integration/flow) folder. In order to run the tests do: +Testing is done with cypress. You can find the tests in the [`integration/flow`](/cypress/integration/flow) folder. In order to run the tests do: ``` npm run test diff --git a/example/src/Provider/Sidebar.js b/example/src/Provider/Sidebar.js index ab8def0d..7006f086 100644 --- a/example/src/Provider/Sidebar.js +++ b/example/src/Provider/Sidebar.js @@ -1,9 +1,14 @@ import React from 'react'; -import { useStoreState } from 'react-flow-renderer'; +import { useStoreState, useStoreActions } from 'react-flow-renderer'; export default () => { const nodes = useStoreState((store) => store.nodes); const transform = useStoreState((store) => store.transform); + const setSelectedElements = useStoreActions((actions) => actions.setSelectedElements); + + const selectAll = () => { + setSelectedElements(nodes.map((node) => ({ id: node.id, type: node.type }))); + }; return ( ); }; diff --git a/example/src/Provider/provider.css b/example/src/Provider/provider.css index ac199bab..8bb8ebf2 100644 --- a/example/src/Provider/provider.css +++ b/example/src/Provider/provider.css @@ -30,3 +30,7 @@ flex-grow: 1; height: 100%; } + +.providerflow .selectall { + margin-top: 10px; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5b91e818..77d2dcb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,13 +63,13 @@ } }, "@babel/core": { - "version": "7.11.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.5.tgz", - "integrity": "sha512-fsEANVOcZHzrsV6dMVWqpSeXClq3lNbYrfFGme6DE25FQWe7pyeYpXyx9guqUnpy466JLzZ8z4uwSr2iv60V5Q==", + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.11.6.tgz", + "integrity": "sha512-Wpcv03AGnmkgm6uS6k8iwhIwTrcP0m17TL1n1sy7qD0qelDu4XNeW0dN0mHfa+Gei211yDaLoEe/VlbXQzM4Bg==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.11.5", + "@babel/generator": "^7.11.6", "@babel/helper-module-transforms": "^7.11.0", "@babel/helpers": "^7.10.4", "@babel/parser": "^7.11.5", @@ -83,7 +83,7 @@ "lodash": "^4.17.19", "resolve": "^1.3.2", "semver": "^5.4.1", - "source-map": "^0.6.1" + "source-map": "^0.5.0" }, "dependencies": { "@babel/code-frame": { @@ -95,6 +95,17 @@ "@babel/highlight": "^7.10.4" } }, + "@babel/generator": { + "version": "7.11.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.6.tgz", + "integrity": "sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA==", + "dev": true, + "requires": { + "@babel/types": "^7.11.5", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, "@babel/helper-member-expression-to-functions": { "version": "7.11.0", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz", @@ -212,12 +223,6 @@ "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true } } }, @@ -4144,9 +4149,9 @@ } }, "@octokit/plugin-paginate-rest": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.3.1.tgz", - "integrity": "sha512-81A+ONLpcSX7vWxnEmVZteQPNsbdeScSVUqjgMYPSk1trzG69iYkhS42wPRWtN0nYw6OEmT48DNeQCjHeyroYw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.3.2.tgz", + "integrity": "sha512-PjHbMhKryxClCrmfvRpGaKCTxUcHIf2zirWRV9SMGf0EmxD/rFew/abSqbMiLl9uQgRZvqtTyCRMGMlUv1ZsBg==", "dev": true, "requires": { "@octokit/types": "^5.3.0" @@ -6104,12 +6109,6 @@ "supports-color": "^5.3.0" } }, - "changelog-filename-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/changelog-filename-regex/-/changelog-filename-regex-1.1.2.tgz", - "integrity": "sha512-kpOfKlZ9x2UpeC4at6FAXHLKfi/JEUqUqkPCb1JUCa5FnNbJIzOHRM9RfeQ1QDcpj+Gxuc/UoHqASgmEeFDejQ==", - "dev": true - }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -7118,15 +7117,6 @@ "object-keys": "^1.0.12" } }, - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -7157,26 +7147,6 @@ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", "dev": true }, - "detect-repo-changelog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/detect-repo-changelog/-/detect-repo-changelog-1.0.1.tgz", - "integrity": "sha1-whCMu/CWTae8B0ZFtq0dO4CmvW0=", - "dev": true, - "requires": { - "changelog-filename-regex": "^1.1.0", - "is-regular-file": "^1.0.1", - "lodash.find": "^4.6.0", - "pify": "^2.3.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -8311,9 +8281,9 @@ } }, "rxjs": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", - "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", "dev": true, "requires": { "tslib": "^1.9.0" @@ -8360,15 +8330,6 @@ "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", "dev": true }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8419,32 +8380,12 @@ "rgba-regex": "^1.0.0" } }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", "dev": true }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, "is-directory": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", @@ -8566,12 +8507,6 @@ "has": "^1.0.3" } }, - "is-regular-file": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regular-file/-/is-regular-file-1.1.1.tgz", - "integrity": "sha512-+1U3MZrVwC4HM6VUKk3L5fiHtNd2d9kayzEJhmQ+B+uIBPE/p8Fy8QVdkx0HIr3o9J5TOKJY40eI5GfTfBqbdA==", - "dev": true - }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", @@ -8791,12 +8726,6 @@ "json-buffer": "3.0.1" } }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, "latest-version": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", @@ -9039,12 +8968,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.find": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", - "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", - "dev": true - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -9418,9 +9341,9 @@ "dev": true }, "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", "dev": true }, "node-releases": { @@ -11102,9 +11025,9 @@ } }, "release-it": { - "version": "13.7.1", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-13.7.1.tgz", - "integrity": "sha512-OAupGLMZJBGrGRJmb8RvZ8ZQAQquKlkGGYvgRrJmkHE+jnGdf+YSvEmItBv08CwhDcwCyXdTkkN/rJ2Flt68+g==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-14.0.2.tgz", + "integrity": "sha512-YJEI8z/cl0gXuVN3a4n/oZ9LYjR9tzEiuUbUROMKG172tzrIDPILT48CR3PQ0fpTGqGimraNFCZrTFiZWqP4pA==", "dev": true, "requires": { "@iarna/toml": "2.2.5", @@ -11114,7 +11037,6 @@ "cosmiconfig": "7.0.0", "debug": "4.1.1", "deprecated-obj": "2.0.0", - "detect-repo-changelog": "1.0.1", "execa": "4.0.3", "find-up": "5.0.0", "form-data": "3.0.0", @@ -11131,11 +11053,9 @@ "parse-json": "5.1.0", "semver": "7.3.2", "shelljs": "0.8.4", - "supports-color": "7.2.0", "update-notifier": "4.1.1", "url-join": "4.0.1", "uuid": "8.3.0", - "window-size": "1.1.1", "yaml": "1.10.0", "yargs-parser": "19.0.4" }, @@ -11377,9 +11297,9 @@ } }, "rollup": { - "version": "2.26.9", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.9.tgz", - "integrity": "sha512-XIiWYLayLqV+oY4S2Lub/shJq4uk/QQLwWToYCL4LjZbYHbFK3czea4UDVRUJu+zNmKmxq5Zb/OG7c5HSvH2TQ==", + "version": "2.26.10", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.26.10.tgz", + "integrity": "sha512-dUnjCWOA0h9qNX6qtcHidyatz8FAFZxVxt1dbcGtKdlJkpSxGK3G9+DLCYvtZr9v94D129ij9zUhG+xbRoqepw==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -11640,9 +11560,9 @@ } }, "rollup-plugin-terser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.1.tgz", - "integrity": "sha512-HL0dgzSxBYG/Ly9i/E5Sc+PuKKZ0zBzk11VmLCfdUtpqH4yYqkLclPkTqRy85FU9246yetImOClaQ/ufnj08vg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", "dev": true, "requires": { "@babel/code-frame": "^7.10.4", @@ -12257,9 +12177,9 @@ "dev": true }, "terser": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.2.1.tgz", - "integrity": "sha512-/AOtjRtAMNGO0fIF6m8HfcvXTw/2AKpsOzDn36tA5RfhRdeXyb4RvHxJ5Pah7iL6dFkLk+gOnCaNHGwJPl6TrQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.0.tgz", + "integrity": "sha512-XTT3D3AwxC54KywJijmY2mxZ8nJiEjBHVYzq8l9OaYuRFWeQNBwvipuzzYEP4e+/AVcd1hqG/CqgsdIRyT45Fg==", "dev": true, "requires": { "commander": "^2.20.0", @@ -12687,36 +12607,6 @@ "string-width": "^4.0.0" } }, - "window-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-1.1.1.tgz", - "integrity": "sha512-5D/9vujkmVQ7pSmc0SCBmHXbkv6eaHwXEx65MywhmUMsI8sGqJ972APq1lotfcwMKPFLuCFfL8xGHLIp7jaBmA==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "is-number": "^3.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "windows-release": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", diff --git a/package.json b/package.json index 586b5bf7..d0ac5b26 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "resize-observer": "^1.0.0" }, "devDependencies": { - "@babel/core": "^7.11.5", + "@babel/core": "^7.11.6", "@babel/preset-env": "^7.11.5", "@babel/preset-react": "^7.10.4", "@svgr/rollup": "^5.4.0", @@ -51,8 +51,8 @@ "prettier": "2.1.1", "prop-types": "^15.7.2", "react": "^16.13.1", - "release-it": "^13.7.1", - "rollup": "^2.26.9", + "release-it": "^14.0.2", + "rollup": "^2.26.10", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-commonjs": "^10.1.0", @@ -62,7 +62,7 @@ "rollup-plugin-postcss": "^3.1.8", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-serve": "^1.0.4", - "rollup-plugin-terser": "^7.0.1", + "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.27.2", "rollup-plugin-uglify": "^6.0.4", "start-server-and-test": "^1.11.3", diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index c43a3c6a..54be5214 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -16,151 +16,11 @@ import { ResizeObserver } from 'resize-observer'; import { useStoreActions } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; -import { - Node, - XYPosition, - Transform, - ElementId, - NodeComponentProps, - WrapNodeProps, - Elements, - Edge, - NodePosUpdate, -} from '../../types'; - -import { noop } from '../../utils'; +import { Node, NodeComponentProps, WrapNodeProps } from '../../types'; const getMouseEvent = (event: MouseEvent | TouchEvent) => typeof TouchEvent !== 'undefined' && event instanceof TouchEvent ? event.touches[0] : (event as MouseEvent); -interface OnDragStartParams { - event: MouseEvent | TouchEvent; - id: ElementId; - type: string; - data: any; - selectNodesOnDrag: boolean; - isSelectable: boolean; - setOffset: (pos: XYPosition) => void; - transform: Transform; - position: XYPosition; - setSelectedElements: (elms: Elements | Node | Edge) => void; - onNodeDragStart?: (event: MouseEvent, node: Node) => void; -} - -const onStart = ({ - event, - onNodeDragStart, - id, - type, - data, - selectNodesOnDrag, - setOffset, - transform, - position, - setSelectedElements, - isSelectable, -}: OnDragStartParams): false | void => { - const startEvent = getMouseEvent(event); - - const scaledClient: XYPosition = { - x: startEvent.clientX * (1 / transform[2]), - y: startEvent.clientY * (1 / transform[2]), - }; - - const offsetX = scaledClient.x - position.x - transform[0]; - const offsetY = scaledClient.y - position.y - transform[1]; - const node = { id, type, position, data }; - - setOffset({ x: offsetX, y: offsetY }); - - if (onNodeDragStart) { - onNodeDragStart(event as MouseEvent, node); - } - - if (selectNodesOnDrag && isSelectable) { - setSelectedElements({ id, type } as Node); - } -}; - -interface OnDragParams { - event: MouseEvent | TouchEvent; - setDragging: (isDragging: boolean) => void; - id: ElementId; - offset: XYPosition; - transform: Transform; - updateNodePos: (params: NodePosUpdate) => void; -} - -const onDrag = ({ event, setDragging, id, offset, transform, updateNodePos }: OnDragParams): void => { - const dragEvent = getMouseEvent(event); - - const scaledClient = { - x: dragEvent.clientX / transform[2], - y: dragEvent.clientY / transform[2], - }; - - setDragging(true); - updateNodePos({ - id, - pos: { - x: scaledClient.x - transform[0] - offset.x, - y: scaledClient.y - transform[1] - offset.y, - }, - }); -}; - -interface OnDragStopParams { - event: MouseEvent; - isDragging: boolean; - setDragging: (isDragging: boolean) => void; - id: ElementId; - type: string; - position: XYPosition; - data: any; - selectNodesOnDrag: boolean; - isSelectable: boolean; - setSelectedElements: (elms: Elements | Node | Edge) => void; - onNodeDragStop?: (event: MouseEvent, node: Node) => void; - onClick?: (event: MouseEvent, node: Node) => void; -} - -const onStop = ({ - event, - id, - type, - position, - data, - isDragging, - setDragging, - selectNodesOnDrag, - isSelectable, - onNodeDragStop, - onClick, - setSelectedElements, -}: OnDragStopParams): void => { - const node = { - id, - type, - position, - data, - } as Node; - if (!isDragging && isSelectable) { - if (!selectNodesOnDrag) { - setSelectedElements({ id, type } as Node); - } - - if (onClick) { - return onClick(event, node); - } - } - - setDragging(false); - - if (onNodeDragStop) { - onNodeDragStop(event, node); - } -}; - export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, @@ -195,60 +55,129 @@ export default (NodeComponent: ComponentType) => { const nodeElement = useRef(null); const [offset, setOffset] = useState({ x: 0, y: 0 }); const [isDragging, setDragging] = useState(false); - const position = { x: xPos, y: yPos }; - const nodeClasses = cc([ - 'react-flow__node', - `react-flow__node-${type}`, - className, - { - selected, - selectable: isSelectable, - }, - ]); - const node = { id, type, position, data }; + + const position = useMemo(() => ({ x: xPos, y: yPos }), [xPos, yPos]); + const node = useMemo(() => ({ id, type, position, data }), [id, type, position, data]); + const nodeStyle: CSSProperties = useMemo( + () => ({ + zIndex: selected ? 10 : 3, + transform: `translate(${xPos}px,${yPos}px)`, + pointerEvents: isSelectable || isDraggable || onClick ? 'all' : 'none', + opacity: isInitialized ? 1 : 0, // prevents jumping of nodes on start + ...style, + }), + [selected, xPos, yPos, isSelectable, isDraggable, onClick, isInitialized, style] + ); const onMouseEnterHandler = useMemo(() => { if (!onMouseEnter || isDragging) { - return noop; + return; } return (event: MouseEvent) => onMouseEnter(event, node); - }, [onMouseEnter, isDragging]); + }, [onMouseEnter, isDragging, node]); const onMouseMoveHandler = useMemo(() => { if (!onMouseMove || isDragging) { - return noop; + return; } return (event: MouseEvent) => onMouseMove(event, node); - }, [onMouseMove, isDragging]); + }, [onMouseMove, isDragging, node]); const onMouseLeaveHandler = useMemo(() => { if (!onMouseLeave || isDragging) { - return noop; + return; } return (event: MouseEvent) => onMouseLeave(event, node); - }, [onMouseLeave, isDragging]); + }, [onMouseLeave, isDragging, node]); const onContextMenuHandler = useMemo(() => { if (!onContextMenu) { - return noop; + return; } return (event: MouseEvent) => onContextMenu(event, node); - }, [onContextMenu]); + }, [onContextMenu, node]); const onSelectNodeHandler = useCallback( (event: MouseEvent) => { - if (!isDraggable && isSelectable) { - setSelectedElements({ id: node.id, type: node.type } as Node); - } + if (!isDraggable) { + if (isSelectable) { + setSelectedElements({ id: node.id, type: node.type } as Node); + } - if (onClick) { - onClick(event, node); + if (onClick) { + onClick(event, node); + } } }, - [isSelectable, isDraggable, node] + [isSelectable, isDraggable, onClick, node] + ); + + const onDragStart = useCallback( + (event) => { + const startEvent = getMouseEvent(event); + + const scaledClientX = startEvent.clientX / transform[2]; + const scaledClientY = startEvent.clientY / transform[2]; + + const offsetX = scaledClientX - position.x - transform[0]; + const offsetY = scaledClientY - position.y - transform[1]; + + setOffset({ x: offsetX, y: offsetY }); + + if (onNodeDragStart) { + onNodeDragStart(event as MouseEvent, node); + } + + if (selectNodesOnDrag && isSelectable) { + setSelectedElements({ id: node.id, type: node.type } as Node); + } + }, + [node, transform, position, selectNodesOnDrag, isSelectable, onNodeDragStart] + ); + + const onDrag = useCallback( + (event) => { + const dragEvent = getMouseEvent(event); + + const scaledClientX = dragEvent.clientX / transform[2]; + const scaledClientY = dragEvent.clientY / transform[2]; + + setDragging(true); + updateNodePos({ + id, + pos: { + x: scaledClientX - transform[0] - offset.x, + y: scaledClientY - transform[1] - offset.y, + }, + }); + }, + [id, transform, offset] + ); + + const onDragStop = useCallback( + (event) => { + if (!isDragging) { + if (isSelectable && !selectNodesOnDrag) { + setSelectedElements({ id: node.id, type: node.type } as Node); + } + + if (onClick) { + onClick(event as MouseEvent, node); + } + + return; + } + + setDragging(false); + + if (onNodeDragStop) { + onNodeDragStop(event as MouseEvent, node); + } + }, + [node, isDragging, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop] ); useEffect(() => { @@ -279,48 +208,21 @@ export default (NodeComponent: ComponentType) => { return null; } - const nodeStyle: CSSProperties = { - zIndex: selected ? 10 : 3, - transform: `translate(${xPos}px,${yPos}px)`, - pointerEvents: isSelectable || isDraggable || onClick ? 'all' : 'none', - opacity: isInitialized ? 1 : 0, // prevents jumping of nodes on start - ...style, - }; + const nodeClasses = cc([ + 'react-flow__node', + `react-flow__node-${type}`, + className, + { + selected, + selectable: isSelectable, + }, + ]); return ( - onStart({ - event: event as MouseEvent, - selectNodesOnDrag, - isSelectable, - onNodeDragStart, - id, - type, - data, - setOffset, - transform, - position, - setSelectedElements, - }) - } - onDrag={(event) => onDrag({ event: event as MouseEvent, setDragging, id, offset, transform, updateNodePos })} - onStop={(event) => - onStop({ - event: event as MouseEvent, - onNodeDragStop, - selectNodesOnDrag, - isSelectable, - onClick, - isDragging, - setDragging, - id, - type, - position, - data, - setSelectedElements, - }) - } + onStart={onDragStart} + onDrag={onDrag} + onStop={onDragStop} scale={transform[2]} disabled={!isDraggable} cancel=".nodrag" diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 0baef218..88e2013c 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -24,6 +24,7 @@ import { OnConnectStartFunc, OnConnectStopFunc, OnConnectEndFunc, + TranslateExtent, } from '../../types'; export interface GraphViewProps { @@ -67,6 +68,7 @@ export interface GraphViewProps { maxZoom: number; defaultZoom: number; defaultPosition: [number, number]; + translateExtent?: TranslateExtent; arrowHeadColor: string; markerEndId?: string; zoomOnScroll: boolean; @@ -112,6 +114,7 @@ const GraphView = ({ maxZoom, defaultZoom, defaultPosition, + translateExtent, arrowHeadColor, markerEndId, zoomOnScroll, @@ -137,6 +140,7 @@ const GraphView = ({ const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); const setInitTransform = useStoreActions((actions) => actions.setInitTransform); const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom); + const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const fitView = useStoreActions((actions) => actions.fitView); const zoom = useStoreActions((actions) => actions.zoom); const zoomTo = useStoreActions((actions) => actions.zoomTo); @@ -254,6 +258,12 @@ const GraphView = ({ setMinMaxZoom({ minZoom, maxZoom }); }, [minZoom, maxZoom]); + useEffect(() => { + if (typeof translateExtent !== 'undefined') { + setTranslateExtent(translateExtent); + } + }, [translateExtent]); + return (
, 'on maxZoom: number; defaultZoom: number; defaultPosition: [number, number]; + translateExtent?: TranslateExtent; arrowHeadColor: string; markerEndId?: string; zoomOnScroll: boolean; @@ -125,6 +127,7 @@ const ReactFlow = ({ maxZoom, defaultZoom, defaultPosition, + translateExtent, arrowHeadColor, markerEndId, zoomOnScroll, @@ -176,6 +179,7 @@ const ReactFlow = ({ maxZoom={maxZoom} defaultZoom={defaultZoom} defaultPosition={defaultPosition} + translateExtent={translateExtent} arrowHeadColor={arrowHeadColor} markerEndId={markerEndId} zoomOnScroll={zoomOnScroll} diff --git a/src/store/index.ts b/src/store/index.ts index 913ba56e..9827092c 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -26,6 +26,7 @@ import { SetConnectionId, NodePosUpdate, FitViewParams, + TranslateExtent, } from '../types'; type TransformXYK = { @@ -65,6 +66,7 @@ export interface StoreModel { d3Initialised: boolean; minZoom: number; maxZoom: number; + translateExtent: TranslateExtent; nodesSelectionActive: boolean; selectionActive: boolean; @@ -116,6 +118,8 @@ export interface StoreModel { setMinMaxZoom: Action; + setTranslateExtent: Action; + setSnapGrid: Action; setConnectionPosition: Action; @@ -154,6 +158,10 @@ export const storeModel: StoreModel = { d3Initialised: false, minZoom: 0.5, maxZoom: 2, + translateExtent: [ + [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY], + [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], + ], nodesSelectionActive: false, selectionActive: false, @@ -351,7 +359,7 @@ export const storeModel: StoreModel = { }), initD3: action((state, zoomPaneNode) => { - const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]); + const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(state.translateExtent); const selection = select(zoomPaneNode).call(d3ZoomInstance); @@ -369,6 +377,14 @@ export const storeModel: StoreModel = { } }), + setTranslateExtent: action((state, translateExtent) => { + state.translateExtent = translateExtent; + + if (state.d3Zoom) { + state.d3Zoom.translateExtent(translateExtent); + } + }), + setConnectionPosition: action((state, position) => { state.connectionPosition = position; }), diff --git a/src/types/index.ts b/src/types/index.ts index 8837c769..7d02712a 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -272,3 +272,5 @@ export type FlowTransform = { y: number; zoom: number; }; + +export type TranslateExtent = [[number, number], [number, number]]; diff --git a/src/utils/index.ts b/src/utils/index.ts index 165d0e2f..95130e1e 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,8 +1,6 @@ import { DraggableEvent } from 'react-draggable'; import { MouseEvent as ReactMouseEvent } from 'react'; -export const noop = () => {}; - export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => { const target = e.target as HTMLElement; return e && target && ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName);