diff --git a/README.md b/README.md index a9c42091..a3c66eb7 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,12 @@ const BasicFlow = () => ( - `elements`: array of [nodes](#nodes) and [edges](#edges) *(required)* - `onElementClick`: element click handler - `onElementsRemove`: element remove handler +- `onNodeDragStart`: node drag start handler - `onNodeDragStop`: node drag stop handler - `onConnect`: connect handler - `onLoad`: editor load handler - `onMove`: move handler +- `onSelectionChange`: fired when element selection changes - `nodeTypes`: object with [node types](#node-types--custom-nodes) - `edgeTypes`: object with [edge types](#edge-types--custom-edges) - `style`: css style passed to the wrapper diff --git a/example/README.md b/example/README.md index 89b278ae..53067429 100644 --- a/example/README.md +++ b/example/README.md @@ -1,68 +1,15 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +## React Flow Examples -## Available Scripts +The examples are deployed here: https://react-flow.netlify.app/ -In the project directory, you can run: +### Installation -### `yarn start` +``` +npm install +``` -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +### Start Dev Server -The page will reload if you make edits.
-You will also see any lint errors in the console. - -### `yarn test` - -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `yarn build` - -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. - -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `yarn build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +``` +npm start +``` \ No newline at end of file diff --git a/example/src/Overview/index.js b/example/src/Overview/index.js index 097bd54c..7df9ed28 100644 --- a/example/src/Overview/index.js +++ b/example/src/Overview/index.js @@ -2,8 +2,10 @@ import React, { useState } from 'react'; import ReactFlow, { removeElements, addEdge, MiniMap, Controls } from 'react-flow-renderer'; +const onNodeDragStart = node => console.log('drag start', node); const onNodeDragStop = node => console.log('drag stop', node); const onElementClick = element => console.log('click', element); +const onSelectionChange = elements => console.log('selection change', elements); const onLoad = (graph) => { console.log('graph loaded:', graph); graph.fitView(); @@ -50,7 +52,9 @@ const OverviewFlow = () => { onElementClick={onElementClick} onElementsRemove={onElementsRemove} onConnect={onConnect} + onNodeDragStart={onNodeDragStart} onNodeDragStop={onNodeDragStop} + onSelectionChange={onSelectionChange} style={{ width: '100%', height: '100%' }} onLoad={onLoad} connectionLineStyle={{ stroke: '#ddd', strokeWidth: 2 }} diff --git a/package-lock.json b/package-lock.json index 9bde76ea..0d9f1a81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3393,18 +3393,6 @@ "defer-to-connect": "^2.0.0" } }, - "@types/blob-util": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/blob-util/-/blob-util-1.3.3.tgz", - "integrity": "sha512-4ahcL/QDnpjWA2Qs16ZMQif7HjGP2cw3AGjHabybjw7Vm1EKu+cfQN1D78BaZbS1WJNa1opSMF5HNMztx7lR0w==", - "dev": true - }, - "@types/bluebird": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.29.tgz", - "integrity": "sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw==", - "dev": true - }, "@types/cacheable-request": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", @@ -3417,22 +3405,6 @@ "@types/responselike": "*" } }, - "@types/chai": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.7.tgz", - "integrity": "sha512-luq8meHGYwvky0O7u0eQZdA7B4Wd9owUCqvbw2m3XCrCU8mplYOujMBbvyS547AxJkC+pGnd0Cm15eNxEUNU8g==", - "dev": true - }, - "@types/chai-jquery": { - "version": "1.1.40", - "resolved": "https://registry.npmjs.org/@types/chai-jquery/-/chai-jquery-1.1.40.tgz", - "integrity": "sha512-mCNEZ3GKP7T7kftKeIs7QmfZZQM7hslGSpYzKbOlR2a2HCFf9ph4nlMRA9UnuOETeOQYJVhJQK7MwGqNZVyUtQ==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/jquery": "*" - } - }, "@types/classnames": { "version": "2.2.10", "resolved": "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz", @@ -3723,15 +3695,6 @@ "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==", "dev": true }, - "@types/jquery": { - "version": "3.3.31", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.31.tgz", - "integrity": "sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, "@types/keyv": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", @@ -3741,24 +3704,6 @@ "@types/node": "*" } }, - "@types/lodash": { - "version": "4.14.149", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.149.tgz", - "integrity": "sha512-ijGqzZt/b7BfzcK9vTrS6MFljQRPn5BFWOx8oE0GYxribu6uV+aA9zZuXI1zc/etK9E8nrgdoF2+LgUw7+9tJQ==", - "dev": true - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mocha": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.7.tgz", - "integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==", - "dev": true - }, "@types/node": { "version": "12.7.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", @@ -3820,22 +3765,12 @@ "@types/node": "*" } }, - "@types/sinon": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.5.1.tgz", - "integrity": "sha512-EZQUP3hSZQyTQRfiLqelC9NMWd1kqLcmQE0dMiklxBkgi84T+cHOhnKpgk4NnOWpGX863yE6+IaGnOXUNFqDnQ==", + "@types/sinonjs__fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz", + "integrity": "sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==", "dev": true }, - "@types/sinon-chai": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.3.tgz", - "integrity": "sha512-TOUFS6vqS0PVL1I8NGVSNcFaNJtFoyZPXZ5zur+qlhDfOmQECZZM4H4kKgca6O8L+QceX/ymODZASfUfn+y4yQ==", - "dev": true, - "requires": { - "@types/chai": "*", - "@types/sinon": "*" - } - }, "@types/sizzle": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", @@ -3843,9 +3778,9 @@ "dev": true }, "@welldone-software/why-did-you-render": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-4.2.1.tgz", - "integrity": "sha512-Q1jH++Sx0F09bU2eTEzAPuBxPiv/uxzgFfWMX1G8LqT4xzdGOrr287DD3lsfPpGLDLx9sqL8jMc0RoxorRewUg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@welldone-software/why-did-you-render/-/why-did-you-render-4.2.2.tgz", + "integrity": "sha512-v08t2WXFQdnxkPodXzbPeho3FwgrlwzjwxasN+8A1LSZnkcxJpYvOF8/z+OySqehC44JT6oPB1KEnBVMrebHdw==", "requires": { "lodash": "^4" } @@ -5102,9 +5037,9 @@ } }, "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "common-tags": { @@ -5540,24 +5475,15 @@ "dev": true }, "cypress": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.5.0.tgz", - "integrity": "sha512-2A4g5FW5d2fHzq8HKUGAMVTnW6P8nlWYQALiCoGN4bqBLvgwhYM/oG9oKc2CS6LnvgHFiKivKzpm9sfk3uU3zQ==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-4.6.0.tgz", + "integrity": "sha512-vIPXAceRP+Nxvnm/O9ruY9EQaRGmVVybtk9F1sfC9mH3067YbitrdBTynaaLuHFj90p9e0U2ZCV7OkX4x4V/Wg==", "dev": true, "requires": { "@cypress/listr-verbose-renderer": "0.4.1", "@cypress/request": "2.88.5", "@cypress/xvfb": "1.2.4", - "@types/blob-util": "1.3.3", - "@types/bluebird": "3.5.29", - "@types/chai": "4.2.7", - "@types/chai-jquery": "1.1.40", - "@types/jquery": "3.3.31", - "@types/lodash": "4.14.149", - "@types/minimatch": "3.0.3", - "@types/mocha": "5.2.7", - "@types/sinon": "7.5.1", - "@types/sinon-chai": "3.2.3", + "@types/sinonjs__fake-timers": "6.0.1", "@types/sizzle": "2.3.2", "arch": "2.1.1", "bluebird": "3.7.2", @@ -5635,12 +5561,6 @@ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, "pretty-bytes": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", @@ -6260,21 +6180,6 @@ "ms": "2.0.0" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -10139,9 +10044,9 @@ } }, "rollup": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.10.2.tgz", - "integrity": "sha512-tivFM8UXBlYOUqpBYD3pRktYpZvK/eiCQ190eYlrAyrpE/lzkyG2gbawroNdbwmzyUc7Y4eT297xfzv0BDh9qw==", + "version": "2.10.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz", + "integrity": "sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg==", "dev": true, "requires": { "fsevents": "~2.1.2" @@ -10402,32 +10307,61 @@ } }, "rollup-plugin-terser": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.3.0.tgz", - "integrity": "sha512-XGMJihTIO3eIBsVGq7jiNYOdDMb3pVxuzY0uhOE/FM4x/u9nQgr3+McsjzqBn3QfHIpNSZmFnpoKAwHBEcsT7g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-6.1.0.tgz", + "integrity": "sha512-4fB3M9nuoWxrwm39habpd4hvrbrde2W2GG4zEGPQg1YITNkM3Tqur5jSuXlWNzbv/2aMLJ+dZJaySc3GCD8oDw==", "dev": true, "requires": { - "@babel/code-frame": "^7.5.5", - "jest-worker": "^24.9.0", - "rollup-pluginutils": "^2.8.2", - "serialize-javascript": "^2.1.2", - "terser": "^4.6.2" + "@babel/code-frame": "^7.8.3", + "jest-worker": "^26.0.0", + "serialize-javascript": "^3.0.0", + "terser": "^4.7.0" }, "dependencies": { - "rollup-pluginutils": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", - "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==", + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, "requires": { - "estree-walker": "^0.6.1" + "@babel/highlight": "^7.8.3" } }, - "serialize-javascript": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", - "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==", + "@babel/highlight": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", + "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.9.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true + }, + "jest-worker": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", + "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -10608,6 +10542,12 @@ } } }, + "serialize-javascript": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", + "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", + "dev": true + }, "shallow-clone": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", @@ -10705,9 +10645,9 @@ "dev": true }, "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", + "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "dev": true, "requires": { "buffer-from": "^1.0.0", @@ -11020,9 +10960,9 @@ "dev": true }, "terser": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.6.tgz", - "integrity": "sha512-4lYPyeNmstjIIESr/ysHg2vUPRGf2tzF9z2yYwnowXVuVzLEamPN1Gfrz7f8I9uEPuHcbFlW4PLIAsJoxXyJ1g==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.7.0.tgz", + "integrity": "sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw==", "dev": true, "requires": { "commander": "^2.20.0", @@ -11144,9 +11084,9 @@ } }, "typescript": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz", - "integrity": "sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw==", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz", + "integrity": "sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 55ac783f..a80c880a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "release": "release-it" }, "dependencies": { - "@welldone-software/why-did-you-render": "^4.2.1", + "@welldone-software/why-did-you-render": "^4.2.2", "classnames": "^2.2.6", "d3-selection": "^1.4.1", "d3-zoom": "^1.8.3", @@ -44,13 +44,13 @@ "autoprefixer": "^9.8.0", "babel-loader": "^8.1.0", "babel-preset-react-app": "^9.1.2", - "cypress": "^4.5.0", + "cypress": "^4.6.0", "postcss-nested": "^4.2.1", "prettier": "2.0.5", "prop-types": "^15.7.2", "react": "^16.13.1", "release-it": "^13.6.1", - "rollup": "^2.10.2", + "rollup": "^2.10.9", "rollup-plugin-babel": "^4.4.0", "rollup-plugin-bundle-size": "^1.0.3", "rollup-plugin-commonjs": "^10.1.0", @@ -60,11 +60,11 @@ "rollup-plugin-postcss": "^3.1.1", "rollup-plugin-replace": "^2.2.0", "rollup-plugin-serve": "^1.0.1", - "rollup-plugin-terser": "^5.3.0", + "rollup-plugin-terser": "^6.1.0", "rollup-plugin-typescript2": "^0.27.1", "rollup-plugin-uglify": "^6.0.4", "start-server-and-test": "^1.11.0", - "typescript": "^3.9.2" + "typescript": "^3.9.3" }, "peerDependencies": { "react": "^16.13.1" diff --git a/src/components/Nodes/wrapNode.tsx b/src/components/Nodes/wrapNode.tsx index 3522fa56..0c8b1376 100644 --- a/src/components/Nodes/wrapNode.tsx +++ b/src/components/Nodes/wrapNode.tsx @@ -12,7 +12,7 @@ const getMouseEvent = (evt: MouseEvent | TouchEvent) => const onStart = ( evt: MouseEvent | TouchEvent, - onClick: (node: Node) => void, + onDragStart: (node: Node) => void, id: ElementId, type: string, data: any, @@ -26,13 +26,14 @@ const onStart = ( x: startEvt.clientX * (1 / transform[2]), y: startEvt.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 }; store.dispatch.setSelectedElements({ id, type } as Node); setOffset({ x: offsetX, y: offsetY }); - onClick(node); + onDragStart(node); }; const onDrag = ( @@ -60,7 +61,8 @@ const onDrag = ( }; const onStop = ( - onNodeDragStop: (node: Node) => void, + onDragStop: (node: Node) => void, + onClick: (node: Node) => void, isDragging: boolean, setDragging: (isDragging: boolean) => void, id: ElementId, @@ -68,15 +70,19 @@ const onStop = ( position: XYPosition, data: any ): void => { - if (isDragging) { - setDragging(false); - onNodeDragStop({ - id, - type, - position, - data, - } as Node); + const node = { + id, + type, + position, + data, + } as Node; + + if (!isDragging) { + return onClick(node); } + + setDragging(false); + onDragStop(node); }; export default (NodeComponent: ComponentType) => { @@ -90,6 +96,7 @@ export default (NodeComponent: ComponentType) => { yPos, selected, onClick, + onNodeDragStart, onNodeDragStop, style, isInteractive, @@ -139,9 +146,9 @@ export default (NodeComponent: ComponentType) => { return ( onStart(evt as MouseEvent, onClick, id, type, data, setOffset, transform, position)} + onStart={(evt) => onStart(evt as MouseEvent, onNodeDragStart, id, type, data, setOffset, transform, position)} onDrag={(evt) => onDrag(evt as MouseEvent, setDragging, id, offset, transform)} - onStop={() => onStop(onNodeDragStop, isDragging, setDragging, id, type, position, data)} + onStop={() => onStop(onNodeDragStop, onClick, isDragging, setDragging, id, type, position, data)} scale={transform[2]} disabled={!isInteractive} cancel=".nodrag" diff --git a/src/components/NodesSelection/index.tsx b/src/components/NodesSelection/index.tsx index 9390aa01..6684b38b 100644 --- a/src/components/NodesSelection/index.tsx +++ b/src/components/NodesSelection/index.tsx @@ -50,9 +50,11 @@ export default memo(() => { }; const offsetX: number = scaledClient.x - position.x - tx; const offsetY: number = scaledClient.y - position.y - ty; - const selectedNodes = (state.selectedElements.filter(isNode) as Node[]).map( - (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node - ); + const selectedNodes = state.selectedElements + ? (state.selectedElements.filter(isNode) as Node[]).map( + (selectedNode) => state.nodes.find((node) => node.id === selectedNode.id)! as Node + ) + : []; const nextStartPositions = getStartPositions(selectedNodes); @@ -68,14 +70,16 @@ export default memo(() => { y: evt.clientY / tScale, }; - (state.selectedElements.filter(isNode) as Node[]).forEach((node) => { - const pos: XYPosition = { - x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, - y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, - }; + if (state.selectedElements) { + (state.selectedElements.filter(isNode) as Node[]).forEach((node) => { + const pos: XYPosition = { + x: startPositions[node.id].x + scaledClient.x - position.x - offset.x - tx, + y: startPositions[node.id].y + scaledClient.y - position.y - offset.y - ty, + }; - updateNodePos({ id: node.id, pos }); - }); + updateNodePos({ id: node.id, pos }); + }); + } }; return ( diff --git a/src/components/SelectionListener/index.ts b/src/components/SelectionListener/index.ts new file mode 100644 index 00000000..64c0e961 --- /dev/null +++ b/src/components/SelectionListener/index.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; + +import { Elements } from '../../types'; +import { useStoreState } from '../../store/hooks'; + +interface SelectionListenerProps { + onSelectionChange: (elements: Elements | null) => void; +} + +// This is just a helper component for calling the onSelectionChange listener. +// As soon as easy-peasy has implemented the effectOn hook, we can remove this compeonent +// and use the hook instead. https://github.com/ctrlplusb/easy-peasy/pull/459 + +export default ({ onSelectionChange }: SelectionListenerProps) => { + const selectedElements = useStoreState((s) => s.selectedElements); + + useEffect(() => { + onSelectionChange(selectedElements); + }, [selectedElements]); + + return null; +}; diff --git a/src/container/EdgeRenderer/index.tsx b/src/container/EdgeRenderer/index.tsx index 917182e0..351b3b34 100644 --- a/src/container/EdgeRenderer/index.tsx +++ b/src/container/EdgeRenderer/index.tsx @@ -117,7 +117,7 @@ function renderEdge( edge: Edge, props: EdgeRendererProps, nodes: Node[], - selectedElements: Elements, + selectedElements: Elements | null, isInteractive: boolean ) { const [sourceId, sourceHandleId] = edge.source.split('__'); @@ -154,9 +154,9 @@ function renderEdge( targetPosition ); - const isSelected = (selectedElements as Edge[]).some( - (elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId - ); + const isSelected = selectedElements + ? (selectedElements as Edge[]).some((elm) => isEdge(elm) && elm.source === sourceId && elm.target === targetId) + : false; return ( void; onElementsRemove: (elements: Elements) => void; + onNodeDragStart: (node: Node) => void; onNodeDragStop: (node: Node) => void; onConnect: (connection: Connection | Edge) => void; onLoad: OnLoadFunc; @@ -46,6 +47,7 @@ const GraphView = memo( onMove, onLoad, onElementClick, + onNodeDragStart, onNodeDragStop, connectionLineType, connectionLineStyle, @@ -140,6 +142,7 @@ const GraphView = memo( nodeTypes={nodeTypes} onElementClick={onElementClick} onNodeDragStop={onNodeDragStop} + onNodeDragStart={onNodeDragStart} onlyRenderVisibleNodes={onlyRenderVisibleNodes} /> void; + onNodeDragStart: (node: Node) => void; onNodeDragStop: (node: Node) => void; onlyRenderVisibleNodes?: boolean; } @@ -15,7 +16,7 @@ function renderNode( node: Node, props: NodeRendererProps, transform: Transform, - selectedElements: Elements, + selectedElements: Elements | null, isInteractive: boolean ) { const nodeType = node.type || 'default'; @@ -24,7 +25,7 @@ function renderNode( console.warn(`No node type found for type "${nodeType}". Using fallback type "default".`); } - const isSelected = selectedElements.some(({ id }) => id === node.id); + const isSelected = selectedElements ? selectedElements.some(({ id }) => id === node.id) : false; return ( , 'on elements: Elements; onElementClick: (element: Node | Edge) => void; onElementsRemove: (elements: Elements) => void; + onNodeDragStart: (node: Node) => void; onNodeDragStop: (node: Node) => void; onConnect: (connection: Edge | Connection) => void; onLoad: OnLoadFunc; onMove: () => void; + onSelectionChange: (elements: Elements | null) => void; nodeTypes: NodeTypesType; edgeTypes: EdgeTypesType; connectionLineType: string; @@ -59,7 +62,9 @@ const ReactFlow = ({ onMove, onElementsRemove, onConnect, + onNodeDragStart, onNodeDragStop, + onSelectionChange, connectionLineType, connectionLineStyle, deleteKeyCode, @@ -83,6 +88,7 @@ const ReactFlow = ({ onLoad={onLoad} onMove={onMove} onElementClick={onElementClick} + onNodeDragStart={onNodeDragStart} onNodeDragStop={onNodeDragStop} nodeTypes={nodeTypesParsed} edgeTypes={edgeTypesParsed} @@ -102,6 +108,7 @@ const ReactFlow = ({ onlyRenderVisibleNodes={onlyRenderVisibleNodes} isInteractive={isInteractive} /> + {onSelectionChange && } {children} @@ -113,6 +120,7 @@ ReactFlow.displayName = 'ReactFlow'; ReactFlow.defaultProps = { onElementClick: () => {}, onElementsRemove: () => {}, + onNodeDragStart: () => {}, onNodeDragStop: () => {}, onConnect: () => {}, onLoad: () => {}, @@ -139,7 +147,6 @@ ReactFlow.defaultProps = { snapGrid: [16, 16], onlyRenderVisibleNodes: true, isInteractive: true, - className: '', }; export default ReactFlow; diff --git a/src/hooks/useGlobalKeyHandler.ts b/src/hooks/useGlobalKeyHandler.ts index d03f3209..48ca0e8d 100644 --- a/src/hooks/useGlobalKeyHandler.ts +++ b/src/hooks/useGlobalKeyHandler.ts @@ -11,22 +11,19 @@ interface HookParams { } export default ({ deleteKeyCode, onElementsRemove }: HookParams): void => { - const state = useStoreState(s => ({ + const state = useStoreState((s) => ({ selectedElements: s.selectedElements, edges: s.edges, })); - const setNodesSelection = useStoreActions(a => a.setNodesSelection); + const setNodesSelection = useStoreActions((a) => a.setNodesSelection); const deleteKeyPressed = useKeyPress(deleteKeyCode); useEffect(() => { - if (deleteKeyPressed && state.selectedElements.length) { + if (deleteKeyPressed && state.selectedElements) { let elementsToRemove = state.selectedElements; // we also want to remove the edges if only one node is selected - if ( - state.selectedElements.length === 1 && - !isEdge(state.selectedElements[0]) - ) { + if (state.selectedElements.length === 1 && !isEdge(state.selectedElements[0])) { const node = (state.selectedElements[0] as unknown) as Node; const connectedEdges = getConnectedEdges([node], state.edges); elementsToRemove = [...state.selectedElements, ...connectedEdges]; diff --git a/src/store/index.ts b/src/store/index.ts index 4ecf4bab..18e99bab 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -56,7 +56,7 @@ export interface StoreModel { transform: Transform; nodes: Node[]; edges: Edge[]; - selectedElements: Elements; + selectedElements: Elements | null; selectedNodesBbox: Rect; d3Zoom: ZoomBehavior | null; @@ -95,8 +95,6 @@ export interface StoreModel { setSelectedElements: Action; - updateSelection: Action; - updateTransform: Action; updateSize: Action; @@ -122,7 +120,7 @@ const storeModel: StoreModel = { transform: [0, 0, 1], nodes: [], edges: [], - selectedElements: [], + selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, d3Zoom: null, @@ -250,8 +248,11 @@ const storeModel: StoreModel = { const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); state.selection = nextRect; - state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements; state.userSelectionRect = nextRect; + + if (selectedElementsUpdated) { + state.selectedElements = nextSelectedElements.length > 0 ? nextSelectedElements : null; + } }), unsetUserSelection: action((state) => { @@ -261,7 +262,7 @@ const storeModel: StoreModel = { state.selectionActive = false; state.userSelectionRect = { ...state.userSelectionRect, draw: false }; state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -283,7 +284,7 @@ const storeModel: StoreModel = { setNodesSelection: action((state, { isActive, selection }) => { if (!isActive || typeof selection === 'undefined') { state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -291,7 +292,7 @@ const storeModel: StoreModel = { if (!selectedNodes.length) { state.nodesSelectionActive = false; - state.selectedElements = []; + state.selectedElements = null; return; } @@ -311,17 +312,6 @@ const storeModel: StoreModel = { state.selectedElements = selectedElements; }), - updateSelection: action((state, selection) => { - const selectedNodes = getNodesInside(state.nodes, selection, state.transform); - const selectedEdges = getConnectedEdges(selectedNodes, state.edges); - - const nextSelectedElements = [...selectedNodes, ...selectedEdges]; - const selectedElementsUpdated = !isEqual(nextSelectedElements, state.selectedElements); - - state.selection = selection; - state.selectedElements = selectedElementsUpdated ? nextSelectedElements : state.selectedElements; - }), - updateTransform: action((state, transform) => { state.transform = [transform.x, transform.y, transform.k]; }), diff --git a/src/types/index.ts b/src/types/index.ts index 289121de..4773d6fa 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -123,6 +123,7 @@ export interface WrapNodeProps { yPos: number; isInteractive: boolean; onClick: (node: Node) => void | undefined; + onNodeDragStart: (node: Node) => void; onNodeDragStop: (node: Node) => void; style?: CSSProperties; sourcePosition?: Position;