diff --git a/.gitignore b/.gitignore index d1b1f57..8e59901 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ dist es demo/demo-compiled.js +demo/webfonts/* node_modules .DS_Store diff --git a/.npmignore b/.npmignore index eb14221..f2f25ec 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,5 @@ demo/demo.js +demo/webfonts/* node_modules src/components src/index.js diff --git a/demo/demo.js b/demo/demo.js index 5ff738e..58a80d0 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import _ from 'underscore'; import url from 'url'; import { default as packageJSON } from './../package.json'; +import '@fortawesome/fontawesome-free/css/all.css'; // Loaded on index.html, defined as an external in webpack.config.demo.js import Graph, { GraphParser } from 'react-workflow-viz'; @@ -180,7 +181,7 @@ class DemoApp extends Component { onChange={this.handleParsingOptChange} onChangeBasicIO={this.handleChangeBasicIO} /> - + ); diff --git a/package-lock.json b/package-lock.json index bd4c65f..72ca252 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@hms-dbmi-bgm/react-workflow-viz", - "version": "0.1.11", + "version": "0.2.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@hms-dbmi-bgm/react-workflow-viz", - "version": "0.1.11", + "version": "0.2.0-beta.1", "license": "MIT", "dependencies": { "memoize-one": "^5.1.1", @@ -29,14 +29,17 @@ "@babel/preset-react": "^7.16.7", "@babel/register": "^7.17.0", "@babel/runtime": "^7.17.8", + "@fortawesome/fontawesome-free": ">=6.6.0", "babel-jest": "^27.5.1", "babel-loader": "^8.2.3", "babel-plugin-minify-dead-code-elimination": "^0.5.1", + "css-loader": "^3.6.0", "d3": ">=5.9.0", "eslint": "^8.10.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.29.2", "fancy-log": "^1.3.3", + "file-loader": "^4.3.0", "gulp": "^4.0.0", "http-server": "^0.12.1", "jasmine": "^3.5.0", @@ -48,7 +51,9 @@ "sass": "^1.39.0", "source-map-support": "^0.5.19", "string-replace-loader": "^2.3.0", + "style-loader": "^1.3.0", "terser-webpack-plugin": "^4.2.3", + "url-loader": "^2.3.0", "webpack": "^4.46.0" }, "peerDependencies": { @@ -1975,6 +1980,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -4412,6 +4426,75 @@ "node": "*" } }, + "node_modules/css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/css-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/css-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -5987,6 +6070,48 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "webpack": "^4.0.0" + } + }, + "node_modules/file-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/file-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -7181,6 +7306,18 @@ "node": ">=0.10.0" } }, + "node_modules/icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "dependencies": { + "postcss": "^7.0.14" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -9599,6 +9736,107 @@ "node": ">=0.10.0" } }, + "node_modules/postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "dependencies": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "dependencies": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "dependencies": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "dependencies": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postcss/node_modules/picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "node_modules/postcss/node_modules/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, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11083,6 +11321,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -11825,6 +12083,67 @@ "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", "dev": true }, + "node_modules/url-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", + "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", + "dev": true, + "dependencies": { + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" + }, + "engines": { + "node": ">= 8.9.0" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true + } + } + }, + "node_modules/url-loader/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/url-loader/node_modules/loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/url-loader/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/url/node_modules/punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -14200,6 +14519,12 @@ } } }, + "@fortawesome/fontawesome-free": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "dev": true + }, "@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -16189,6 +16514,55 @@ "randomfill": "^1.0.3" } }, + "css-loader": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-3.6.0.tgz", + "integrity": "sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "cssesc": "^3.0.0", + "icss-utils": "^4.1.1", + "loader-utils": "^1.2.3", + "normalize-path": "^3.0.0", + "postcss": "^7.0.32", + "postcss-modules-extract-imports": "^2.0.0", + "postcss-modules-local-by-default": "^3.0.2", + "postcss-modules-scope": "^2.2.0", + "postcss-modules-values": "^3.0.0", + "postcss-value-parser": "^4.1.0", + "schema-utils": "^2.7.0", + "semver": "^6.3.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, "csstype": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.11.tgz", @@ -17436,6 +17810,38 @@ "flat-cache": "^3.0.4" } }, + "file-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-4.3.0.tgz", + "integrity": "sha512-aKrYPYjF1yG3oX0kWRrqrSMfgftm7oJW5M+m4owoldH5C51C0RkIwB++JbRvEW3IU6/ZG5n8UvEcdgwOt2UOWA==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "schema-utils": "^2.5.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -18382,6 +18788,15 @@ "safer-buffer": ">= 2.1.2 < 3.0.0" } }, + "icss-utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", + "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", + "dev": true, + "requires": { + "postcss": "^7.0.14" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -20273,6 +20688,87 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", + "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", + "dev": true, + "requires": { + "picocolors": "^0.2.1", + "source-map": "^0.6.1" + }, + "dependencies": { + "picocolors": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", + "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", + "dev": true + }, + "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 + } + } + }, + "postcss-modules-extract-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", + "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", + "dev": true, + "requires": { + "postcss": "^7.0.5" + } + }, + "postcss-modules-local-by-default": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-3.0.3.tgz", + "integrity": "sha512-e3xDq+LotiGesympRlKNgaJ0PCzoUIdpH0dj47iWAui/kyTgh3CiAr1qP54uodmJhl6p9rN6BoNcdEDVJx9RDw==", + "dev": true, + "requires": { + "icss-utils": "^4.1.1", + "postcss": "^7.0.32", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", + "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", + "dev": true, + "requires": { + "postcss": "^7.0.6", + "postcss-selector-parser": "^6.0.0" + } + }, + "postcss-modules-values": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-3.0.0.tgz", + "integrity": "sha512-1//E5jCBrZ9DmRX+zCtmQtRSV6PV42Ix7Bzj9GbwJceduuf7IqP8MgeTXuRDHOWj2m0VzZD5+roFWDuU8RQjcg==", + "dev": true, + "requires": { + "icss-utils": "^4.0.0", + "postcss": "^7.0.6" + } + }, + "postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -21487,6 +21983,16 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "style-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-1.3.0.tgz", + "integrity": "sha512-V7TCORko8rs9rIqkSrlMfkqA63DfoGBBJmK1kKGCcSi+BWb4cqz0SRsnp4l6rU5iwOEd0/2ePv68SV22VXon4Q==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "schema-utils": "^2.7.0" + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -22067,6 +22573,45 @@ "integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=", "dev": true }, + "url-loader": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-2.3.0.tgz", + "integrity": "sha512-goSdg8VY+7nPZKUEChZSEtW5gjbS66USIGCeSJ1OVOJ7Yfuh/36YxCwMi5HVEJh6mqUYOoy3NJ0vlOMrWsSHog==", + "dev": true, + "requires": { + "loader-utils": "^1.2.3", + "mime": "^2.4.4", + "schema-utils": "^2.5.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", + "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + } + } + }, "use": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", diff --git a/package.json b/package.json index fa6bb62..ba24b20 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hms-dbmi-bgm/react-workflow-viz", - "version": "0.1.11", + "version": "0.2.0-beta.1", "description": "React component for visualizing CWL-like workflows and provenance graphs.", "main": "./dist/react-workflow-viz.min.js", "unpkg": "./dist/react-workflow-viz.min.js", @@ -27,42 +27,47 @@ "author": "4DN DCIC", "license": "MIT", "devDependencies": { + "@babel/cli": "^7.6.0", "@babel/core": "^7.17.5", "@babel/eslint-parser": "^7.5.0", "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-decorators": "^7.17.8", + "@babel/plugin-proposal-export-default-from": "^7.16.7", "@babel/plugin-proposal-object-rest-spread": "^7.17.3", + "@babel/plugin-proposal-pipeline-operator": "^7.17.6", "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@babel/plugin-transform-runtime": "^7.17.0", + "@babel/plugin-transform-template-literals": "^7.16.7", "@babel/preset-env": "^7.16.11", "@babel/preset-react": "^7.16.7", "@babel/register": "^7.17.0", "@babel/runtime": "^7.17.8", - "@babel/plugin-transform-runtime": "^7.17.0", - "@babel/plugin-proposal-export-default-from": "^7.16.7", - "@babel/plugin-transform-template-literals": "^7.16.7", - "@babel/plugin-proposal-decorators": "^7.17.8", - "@babel/plugin-proposal-pipeline-operator": "^7.17.6", - "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@fortawesome/fontawesome-free": ">=6.6.0", "babel-jest": "^27.5.1", "babel-loader": "^8.2.3", "babel-plugin-minify-dead-code-elimination": "^0.5.1", - "@babel/cli": "^7.6.0", + "css-loader": "^3.6.0", "d3": ">=5.9.0", "eslint": "^8.10.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.29.2", "fancy-log": "^1.3.3", + "file-loader": "^4.3.0", "gulp": "^4.0.0", "http-server": "^0.12.1", "jasmine": "^3.5.0", - "sass": "^1.39.0", "plugin-error": "^1.0.1", "prop-types": "^15.7.2", "react": ">=16.14.0", "react-dom": ">=16.14.0", "react-transition-group": "^4.4.1", + "sass": "^1.39.0", "source-map-support": "^0.5.19", "string-replace-loader": "^2.3.0", + "style-loader": "^1.3.0", "terser-webpack-plugin": "^4.2.3", + "url-loader": "^2.3.0", "webpack": "^4.46.0" }, "peerDependencies": { diff --git a/src/components/Edge.js b/src/components/Edge.js index 5639cd8..8bb30a1 100644 --- a/src/components/Edge.js +++ b/src/components/Edge.js @@ -8,6 +8,7 @@ import * as d3 from 'd3'; import Node from './Node'; import { traceNodePathAndRun } from './parsing-functions'; +import { roundScaled } from '../utilities'; export const pathDimensionFunctions = { @@ -428,10 +429,16 @@ export default class Edge extends React.Component { generatePathDimension(startPtOverride = null, endPtOverride = null, edgeVerticesOverride = null){ const { - edgeStyle, startX, startY, endX, endY, columnWidth, - curveRadius, columnSpacing, rowSpacing, nodeEdgeLedgeWidths, - edge: { vertices: customEdgeVertices = null } + edgeStyle, startX, startY, endX, endY, curveRadius, + scale = 1, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing, + nodeEdgeLedgeWidths, edge: { vertices: customEdgeVertices = null } } = this.props; + + // scaling + const columnWidth = roundScaled(propColumnWidth, scale); + const columnSpacing = roundScaled(propColumnSpacing, scale); + const rowSpacing = roundScaled(propRowSpacing, scale); + const { startOffset, endOffset } = this.getPathOffsets(); const startPt = { diff --git a/src/components/EdgesLayer.js b/src/components/EdgesLayer.js index 8de8462..d943307 100644 --- a/src/components/EdgesLayer.js +++ b/src/components/EdgesLayer.js @@ -8,8 +8,7 @@ import { TransitionGroup, Transition } from 'react-transition-group'; import { path as d3Path } from 'd3'; import Edge from './Edge'; - - +import { roundScaled } from '../utilities'; @@ -410,10 +409,22 @@ export default class EdgesLayer extends React.PureComponent { */ render(){ const { - outerHeight, innerWidth, innerMargin, width, edges: origEdges, nodes, + outerHeight, innerWidth, innerHeight, width, edges: origEdges, nodes, selectedNode, isNodeDisabled, contentWidth, - columnWidth, columnSpacing, rowSpacing, innerHeight + scale = 1, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing, innerMargin: propInnerMargin } = this.props; + + // scaling + const columnWidth = roundScaled(propColumnWidth, scale); + const columnSpacing = roundScaled(propColumnSpacing, scale); + const rowSpacing = roundScaled(propRowSpacing, scale); + const innerMargin = { + top: roundScaled(propInnerMargin.top, scale), + right: roundScaled(propInnerMargin.right, scale), + bottom: roundScaled(propInnerMargin.bottom, scale), + left: roundScaled(propInnerMargin.left, scale), + }; + const { edges, horizontalSegments diff --git a/src/components/Graph.js b/src/components/Graph.js index 6d9c4d9..38f6b13 100644 --- a/src/components/Graph.js +++ b/src/components/Graph.js @@ -12,6 +12,8 @@ import NodesLayer from './NodesLayer'; import EdgesLayer from './EdgesLayer'; import { DefaultDetailPane } from './DefaultDetailPane'; import { DefaultNodeElement } from './Node'; +import { ScaleController, ScaleControls } from './ScaleController'; +import { requestAnimationFrame, cancelAnimationFrame, roundScaled } from '../utilities' import { parseAnalysisSteps, parseBasicIOAnalysisSteps } from './parsing-functions'; @@ -70,7 +72,12 @@ export default class Graph extends React.Component { 'capacity' : PropTypes.string })).isRequired, 'nodeTitle' : PropTypes.func, - 'rowSpacingType' : PropTypes.oneOf([ 'compact', 'wide', 'stacked' ]) + 'rowSpacingType' : PropTypes.oneOf([ 'compact', 'wide', 'stacked' ]), + //scale + 'showZoomControls': PropTypes.bool, + 'scale': PropTypes.number, + 'minScale': PropTypes.number, + 'maxScale': PropTypes.number }; static defaultProps = { @@ -100,7 +107,12 @@ export default class Graph extends React.Component { return false; }, 'nodeClassName' : function(node){ return ''; }, - 'nodeEdgeLedgeWidths' : [3,5] + 'nodeEdgeLedgeWidths' : [3,5], + //scale + 'showZoomControls': true, + 'scale': 1, + 'minScale': 0.50, + 'maxScale': 1.50 }; static getHeightFromNodes(nodes, nodesPreSortFxn, rowSpacing){ @@ -150,7 +162,6 @@ export default class Graph extends React.Component { columnSpacing = 56, isNodeCurrentContext = false ){ - /** Vertically centers a single node within a column */ function centerNode(n){ n.y = (contentHeight / 2) + innerMargin.top; @@ -181,14 +192,14 @@ export default class Graph extends React.Component { else { var padding = Math.max(0, contentHeight - ((countInCol - 1) * rowSpacing)) / 2; _.forEach(nodesInColumn, function(nodeInCol, idx){ - nodeInCol.y = ((idx + 0) * rowSpacing) + (innerMargin.top) + padding; + nodeInCol.y = ((idx + 0) * rowSpacing) + innerMargin.top + padding; nodeInCol.nodesInColumn = countInCol; }); } } else if (rowSpacingType === 'stacked') { _.forEach(nodesInColumn, function(nodeInCol, idx){ if (!nodeInCol) return; - nodeInCol.y = (rowSpacing * idx) + innerMargin.top; //num + (this.props.innerMargin.top + verticalMargin); + nodeInCol.y = (rowSpacing * idx) + innerMargin.top; nodeInCol.nodesInColumn = countInCol; }); } else if (rowSpacingType === 'wide') { @@ -223,7 +234,7 @@ export default class Graph extends React.Component { // Set correct X coordinate on each node depending on column and spacing prop. _.forEach(nodesWithCoords, (node, i) => { - node.x = (node.column * (columnWidth + columnSpacing)) + leftOffset; + node.x = node.column * (columnWidth + columnSpacing) + leftOffset; }); // Finally, add boolean `isCurrentContext` flag to each node object if needed. @@ -240,8 +251,11 @@ export default class Graph extends React.Component { super(props); this.height = this.height.bind(this); this.nodesWithCoordinates = this.nodesWithCoordinates.bind(this); + this.setScale = this.setScale.bind(this); this.state = { - 'mounted' : false + mounted: false, + scale: props.scale, + minScale: props.minScale }; this.memoized = { getHeightFromNodes: memoize(Graph.getHeightFromNodes), @@ -252,34 +266,124 @@ export default class Graph extends React.Component { componentDidMount(){ this.setState({ 'mounted' : true }); + + const { + containerWidth, + containerHeight, + minScale: propMinScale, + maxScale, + graphWidth, + graphHeight, + zoomToExtentsOnMount = true + } = this.props; + + // if (typeof containerWidth !== "number" || typeof containerHeight !== "number") { + // // Maybe will become set in componentDidUpdate later. + // return false; + // } + + // if (isNaN(containerWidth) || isNaN(containerHeight)) { + // throw new Error("Width or height is NaN."); + // } + + // const minScaleUnbounded = Math.min( + // (containerWidth / graphWidth), + // (containerHeight / graphHeight) + // ); + + // // Decrease by 5% for scrollbars, etc. + // const nextMinScale = Math.floor( + // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded)) + // * 95) / 100; + // const retObj = { minScale: nextMinScale, mounted: true }; + + // // First time that we've gotten dimensions -- set scale to fit. + // // Also, if nextMinScale > scale or we had scale === minScale before. + // // TODO: Maybe do this onMount also + // if (zoomToExtentsOnMount) { + // retObj.scale = nextMinScale; + // } + // requestAnimationFrame(() => { + // this.setState(retObj); + // }); + } + + setScale(scaleToSet, cb){ + this.setState(function( + { minScale: stateMinScale }, + { minScale: propMinScale, maxScale } + ){ + const scale = Math.max( + Math.min( + maxScale, + scaleToSet + ), + stateMinScale || propMinScale + ); + return { scale }; + }, cb); } height() { - const { nodes, nodesPreSortFxn, rowSpacing } = this.props; + const { nodes, nodesPreSortFxn, rowSpacing: propRowSpacing } = this.props; + const { scale } = this.state; + + const rowSpacing = roundScaled(propRowSpacing, scale); return this.memoized.getHeightFromNodes(nodes, nodesPreSortFxn, rowSpacing); } scrollableWidth(){ - const { nodes, columnWidth, columnSpacing, innerMargin } = this.props; + const { nodes, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, innerMargin } = this.props; + const { scale } = this.state; + + const columnWidth = roundScaled(propColumnWidth, scale); + const columnSpacing = roundScaled(propColumnSpacing, scale); return this.memoized.getScrollableWidthFromNodes(nodes, columnWidth, columnSpacing, innerMargin); } nodesWithCoordinates(viewportWidth, contentWidth, contentHeight){ - const { nodes, innerMargin, rowSpacingType, rowSpacing, columnWidth, columnSpacing, isNodeCurrentContext } = this.props; + const { + nodes, innerMargin, + rowSpacingType, rowSpacing: propRowSpacing, columnWidth: propColumnWidth, columnSpacing: propColumnSpacing, + isNodeCurrentContext + } = this.props; + const { scale } = this.state; + + const rowSpacing = roundScaled(propRowSpacing, scale); + const columnWidth = roundScaled(propColumnWidth, scale); + const columnSpacing = roundScaled(propColumnSpacing, scale); + return this.memoized.getNodesWithCoordinates( nodes, viewportWidth, contentWidth, contentHeight, innerMargin, - rowSpacingType, rowSpacing, columnWidth, columnSpacing, isNodeCurrentContext + rowSpacingType, rowSpacing, columnWidth, columnSpacing, + isNodeCurrentContext, scale || 1 ); } render(){ - const { width, innerMargin, edges, minimumHeight } = this.props; - const { mounted } = this.state; + const { + width, innerMargin: propInnerMargin, edges, minimumHeight, + columnSpacing: propColumnSpacing, rowSpacing: propRowSpacing, columnWidth: propColumnWidth, + scale: propScale = 1, maxScale: propMaxScale = 1.1, minScale: propMinScale = 0.9, + showZoomControls + } = this.props; + const { mounted, scale: stateScale } = this.state; + const scale = stateScale || propScale; const innerHeight = this.height(); const contentWidth = this.scrollableWidth(); let innerWidth = width; - if (!mounted){ + const columnSpacing = roundScaled(propColumnSpacing, scale); + const rowSpacing = roundScaled(propRowSpacing, scale); + const columnWidth = roundScaled(propColumnWidth, scale); + const innerMargin = { + top: roundScaled(propInnerMargin.top, scale), + right: roundScaled(propInnerMargin.right, scale), + bottom: roundScaled(propInnerMargin.bottom, scale), + left: roundScaled(propInnerMargin.left, scale), + }; + + if (!mounted) { return (
 
@@ -302,15 +406,23 @@ export default class Graph extends React.Component { graphHeight += (spacerCount * this.props.columnSpacing); } */ - + let scaleControls = null; + if (showZoomControls && typeof this.setScale === "function") { + const scaleProps = { scale, minScale: this.state.minScale || propMinScale, maxScale: propMaxScale, setScale: this.setScale }; + scaleControls = ; + } + return (
+ {scaleControls} + {..._.pick(this.props, 'pathArrows', 'href', 'onNodeClick', 'renderDetailPane')}> - - + +
diff --git a/src/components/Node.js b/src/components/Node.js index 61ca5eb..93401fc 100644 --- a/src/components/Node.js +++ b/src/components/Node.js @@ -5,6 +5,7 @@ import PropTypes from 'prop-types'; import memoize from 'memoize-one'; import _ from 'underscore'; import { traceNodePathAndRun } from './parsing-functions'; +import { roundScaled } from '../utilities'; /** @todo separate methods out into functional components */ @@ -72,7 +73,7 @@ export class DefaultNodeElement extends React.PureComponent { render(){ const { node, title, columnWidth } = this.props; const style = node.nodeType === 'input' || node.nodeType === 'output' ? - { width : (columnWidth || 100) } + { width : columnWidth || 100 } : null; return (
1 && lastActiveContextNode === node)) @@ -215,21 +219,21 @@ export default class Node extends React.Component { return false; } - render(){ - var { node, isNodeDisabled, className, columnWidth, renderNodeElement, selectedNode, forwardedRef } = this.props, - disabled = typeof node.disabled !== 'undefined' ? node.disabled : this.isDisabled(node, isNodeDisabled), - isCurrentContext = typeof node.isCurrentContext !== 'undefined' ? node.isCurrentContext : null, - classNameList = ["node", "node-type-" + node.nodeType], - selected = (!disabled && Node.isSelected(node, selectedNode)) || false, - related = (!disabled && this.isRelated(node, selectedNode)) || false, - inSelectionPath = selected || (!disabled && this.isInSelectionPath(node, selectedNode)) || false; - - if (disabled) classNameList.push('disabled'); - if (isCurrentContext) classNameList.push('current-context'); - if (typeof className === 'function') classNameList.push(className(node)); - else if (typeof className === 'string' ) classNameList.push(className); - - var visibleNodeProps = _.extend( + render() { + const { node, isNodeDisabled, className, columnWidth, renderNodeElement, selectedNode, forwardedRef, scale = 1 } = this.props; + const disabled = typeof node.disabled !== 'undefined' ? node.disabled : this.isDisabled(node, isNodeDisabled); + const isCurrentContext = typeof node.isCurrentContext !== 'undefined' ? node.isCurrentContext : null; + const classNameList = ["node", "node-type-" + node.nodeType]; + const selected = (!disabled && Node.isSelected(node, selectedNode)) || false; + const related = (!disabled && this.isRelated(node, selectedNode)) || false; + const inSelectionPath = selected || (!disabled && this.isInSelectionPath(node, selectedNode)) || false; + + if (disabled) classNameList.push('disabled'); + if (isCurrentContext) classNameList.push('current-context'); + if (typeof className === 'function') classNameList.push(className(node)); + else if (typeof className === 'string') classNameList.push(className); + + const visibleNodeProps = _.extend( _.omit(this.props, 'children', 'onMouseEnter', 'onMouseLeave', 'onClick', 'className', 'nodeElement'), { disabled, selected, related, isCurrentContext, inSelectionPath } ); @@ -240,10 +244,11 @@ export default class Node extends React.Component { data-node-selected={selected} data-node-in-selection-path={inSelectionPath} data-node-related={related} data-node-type-detail={node.ioType && node.ioType.toLowerCase()} data-node-column={node.column} style={{ - 'top' : node.y, - 'left' : node.x, - 'width' : columnWidth || 100, - 'zIndex' : 2 + (node.indexInColumn || 0) + top: node.y, + left: node.x + ((scale - 1) * columnWidth) / 2, + width: columnWidth || 100, + zIndex: 2 + (node.indexInColumn || 0), + transform: `scale(${scale})` }} ref={forwardedRef}>
diff --git a/src/components/NodesLayer.js b/src/components/NodesLayer.js index e8cae9d..e8514d0 100644 --- a/src/components/NodesLayer.js +++ b/src/components/NodesLayer.js @@ -6,6 +6,7 @@ import memoize from 'memoize-one'; import { TransitionGroup, CSSTransition } from 'react-transition-group'; import Node from './Node'; +import { roundScaled } from '../utilities'; export default class NodesLayer extends React.PureComponent { @@ -73,9 +74,17 @@ export default class NodesLayer extends React.PureComponent { } render(){ - var { innerMargin, innerWidth, outerHeight, contentWidth } = this.props, - fullWidth = innerWidth + innerMargin.left + innerMargin.right, - layerStyle = { 'width' : Math.max(contentWidth, fullWidth), 'height' : outerHeight }; + const { innerMargin: propInnerMargin, innerWidth, outerHeight, contentWidth, scale } = this.props; + + const innerMargin = { + top: roundScaled(propInnerMargin.top, scale), + right: roundScaled(propInnerMargin.right, scale), + bottom: roundScaled(propInnerMargin.bottom, scale), + left: roundScaled(propInnerMargin.left, scale), + }; + + const fullWidth = innerWidth + innerMargin.left + innerMargin.right; + const layerStyle = { 'width': Math.max(contentWidth, fullWidth), 'height': outerHeight }; return (
diff --git a/src/components/ScaleController.js b/src/components/ScaleController.js new file mode 100644 index 0000000..7782b07 --- /dev/null +++ b/src/components/ScaleController.js @@ -0,0 +1,394 @@ +'use strict'; + +import React from 'react'; +import { requestAnimationFrame, cancelAnimationFrame, roundScaled } from '../utilities' + + +/** + * @deprecated Moved most of the functionality into Graph + */ +export class ScaleController extends React.PureComponent { + + static defaultProps = { + minScale: 0.01, + maxScale: 1.25, + initialScale: 1, + zoomToExtentsOnMount: true + }; + + constructor(props){ + super(props); + this.handleWheelMove = this.handleWheelMove.bind(this); + this.handleInnerContainerMounted = this.handleInnerContainerMounted.bind(this); + this.handleInnerContainerWillUnmount = this.handleInnerContainerWillUnmount.bind(this); + this.innerElemReference = null; + } + + componentDidMount(){ + // const { + // containerWidth, + // containerHeight, + // minScale: propMinScale, + // maxScale, + // graphWidth, + // graphHeight, + // zoomToExtentsOnMount = true + // } = this.props; + + // if (typeof containerWidth !== "number" || typeof containerHeight !== "number") { + // // Maybe will become set in componentDidUpdate later. + // return false; + // } + + // if (isNaN(containerWidth) || isNaN(containerHeight)) { + // throw new Error("Width or height is NaN."); + // } + + // const minScaleUnbounded = Math.min( + // (containerWidth / graphWidth), + // (containerHeight / graphHeight) + // ); + + // // Decrease by 5% for scrollbars, etc. + // const nextMinScale = Math.floor( + // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded)) + // * 95) / 100; + // const retObj = { minScale: nextMinScale }; + + // // First time that we've gotten dimensions -- set scale to fit. + // // Also, if nextMinScale > scale or we had scale === minScale before. + // // TODO: Maybe do this onMount also + // if (zoomToExtentsOnMount) { + // retObj.scale = nextMinScale; + // } + // requestAnimationFrame(() => { + // this.setState(retObj); + // }); + } + + componentDidUpdate(pastProps, pastState){ + const { + enableMouseWheelZoom, + enablePinchZoom, + zoomToExtentsOnMount, + containerWidth, + containerHeight, + graphWidth, + graphHeight, + minScale: propMinScale, + maxScale + } = this.props; + // const { scale, minScale: stateMinScale } = this.state; + // const { + // enableMouseWheelZoom: pastWheelEnabled, + // enablePinchZoom: pastPinchEnabled, + // containerWidth: pastWidth, + // containerHeight: pastHeight, + // graphWidth: pastGraphWidth, + // graphHeight: pastGraphHeight + // } = pastProps; + + // // Remove or attach listeners if needed. + // const listenNow = (enableMouseWheelZoom || enablePinchZoom); + // const listenBefore = (pastWheelEnabled || pastPinchEnabled); + + // if (this.innerElemReference){ + // if (listenNow && !listenBefore){ + // this.innerElemReference.addEventListener("wheel", this.handleWheelMove, { "passive": false, "capture": true }); + // } else if (!listenNow && listenBefore){ + // this.innerElemReference.removeEventListener("wheel", this.handleWheelMove); + // } + // } + + // // Update minScale (& possibly scale itself) + // // We read `pastState` here before updating set + // // vs using functional updater because want to avoid + // // React's state change queuing mechanisms / reading + // // most accurate prev value not important. + // if (containerWidth !== pastWidth || + // containerHeight !== pastHeight || + // graphWidth !== pastGraphWidth || + // graphHeight !== pastGraphHeight + // ){ + // const minScaleUnbounded = Math.min( + // (containerWidth / graphWidth), + // (containerHeight / graphHeight) + // ); + + // // Decrease by 5% for scrollbars, etc. + // const nextMinScale = Math.floor( + // Math.min(1, maxScale, Math.max(propMinScale, minScaleUnbounded)) + // * 95) / 100; + // const retObj = { minScale: nextMinScale }; + + // // First time that we've gotten dimensions -- set scale to fit. + // // Also, if nextMinScale > scale or we had scale === minScale before. + // // TODO: Maybe do this onMount also + // if (nextMinScale > scale || stateMinScale === scale || (zoomToExtentsOnMount && (!pastHeight || !pastWidth))) { + // retObj.scale = nextMinScale; + // } + // requestAnimationFrame(() => { + // this.setState(retObj); + // }); + // } + } + + + /** + * If `enableMouseWheelZoom` or `enablePinchZoom` are enabled, will + * zoom in response to mouse wheel events or ctrl+mousewheel & touchpad + * pinch events, respectively. + */ + handleWheelMove(evt){ + const { deltaY, deltaX, ctrlKey } = evt; + const { enableMouseWheelZoom, enablePinchZoom, scale, setScale } = this.props; + + if (!enableMouseWheelZoom && !enablePinchZoom) { + return false; + } + + // Chrome registers touchpad-based pinching as scrollwheel with ctrlKey. + if (enablePinchZoom && !enableMouseWheelZoom && !ctrlKey) { + return false; + } + + if (!enablePinchZoom && enableMouseWheelZoom && ctrlKey) { + return false; + } + + if (enableMouseWheelZoom && Math.abs(deltaX) > 0){ + // Not perfect -- + // Make sure is mousewheel and not bidirectional touchpad, + // for which we might still wanna allow left/right movement. + return false; + } + + evt.preventDefault(); + evt.stopPropagation(); + + // `ctrlKey=true` implies 2nd ctrl key being pressed -or- touchpad pinching + const deltaMultiplier = ctrlKey ? 0.01 : 0.0005; + + // React uses own state change queuing system, which guessing + // gets bypassed w. raf, so below line might work better, since + // `state.scale` unchanged.. (vs. functional updater) + setScale(scale - (deltaY * deltaMultiplier)); + } + + handleInnerContainerMounted(innerElem){ + const { onMount, enableMouseWheelZoom, enablePinchZoom } = this.props; + if (typeof onMount === "function"){ + onMount(...arguments); + } + this.innerElemReference = innerElem; + // We need to listen to `wheel` events directly (not thru React) + // as react doesn't handle these (it seems / afaik). + if (enableMouseWheelZoom || enablePinchZoom) { + // Chrome & some other browsers set passive:true by default. + innerElem.addEventListener("wheel", this.handleWheelMove, { "passive": false, "capture": true }); + } + } + + handleInnerContainerWillUnmount(innerElem){ + const { onWillUnmount } = this.props; + if (typeof onMount === "function"){ + onWillUnmount(...arguments); + } + if (this.innerElemReference === null) { + console.error("No inner elem, exiting"); + return; + } + if (this.innerElemReference !== innerElem) { + throw new Error("Inner elem is different, exiting"); + } + this.innerElemReference.removeEventListener("wheel", this.handleWheelMove); + this.innerElemReference = null; + } + + render(){ + const { children, initialScale = null, scale, setScale, minScale, ...passProps } = this.props; + const childProps = { + ...passProps, + scale: scale || 1, + minScale: minScale, + setScale: setScale, + onMount: this.handleInnerContainerMounted, + onWillUnmount: this.handleInnerContainerWillUnmount + }; + return React.Children.map(children, (child) => child && React.cloneElement(child, childProps) ); + } + +} + +/** + * Component which provides UI for adjusting scale and + * calls `ScaleController`'s `setScale` function. + * + * Uses `requestAnimationFrame` (`raf`) for smooth and performant + * zooming transitions. + * + * To assert whether `raf` makes a meaningful difference, try to comment out + * the `raf`-related lines in `onSliderChange` method (except for `setScale(nextVal)`) + * and compare performance/smoothness :-D + * + * React _does_ seem to use requestAnimationFrame under the hood but maybe only + * for batched updates, as animation frames aren't always requested (Chrome dev + * tools > performance > profiling). + * + * We're getting performance gain from using `raf` in onSliderChange potentially + * because we're listening to `SyntheticEvent`s passed in from React element, which + * may be throttled or deferred until after state changes (vs native events). + */ +export class ScaleControls extends React.PureComponent { + + static defaultProps = { + scaleChangeInterval: 15, // milliseconds + scaleChangeUpFactor: 1.025, + scaleChangeDownFactor: 0.975 + }; + + constructor(props){ + super(props); + this.onZoomOutDown = this.onZoomOutDown.bind(this); + this.onZoomOutUp = this.onZoomOutUp.bind(this); + this.onZoomInDown = this.onZoomInDown.bind(this); + this.onZoomInUp = this.onZoomInUp.bind(this); + this.cancelAnimationFrame = this.cancelAnimationFrame.bind(this); + this.onSliderChange = this.onSliderChange.bind(this); + this.state = { + zoomOutPressed: false, + zoomInPressed: false + }; + this.nextAnimationFrame = null; + } + + cancelAnimationFrame(){ + if (this.nextAnimationFrame !== null) { + cancelAnimationFrame(this.nextAnimationFrame); + this.nextAnimationFrame = null; + } + } + + onZoomOutDown(evt){ + evt.preventDefault(); + evt.stopPropagation(); + const { setScale, scaleChangeInterval, scaleChangeDownFactor, scale: initScale } = this.props; + this.setState({ zoomOutPressed: true }, ()=>{ + const start = Date.now(); + //const diff = (scaleChangeDownFactor * initScale) - initScale; + + const performZoom = () => { + const { scale, minScale } = this.props; + if (scale <= minScale){ // Button becomes disabled so `onZoomOutUp` is not guaranteed to be called. + this.setState({ zoomOutPressed: false }); + this.nextAnimationFrame = null; + return; + } + setScale( + //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval)) + initScale * + (scaleChangeDownFactor ** Math.floor((Date.now() - start) / scaleChangeInterval)) + ); + this.nextAnimationFrame = requestAnimationFrame(performZoom); + }; + + this.nextAnimationFrame = requestAnimationFrame(performZoom); + }); + } + + onZoomOutUp(evt){ + evt.preventDefault(); + evt.stopPropagation(); + this.cancelAnimationFrame(); + this.setState({ zoomOutPressed: false }); + } + + onZoomInDown(evt){ + evt.preventDefault(); + evt.stopPropagation(); + const { setScale, scaleChangeInterval, scaleChangeUpFactor, scale: initScale } = this.props; + this.setState({ zoomInPressed: true }, ()=>{ + const start = Date.now(); + //const diff = (scaleChangeUpFactor * initScale) - initScale; + + const performZoom = () => { + const { scale, maxScale } = this.props; + if (scale >= maxScale){ // Button becomes disabled so `onZoomOutUp` is not guaranteed to be called. + this.setState({ zoomInPressed: false }); + this.nextAnimationFrame = null; + return; + } + setScale( + //initScale + (diff * Math.floor((Date.now() - start) / scaleChangeInterval)) + initScale * + (scaleChangeUpFactor ** Math.floor((Date.now() - start) / scaleChangeInterval)) + ); + this.nextAnimationFrame = requestAnimationFrame(performZoom); + }; + + this.nextAnimationFrame = requestAnimationFrame(performZoom); + }); + } + + onZoomInUp(evt){ + evt.preventDefault(); + evt.stopPropagation(); + this.cancelAnimationFrame(); + this.setState({ zoomInPressed: false }); + } + + onSliderChange(evt){ + evt.preventDefault(); + evt.stopPropagation(); + const { setScale } = this.props; + const nextVal = parseFloat(evt.target.value); + this.cancelAnimationFrame(); + this.nextAnimationFrame = requestAnimationFrame(() => { + setScale(nextVal); + this.nextAnimationFrame = null; + }); + } + + render(){ + const { scale, setScale, minScale, maxScale } = this.props; + + if (typeof setScale !== "function" || typeof scale !== "number" || isNaN(scale)) { + return null; + } + + return ( +
+
+ +
+ { Math.round(scale * 100) } + +
+ +
+
+ +
+
+ ); + } +} + +export function scaledStyle(graphHeight, graphWidth, scale){ + return { + width: roundScaled(graphWidth, scale), + height: roundScaled(graphHeight, scale), + transform : "scale3d(" + scale + "," + scale + ",1)" + }; +} diff --git a/src/styles.scss b/src/styles.scss index 47ab005..cc3d179 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -536,7 +536,7 @@ $workflow-node-color-type-global-context: #ffb3b3 !default; } .scroll-container { - + display: flex; position: relative; box-sizing: content-box; transition: @@ -624,4 +624,79 @@ $workflow-node-color-type-global-context: #ffb3b3 !default; @include workflow-edge-layer; } } +} + +.zoom-controls-container { + display: flex; + position: absolute; + z-index: 999; + top: 8px; + left: 50%; + transform: translateX(-50%); + > .zoom-buttons-row { + display: flex; + align-items: flex-start; + > .zoom-btn, + > .zoom-value { + box-sizing: content-box; + text-align: center; + padding: 1px 4px; + height: 25px; + color: #555; + background-color: #fff; + } + > .zoom-btn { + border: 1px solid #999; + width: 24px; + + &.zoom-out { // Left btn + border-radius: 5px 0 0 5px; + border-right-color: #ddd !important; + } + &.zoom-in { // Right btn + border-radius: 0 5px 5px 0; + border-left-color : #ddd !important; + } + + &:disabled { + color: #999; + pointer-events: none; + } + &:hover:not(:disabled), + &:focus:not(:disabled), &:active:not(:disabled) { + color: #000; + border-color: #555; + outline: none; + } + &:focus:not(:disabled), &:active:not(:disabled) { + box-shadow: inset 0px 0px 2px 0px #333; + } + } + > .zoom-value { + border-top: 1px solid #999999; + border-bottom: 1px solid #999999; + width: 36px; + font-size: 0.875em; + line-height: 25px; + white-space: nowrap; + text-align: right; + } + } + > .zoom-slider { + width: 112px; + padding-top: 3px; + padding-left: 3px; + > input[type="range"] { + width: inherit; + // If we want to make it vertical later: + //&[orient="vertical"] { + // writing-mode: bt-lr; /* IE */ + // -webkit-appearance: slider-vertical; /* WebKit */ + // height: 120px; + //} + } + } + + div.state-container { + margin-top: 40px; + } } \ No newline at end of file diff --git a/src/utilities.js b/src/utilities.js new file mode 100644 index 0000000..74ba619 --- /dev/null +++ b/src/utilities.js @@ -0,0 +1,36 @@ +'use strict'; + +import React from 'react'; + +/** + * Helper function for window.requestAnimationFrame. Falls back to browser-prefixed versions if default not available, or falls back to setTimeout with 0ms delay if no requestAnimationFrame available at all. + * + * @param {function} cb - Callback method. + * @returns {undefined|string} Undefined or timeout ID if falling back to setTimeout. + */ +export function requestAnimationFrame(cb){ + if (/*!isServerSide() && */typeof window !== 'undefined'){ + if (typeof window.requestAnimationFrame !== 'undefined') return window.requestAnimationFrame(cb); + if (typeof window.webkitRequestAnimationFrame !== 'undefined') return window.webkitRequestAnimationFrame(cb); + if (typeof window.mozRequestAnimationFrame !== 'undefined') return window.mozRequestAnimationFrame(cb); + } + return setTimeout(cb, 0); // Mock it for old browsers and server-side. +} + +export function cancelAnimationFrame(identifier){ + if (/*!isServerSide() && */typeof window !== 'undefined'){ + if (typeof window.cancelAnimationFrame !== 'undefined') return window.cancelAnimationFrame(identifier); + if (typeof window.webkitCancelAnimationFrame !== 'undefined') return window.webkitCancelAnimationFrame(identifier); + if (typeof window.mozCancelAnimationFrame !== 'undefined') return window.mozCancelAnimationFrame(identifier); + } + return clearTimeout(identifier); // Mock it for old browsers and server-side. +} + +export function roundScaled(value, scale, decimals = 2) { + //shortcut - useful for most cases + if (scale === 1) return value; + + const factor = Math.pow(10, decimals); + const result = value * scale; + return Math.round(result * factor) / factor; +} \ No newline at end of file diff --git a/webpack.config.demo.js b/webpack.config.demo.js index 26b0c8a..f460e63 100644 --- a/webpack.config.demo.js +++ b/webpack.config.demo.js @@ -64,6 +64,22 @@ module.exports = [{ loader: 'babel-loader' } ] + }, + { + test: /\.css$/, + use: ['style-loader', 'css-loader'] + }, + { + test: /\.(woff|woff2|eot|ttf|otf|svg)$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[name].[ext]', + outputPath: 'webfonts/', + } + } + ] } ] },