diff --git a/package.json b/package.json index e603df5d6..46da0f7d8 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ ], "devDependencies": { "@babel/core": "^7.5.0", + "@babel/plugin-proposal-class-properties": "^7.5.0", + "@babel/plugin-syntax-dynamic-import": "^7.2.0", + "@babel/plugin-transform-runtime": "^7.5.0", "@babel/preset-env": "^7.5.0", "@babel/preset-react": "^7.0.0", "lerna": "^3.15.0" diff --git a/packages/_components/.all-contributorsrc b/packages/_components/.all-contributorsrc new file mode 100644 index 000000000..81c149133 --- /dev/null +++ b/packages/_components/.all-contributorsrc @@ -0,0 +1,135 @@ +{ + "projectName": "react-viewerbase", + "projectOwner": "OHIF", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "swederik", + "name": "Erik Ziegler", + "avatar_url": "https://avatars3.githubusercontent.com/u/607793?v=4", + "profile": "https://github.com/swederik", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "dannyrb", + "name": "Danny Brown", + "avatar_url": "https://avatars1.githubusercontent.com/u/5797588?v=4", + "profile": "http://dannyrb.com/", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "galelis", + "name": "Gustavo André Lelis", + "avatar_url": "https://avatars3.githubusercontent.com/u/2378326?v=4", + "profile": "https://github.com/galelis", + "contributions": [ + "code", + "maintenance" + ] + }, + { + "login": "maltempi", + "name": "Thiago Maltempi", + "avatar_url": "https://avatars3.githubusercontent.com/u/7017182?v=4", + "profile": "https://github.com/maltempi", + "contributions": [ + "code" + ] + }, + { + "login": "EsrefDurna", + "name": "Esref Durna", + "avatar_url": "https://avatars0.githubusercontent.com/u/1230575?v=4", + "profile": "https://www.linkedin.com/in/siliconvalleynextgeneration/", + "contributions": [ + "code" + ] + }, + { + "login": "pieper", + "name": "Steve Pieper", + "avatar_url": "https://avatars0.githubusercontent.com/u/126077?v=4", + "profile": "http://www.isomics.com", + "contributions": [ + "code" + ] + }, + { + "login": "biharck", + "name": "Biharck Araujo", + "avatar_url": "https://avatars0.githubusercontent.com/u/1713255?v=4", + "profile": "http://www.biharck.com.br", + "contributions": [ + "code" + ] + }, + { + "login": "rodrigolabs", + "name": "Rodrigo Antinarelli", + "avatar_url": "https://avatars3.githubusercontent.com/u/1905961?v=4", + "profile": "https://rodrigoea.com/", + "contributions": [ + "code" + ] + }, + { + "login": "jfmedeiros1820", + "name": "João Felipe de Medeiros Moreira", + "avatar_url": "https://avatars1.githubusercontent.com/u/2211708?v=4", + "profile": "https://github.com/jfmedeiros1820", + "contributions": [ + "code" + ] + }, + { + "login": "jamesg1", + "name": "James Gosbell", + "avatar_url": "https://avatars3.githubusercontent.com/u/3621147?v=4", + "profile": "https://github.com/jamesg1", + "contributions": [ + "code" + ] + }, + { + "login": "evren217", + "name": "Evren Ozkan", + "avatar_url": "https://avatars1.githubusercontent.com/u/4920551?v=4", + "profile": "https://github.com/evren217", + "contributions": [ + "code" + ] + }, + { + "login": "zsaltzman", + "name": "Zach S.", + "avatar_url": "https://avatars1.githubusercontent.com/u/19156530?v=4", + "profile": "https://github.com/zsaltzman", + "contributions": [ + "code" + ] + }, + { + "login": "muakdogan", + "name": "Mete Ugur Akdogan", + "avatar_url": "https://avatars0.githubusercontent.com/u/19971240?v=4", + "profile": "https://github.com/muakdogan", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} diff --git a/packages/_components/.babelrc b/packages/_components/.babelrc new file mode 100644 index 000000000..3f85000fd --- /dev/null +++ b/packages/_components/.babelrc @@ -0,0 +1,18 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "ie": "11" + } + } + ], + "@babel/preset-react" + ], + "plugins": [ + "inline-react-svg", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-runtime" + ] +} diff --git a/packages/_components/.circleci/config.yml b/packages/_components/.circleci/config.yml new file mode 100644 index 000000000..cf72be118 --- /dev/null +++ b/packages/_components/.circleci/config.yml @@ -0,0 +1,85 @@ +version: 2 + +### ABOUT +# +# This configuration powers our Circleci.io integration +# +# Note: +# Netlify works independently from this configuration to +# create pull request previews and to update `https://react.ohif.org` +### + +defaults: &defaults + working_directory: ~/repo + docker: + - image: circleci/node:10.15.1 + +jobs: + build_and_test: + <<: *defaults + steps: + # Download and cache dependencies + - checkout + - restore_cache: + name: Restore Yarn Package Cache + keys: + # when lock file changes, use increasingly general patterns to restore cache + - yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} + - yarn-packages-v1-{{ .Branch }}- + - yarn-packages-v1- + - run: + name: Install Dependencies + command: yarn install --frozen-lockfile + - save_cache: + name: Save Yarn Package Cache + paths: + - ~/.cache/yarn + key: yarn-packages-v1-{{ .Branch }}-{{ checksum "yarn.lock" }} + + # Build & Test + - run: yarn build + - run: yarn test + + # Persist :+1: + - persist_to_workspace: + root: ~/repo + paths: . + + npm_publish: + <<: *defaults + steps: + - attach_workspace: + at: ~/repo + - run: + name: Avoid hosts unknown for github + command: + mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking + no\n" > ~/.ssh/config + - run: + name: Publish using Semantic Release + command: npx semantic-release + +workflows: + version: 2 + + # PULL REQUESTS + pull_requests: + jobs: + - build_and_test: + filters: + branches: + ignore: + - master + - feature/* + - hotfix/* + + # MERGE TO MASTER + cut_release: + jobs: + - build_and_test: + filters: + branches: + only: master + - npm_publish: + requires: + - build_and_test diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/.editorconfig b/packages/_components/.editorconfig similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/.editorconfig rename to packages/_components/.editorconfig diff --git a/packages/_components/.env b/packages/_components/.env new file mode 100644 index 000000000..0708aefac --- /dev/null +++ b/packages/_components/.env @@ -0,0 +1,11 @@ +#### +## +## We need to pin WebPack's version for docz to work. See this issue: +## https://github.com/pedronauck/docz/issues/704#issuecomment-480295032 +## +## react-scripts checks that dependency versions match before running +## Our top level webpack version causes the preflight check to fail +## This prevents that check +## +#### +SKIP_PREFLIGHT_CHECK=true diff --git a/packages/_components/.eslintrc b/packages/_components/.eslintrc new file mode 100644 index 000000000..7556af95d --- /dev/null +++ b/packages/_components/.eslintrc @@ -0,0 +1,17 @@ +{ + "extends": [ + "react-app", + "eslint:recommended", + "plugin:react/recommended", + "plugin:prettier/recommended" + ], + "parser": "babel-eslint", + "env": { + "jest": true + }, + "settings": { + "react": { + "version": "detect" + } + } +} diff --git a/packages/_components/.gitattributes b/packages/_components/.gitattributes new file mode 100644 index 000000000..fcadb2cf9 --- /dev/null +++ b/packages/_components/.gitattributes @@ -0,0 +1 @@ +* text eol=lf diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md b/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md new file mode 100644 index 000000000..974c3fd37 --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md @@ -0,0 +1,40 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: '' +labels: 'Bug Report :bug:' +assignees: '' + +--- + +**Before Creating an issue** + +- Are you running the latest version? +- Are you reporting to the correct repository? +- Did you search existing issues? + +**Describe the bug** + +*A clear and concise description of what the bug is.* + +**Steps To Reproduce** + +1. [First Step] +2. [Second Step] +3. ... + +``` +Please use code blocks to show formatted errors or code snippets +``` + +**Expected behavior** + +*A clear and concise description of what you expected to happen.* + + +**Environment** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md b/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md new file mode 100644 index 000000000..0231ff905 --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md @@ -0,0 +1,18 @@ +--- +name: "\U0001F680 Feature request" +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +## Awesome, do you have an idea? 😍 + +If you have a **feature request, improvement or idea**, check [our official roadmap](https://github.com/OHIF/react-viewerbase/projects) to see if it is already planned! + +### 👉   [Go to Roadmap](https://github.com/OHIF/react-viewerbase/projects) + +If your feature request isn't there, continue with this issue and we can discuss it 🤟 + +Please include the reasons why you think your change should be made, and any supporting evidence that helps us asses its value and priority. diff --git a/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md b/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md new file mode 100644 index 000000000..e2cd7e5ff --- /dev/null +++ b/packages/_components/.github/ISSUE_TEMPLATE/---support-question.md @@ -0,0 +1,15 @@ +--- +name: "\U0001F917 Support Question" +about: "I have a question \U0001F4AC" +title: '' +labels: question +assignees: '' + +--- + +We are a small team with limited resources. Your question is much more likely to be answered if it is [a good question](https://stackoverflow.com/help/how-to-ask) + +**Description** + + +Questions can often be answered by our documentation. Unable to find an answer in our docs? We'll try to help. In the meantime, if you answer your own question, please respond with the answer here so that others may benefit as well. Better yet, open a PR to expand our docs ^_^ diff --git a/packages/_components/.gitignore b/packages/_components/.gitignore new file mode 100644 index 000000000..65d069f84 --- /dev/null +++ b/packages/_components/.gitignore @@ -0,0 +1,32 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules +# Don't track package-lock if we intend to use yarn.lock +package-lock.json + +# builds +build +dist/ +.rpt2_cache +.docz/ +.yalc +yalc.lock + +# Log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.idea + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Can be removed after we update netlify +# to use default .docz/ output destination +example/ diff --git a/packages/_components/.nvmrc b/packages/_components/.nvmrc new file mode 100644 index 000000000..d16c3a34a --- /dev/null +++ b/packages/_components/.nvmrc @@ -0,0 +1 @@ +10.15.3 \ No newline at end of file diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/.prettierrc b/packages/_components/.prettierrc similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/.prettierrc rename to packages/_components/.prettierrc diff --git a/packages/_components/.vscode/extensions.json b/packages/_components/.vscode/extensions.json new file mode 100644 index 000000000..2280f7c91 --- /dev/null +++ b/packages/_components/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "sysoev.language-stylus", + "silvenon.mdx", + "dbaeumer.vscode-eslint" + ] +} diff --git a/packages/_components/.vscode/settings.json b/packages/_components/.vscode/settings.json new file mode 100644 index 000000000..4b67a8bd3 --- /dev/null +++ b/packages/_components/.vscode/settings.json @@ -0,0 +1,29 @@ +{ + "editor.rulers": [80, 120], + + // === + // Spacing + // === + + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "files.trimTrailingWhitespace": true, + "files.eol": "\n", + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + + // === + // Event Triggers + // === + + "editor.formatOnSave": true, + "eslint.autoFixOnSave": true, + "eslint.run": "onSave", + "eslint.validate": [ + { "language": "javascript", "autoFix": true }, + { "language": "javascriptreact", "autoFix": true } + ], + "prettier.disableLanguages": [], + "prettier.endOfLine": "lf" +} diff --git a/packages/_viewer/extensions/ohif-cornerstone-extension/LICENSE b/packages/_components/LICENSE similarity index 100% rename from packages/_viewer/extensions/ohif-cornerstone-extension/LICENSE rename to packages/_components/LICENSE diff --git a/packages/_components/README.md b/packages/_components/README.md new file mode 100644 index 000000000..98679bfab --- /dev/null +++ b/packages/_components/README.md @@ -0,0 +1,165 @@ + + +
+

react-viewerbase

+ +

React Viewerbase is a collection of components and utilities that power OHIF's zero-footprint DICOM viewer.

+
+ +
+Read The Docs | +Edit the docs + +
+ +
+ +[![CircleCI][circleci-image]][circleci-url] +[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors) +[![code style: prettier][prettier-image]][prettier-url] +[![semantic-release][semantic-image]][semantic-url] + +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![MIT License][license-image]][license-url] + + + +This set of "Medical Imaging Viewer" React components are maintained separately +to: + +- Decouple presentation from business logic +- Test and develop components in isolation +- Provide well documented, reusable components +- Aid rapid application development for context specific viewers + +## Install + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +For full installation instructions, be sure to check out our +[getting started](https://react.ohif.org/getting-started#installation) guide. + +```bash +// with npm +npm i react-viewerbase --save-exact + +// with yarn +yarn add react-viewerbase --exact +``` + +## Usage + +```jsx +import React, { Component } from 'react'; +import { LayoutButton } from 'react-viewerbase'; + +class Example extends Component { + constructor(props) { + super(props); + + this.state = { + selectedCell: { + className: 'hover', + col: 1, + row: 1, + }, + }; + } + + render() { + return ( + this.setState({ selectedCell: cell })} + /> + ); + } +} +``` + +## Running Locally + +_Restore dependencies after cloning:_ + +1. `cd react-viewerbase` +2. `yarn install` + +_Develop w/ Hot Reloading:_ + +`yarn dev` + +_Build for Production:_ + +`yarn build` + +## Contributors ✨ + +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + +
Erik Ziegler
Erik Ziegler

💻 🚧
Danny Brown
Danny Brown

💻 🚧
Gustavo André Lelis
Gustavo André Lelis

💻 🚧
Thiago Maltempi
Thiago Maltempi

💻
Esref Durna
Esref Durna

💻
Steve Pieper
Steve Pieper

💻
Biharck Araujo
Biharck Araujo

💻
Rodrigo Antinarelli
Rodrigo Antinarelli

💻
João Felipe de Medeiros Moreira
João Felipe de Medeiros Moreira

💻
James Gosbell
James Gosbell

💻
Evren Ozkan
Evren Ozkan

💻
Zach S.
Zach S.

💻
Mete Ugur Akdogan
Mete Ugur Akdogan

💻
+ + + +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! + +## Issues + +_Looking to contribute? Look for the [Good First Issue][good-first-issue] +label._ + +### 🐛 Bugs + +Please file an issue for bugs, missing documentation, or unexpected behavior. + +[**See Bugs**][bugs] + +### 💡 Feature Requests + +Please file an issue to suggest new features. Vote on feature requests by adding +a 👍. This helps maintainers prioritize what to work on. + +[**See Feature Requests**][requests-feature] + +### ❓ Questions + +For questions related to using the library, please visit our support community, +or file an issue on GitHub. + +[Google Group][google-group] + +## License + +MIT © [OHIF](https://github.com/OHIF) + + + + +[all-contributors-image]: https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square +[contributing-url]: https://github.com/OHIF/react-viewerbase/blob/master/CONTRIBUTING.md +[circleci-image]: https://circleci.com/gh/OHIF/react-viewerbase.svg?style=svg +[circleci-url]: https://circleci.com/gh/OHIF/react-viewerbase +[prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square +[prettier-url]: https://github.com/prettier/prettier +[npm-url]: https://npmjs.org/package/react-viewerbase +[npm-downloads-image]: https://img.shields.io/npm/dm/react-viewerbase.svg?style=flat-square +[npm-version-image]: https://img.shields.io/npm/v/react-viewerbase.svg?style=flat-square +[license-image]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square +[license-url]: LICENSE +[semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg +[semantic-url]: https://github.com/semantic-release/semantic-release + +[bugs]: https://github.com/OHIF/react-viewerbase/labels/bug +[requests-feature]: https://github.com/OHIF/react-viewerbase/labels/enhancement +[good-first-issue]: https://github.com/OHIF/react-viewerbase/labels/good%20first%20issue +[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform + diff --git a/packages/_components/doczrc.js b/packages/_components/doczrc.js new file mode 100644 index 000000000..06466abd9 --- /dev/null +++ b/packages/_components/doczrc.js @@ -0,0 +1,113 @@ +/** + * Docz Configuration File: + * https://www.docz.site/docs/project-configuration + */ + +import { css } from 'docz-plugin-css'; + +export default { + dest: 'example/build', + public: '/public', + wrapper: 'src/__docs__/wrapper', + indexHtml: 'src/__docs__/index.html', + // Limited support for importing `.styl` files + codeSandbox: false, + // https://github.com/pedronauck/docz/pull/849/files + // Because we re-export using `index.js` files + notUseSpecifiers: true, + // filterComponents: files => + // files.filter(filepath => + // /\/[A-Z]\w*(\/index)?\.(js|jsx|ts|tsx)$/.test(filepath) + // ), + menu: [ + 'Introduction', + 'Getting Started', + { name: 'Elements', menu: ['Form / Select'] }, + { + name: 'Components', + menu: [ + 'CINE Dialog', + 'Layout Button', + 'Measurement Table', + 'Overlay Trigger', + 'Quick Switch', + 'Rounded Button Group', + 'Select Tree', + 'Simple Dialog', + 'Study Browser', + 'Study List', + 'Table List', + 'Toolbar Section', + 'About Modal', + 'User Preferences Modal', + ], + }, + 'Styling & Theming', + 'Translating', + 'Compatibility', + ], + // Rollup Aliases? + // https://github.com/pedronauck/docz/issues/373 + plugins: [ + // CSS + css(), + // Stylus + css({ + preprocessor: 'stylus', + cssmodules: false, + }), + ], + // `docz` uses file-loader to pull in SVGs. This kills our icons before + // They can be picked up by our `babel-plugin-inline-react-svg` dependency + // You can see where it's configured here: + // https://github.com/pedronauck/docz/blob/f72624d0aa5231dab17e1770e8d36be920f590a2/core/docz-core/src/bundler/loaders.ts#L135-L144 + // How we delete our rule: + // https://github.com/neutrinojs/webpack-chain/issues/48 + onCreateWebpackChain: config => { + config.module.rules.delete('svg'); + + // config.module + // .rule('svg') + // .test(/\.(svg)(\?.*)?$/) + // .use('file-loader') + // .loader(require.resolve('file-loader')) + // .options({ + // name: `static/img/[name].[ext]`, + // }) + }, + modifyBabelRc: babelrc => { + const newBabelRc = { + ...babelrc, + plugins: [ + ...babelrc.plugins, + require.resolve('babel-plugin-inline-react-svg'), + ], + }; + return newBabelRc; + }, +}; + +/* + * Alternative ways to extend/modify underlying webpack config + * +modifyBundlerConfig: (config) => { + config.module.rules.push({ + test: /\.scss$/, + use: ["style-loader", "css-loader", "sass-loader"] + }) + + return config +} + * + * + * +onCreateWebpackChain: config => { + config.module + .rule('css') + .test(/\.css$/) + .use('css-loader') + .loader('css-loader') + .options({ sourceMap: false, + importLoaders: 2 }) +} +*/ diff --git a/packages/_components/netlify.toml b/packages/_components/netlify.toml new file mode 100644 index 000000000..2aace3d92 --- /dev/null +++ b/packages/_components/netlify.toml @@ -0,0 +1,21 @@ +# https://www.netlify.com/docs/continuous-deployment/#deploy-contexts +# +# Global settings applied to the whole site. +# +# “base” is the directory to change to before starting build. If you set base: +# that is where we will look for package.json/.nvmrc/etc, not repo root! +# “command” is your build command. +# “publish” is the directory to publish (relative to the root of your repo). + +# COMMENT: NODE_VERSION in root `.nvmrc` takes priority +# COMMENT: Why we specify YARN_FLAGS: https://www.netlify.com/docs/build-gotchas/#yarn +[build.environment] + NODE_VERSION = "10.15.3" + YARN_VERSION = "1.15.2" + YARN_FLAGS = "--no-ignore-optional --pure-lockfile" + +# COMMENT: This a rule for Single Page Applications +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 \ No newline at end of file diff --git a/packages/_components/package.json b/packages/_components/package.json new file mode 100644 index 000000000..f3ae738a1 --- /dev/null +++ b/packages/_components/package.json @@ -0,0 +1,127 @@ +{ + "name": "react-viewerbase", + "version": "0.2.17", + "description": "A set of React components for Medical Imaging Viewers", + "author": "OHIF Contributors", + "license": "MIT", + "repository": "OHIF/react-viewerbase", + "main": "dist/index.js", + "module": "dist/index.es.js", + "jsnext:main": "dist/index.es.js", + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "scripts": { + "cm": "npx git-cz", + "dev": "docz dev", + "test": "cross-env CI=1 react-scripts test --env=jsdom --transformIgnorePatterns \"node_modules/(?!react-dnd)/\"", + "test:watch": "react-scripts test --env=jsdom --transformIgnorePatterns \"node_modules/(?!react-dnd)/\"", + "build": "rollup -c", + "start": "rollup -c -w", + "lint": "eslint -c .eslintrc --fix src && prettier --single-quote --write src/**/*.{js,jsx,json,css}", + "prepare": "yarn build", + "prepublishOnly": "yarn build", + "generateStaticSite": "docz build", + "contributors:add": "all-contributors add", + "contributors:generate": "all-contributors generate" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,jsx,json,css}": [ + "prettier --single-quote --write", + "git add" + ] + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ], + "files": [ + "dist" + ], + "peerDependencies": { + "@ohif/i18n": "0.2.1", + "i18next": "^17.0.3", + "react": "^16.8.6", + "react-dom": "^16.8.6", + "react-i18next": "^10.11.0" + }, + "dependencies": { + "@babel/runtime": "7.2.0", + "@ohif/i18n": "0.2.1", + "classnames": "2.2.6", + "i18next": "^17.0.3", + "i18next-browser-languagedetector": "^3.0.1", + "lodash.debounce": "^4.0.8", + "lodash.isequal": "4.5.0", + "moment": "2.24.0", + "prop-types": "15.6.2", + "react-bootstrap-modal": "4.2.0", + "react-dates": "18.4.1", + "react-dnd": "8.0.3", + "react-dnd-touch-backend": "^0.8.3", + "react-i18next": "^10.11.0", + "react-with-direction": "1.3.0" + }, + "devDependencies": { + "@babel/core": "^7.2.2", + "@babel/plugin-external-helpers": "^7.2.0", + "@babel/plugin-proposal-class-properties": "^7.2.3", + "@babel/plugin-transform-runtime": "^7.2.0", + "@babel/preset-env": "^7.2.3", + "@babel/preset-react": "^7.0.0", + "@mdx-js/tag": "^0.20.3", + "all-contributors-cli": "6.7.0", + "autoprefixer": "^9.6.0", + "babel-eslint": "^9.0.0", + "babel-plugin-inline-react-svg": "1.1.0", + "commitizen": "3.0.7", + "cpx": "1.5.0", + "cross-env": "^5.2.0", + "cz-conventional-changelog": "2.1.0", + "docz": "1.2.0", + "docz-core": "1.2.0", + "docz-plugin-css": "0.11.0", + "docz-theme-default": "1.2.0", + "eslint": "5.12.0", + "eslint-config-prettier": "4.1.0", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-node": "^8.0.0", + "eslint-plugin-prettier": "3.0.1", + "eslint-plugin-promise": "^4.0.1", + "eslint-plugin-react": "^7.11.1", + "husky": "^1.3.1", + "lint-staged": "^8.1.0", + "lodash.clonedeep": "^4.5.0", + "prettier": "1.16.4", + "react": "16.8.6", + "react-dom": "16.8.6", + "react-powerplug": "1.0.0", + "react-scripts": "^2.1.5", + "rollup": "^0.68.2", + "rollup-plugin-babel": "^4.2.0", + "rollup-plugin-commonjs": "^9.2.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-resolve": "^4.0.0", + "rollup-plugin-peer-deps-external": "^2.2.0", + "rollup-plugin-postcss": "^1.6.3", + "rollup-plugin-url": "^2.1.0", + "semantic-release": "15.13.x", + "stylelint": "^9.9.0", + "stylelint-config-recommended": "^2.1.0", + "stylus": "^0.54.5", + "webpack": "4.28.4" + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + } +} diff --git a/packages/_components/public/docz.css b/packages/_components/public/docz.css new file mode 100644 index 000000000..1c45c19aa --- /dev/null +++ b/packages/_components/public/docz.css @@ -0,0 +1,44 @@ +/* Tip & Success "Call outs" */ + +p.tip { + border-left-color: #f66; +} +p.success { + border-left-color: #42b983; +} +p.tip::before { + content: '!'; + background-color: #f66; +} +p.success::before { + content: '\f00c'; + font-family: FontAwesome; + background-color: #42b983; +} +p.tip, +p.success { + padding: 12px 24px 12px 30px; + margin: 2em 0; + line-height: 1.6em; + border-left-width: 4px; + border-left-style: solid; + background-color: #f8f8f8; + position: relative; + border-bottom-right-radius: 2px; + border-top-right-radius: 2px; +} +p.tip::before, +p.success::before { + position: absolute; + top: 14px; + left: -12px; + color: #fff; + width: 20px; + height: 20px; + border-radius: 100%; + text-align: center; + line-height: 20px; + font-weight: bold; + font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif; + font-size: 14px; +} diff --git a/packages/_components/rollup.config.js b/packages/_components/rollup.config.js new file mode 100644 index 000000000..0fb594827 --- /dev/null +++ b/packages/_components/rollup.config.js @@ -0,0 +1,61 @@ +import autoprefixer from 'autoprefixer'; +import babel from 'rollup-plugin-babel'; +import builtins from 'rollup-plugin-node-builtins'; +import commonjs from 'rollup-plugin-commonjs'; +import external from 'rollup-plugin-peer-deps-external'; +import pkg from './package.json'; +import postcss from 'rollup-plugin-postcss'; +import resolve from 'rollup-plugin-node-resolve'; +import url from 'rollup-plugin-url'; + +const globals = { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-i18next': 'reactI18next', + '@ohif/i18n': 'i18n', +}; + +export default { + input: 'src/index.js', + output: [ + { + file: pkg.main, + format: 'umd', + name: 'react-viewerbase', + sourcemap: true, + exports: 'named', + globals, + }, + { + file: pkg.module, + format: 'es', + sourcemap: true, + globals, + }, + ], + plugins: [ + builtins(), + external(), + postcss({ + modules: false, + plugins: [autoprefixer], + }), + url(), + babel({ + exclude: 'node_modules/**', + runtimeHelpers: true, + }), + resolve({ + browser: true, + }), + commonjs({ + // https://github.com/airbnb/react-dates/issues/1183#issuecomment-392073823 + namedExports: { + 'node_modules/react-dates/index.js': [ + 'DateRangePicker', + 'isInclusivelyBeforeDay', + ], + }, + }), + ], +}; diff --git a/packages/_components/src/LayoutChooser/LayoutManager.css b/packages/_components/src/LayoutChooser/LayoutManager.css new file mode 100644 index 000000000..0b568e259 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutManager.css @@ -0,0 +1,22 @@ +.viewport-container { + float: left; + position: relative; + border: var(--viewport-border-thickness) solid var(--ui-border-color); +} + +.viewport-container.active { + border: var(--viewport-border-thickness) solid var(--active-color); +} + +.EmptyViewport { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: var(--text-secondary-color); +} + +.LayoutPanel { + width: 100%; + height: 100%; +} diff --git a/packages/_components/src/LayoutChooser/LayoutManager.js b/packages/_components/src/LayoutChooser/LayoutManager.js new file mode 100644 index 000000000..6ec9f65f0 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutManager.js @@ -0,0 +1,162 @@ +import './LayoutManager.css'; + +import React, { Component } from 'react'; + +import LayoutPanelDropTarget from './LayoutPanelDropTarget.js'; +import PropTypes from 'prop-types'; + +function defaultViewportPlugin(props) { + return
{JSON.stringify(props)}
; +} + +function EmptyViewport() { + return ( +
+

Please drag a stack here to view images.

+
+ ); +} + +export class LayoutManager extends Component { + static className = 'LayoutManager'; + static defaultProps = { + viewportData: [], + layout: { + viewports: [ + { + top: 0, + left: 0, + height: '100%', + width: '100%', + }, + ], + }, + activeViewportIndex: 0, + supportsDragAndDrop: true, + availablePlugins: { + defaultViewportPlugin, + }, + defaultPlugin: 'defaultViewportPlugin', + }; + + static propTypes = { + viewportData: PropTypes.array.isRequired, + supportsDragAndDrop: PropTypes.bool.isRequired, + activeViewportIndex: PropTypes.number.isRequired, + layout: PropTypes.object.isRequired, + availablePlugins: PropTypes.object.isRequired, + setViewportData: PropTypes.func, + studies: PropTypes.array, + children: PropTypes.node, + }; + + onDrop = ({ viewportIndex, item }) => { + if (this.props.setViewportData) { + this.props.setViewportData({ viewportIndex, item }); + } + }; + + getPluginComponent = plugin => { + const pluginComponent = this.props.availablePlugins[ + plugin || this.props.defaultPlugin + ]; + + if (!pluginComponent) { + throw new Error( + `No Viewport Plugin available for plugin ${plugin}. Available plugins: ${JSON.stringify( + this.props.availablePlugins + )}` + ); + } + + return pluginComponent; + }; + + getChildComponent(plugin, data, viewportIndex, children) { + if (data.displaySet) { + const PluginComponent = this.getPluginComponent(plugin); + + return ( + + ); + } + + return ; + } + + getContent(childComponent, supportsDragAndDrop, viewportIndex) { + if (supportsDragAndDrop) { + return ( + + {childComponent} + + ); + } + + return
{childComponent}
; + } + + render() { + if (!this.props.viewportData.length) { + return ''; + } + + const { supportsDragAndDrop, studies, viewportData } = this.props; + const viewports = this.props.layout.viewports; + const viewportElements = viewports.map((layout, viewportIndex) => { + const displaySet = viewportData[viewportIndex]; + const data = { + displaySet, + studies, + }; + + // Use whichever plugin is currently in use in the panel + // unless nothing is specified. If nothing is specified + // and the display set has a plugin specified, use that. + // + // TODO: Change this logic to: + // - Plugins define how capable they are of displaying a SopClass + // - When updating a panel, ensure that the currently enabled plugin + // in the viewport is capable of rendering this display set. If not + // then use the most capable available plugin + let plugin = layout.plugin; + if (!layout.plugin && displaySet && displaySet.plugin) { + plugin = displaySet.plugin; + } + + const childComponent = this.getChildComponent( + plugin, + data, + viewportIndex, + this.props.children + ); + const content = this.getContent( + childComponent, + supportsDragAndDrop, + viewportIndex + ); + + let className = 'viewport-container'; + if (this.props.activeViewportIndex === viewportIndex) { + className += ' active'; + } + + return ( +
+ {content} +
+ ); + }); + + return
{viewportElements}
; + } +} + +export default LayoutManager; diff --git a/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css new file mode 100644 index 000000000..7c629feaf --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.css @@ -0,0 +1,12 @@ +.LayoutPanelDropTarget { + width: 100%; + height: 100%; + + opacity: 1; + transition: 0.3s all ease; +} + +.LayoutPanelDropTarget.hovered { + opacity: 0.5; + cursor: copy; +} diff --git a/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js new file mode 100644 index 000000000..96355b519 --- /dev/null +++ b/packages/_components/src/LayoutChooser/LayoutPanelDropTarget.js @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { DropTarget } from 'react-dnd'; +import './LayoutPanelDropTarget.css'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const divTarget = { + drop(props, monitor, component) { + const item = monitor.getItem(); + + if (props.onDrop) { + props.onDrop({ + viewportIndex: props.viewportIndex, + item, + }); + } + + return { + id: `LayoutPanelDropTarget-${props.viewportIndex}`, + viewportIndex: props.viewportIndex, + item, + }; + }, +}; + +// TODO: Find out why we can't move this into the Example app instead. +// It looks like the context isn't properly shared. +class LayoutPanelDropTarget extends Component { + static className = 'LayoutPanelDropTarget'; + + static defaultProps = { + isOver: false, + canDrop: false, + }; + + static propTypes = { + connectDropTarget: PropTypes.func.isRequired, + canDrop: PropTypes.bool.isRequired, + isOver: PropTypes.bool.isRequired, + viewportComponent: PropTypes.object, + }; + + render() { + const { canDrop, isOver, connectDropTarget } = this.props; + const isActive = canDrop && isOver; + + let className = LayoutPanelDropTarget.className; + + if (isActive) { + className += ' hovered'; + } else if (canDrop) { + className += ' can-drop'; + } + + return connectDropTarget( +
{this.props.children}
+ ); + } +} + +const collect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + canDrop: monitor.canDrop(), + isOver: monitor.isOver(), +}); + +export default DropTarget(Types.THUMBNAIL, divTarget, collect)( + LayoutPanelDropTarget +); diff --git a/packages/_components/src/ScrollableArea/ScrollableArea.js b/packages/_components/src/ScrollableArea/ScrollableArea.js new file mode 100644 index 000000000..0a76c63e5 --- /dev/null +++ b/packages/_components/src/ScrollableArea/ScrollableArea.js @@ -0,0 +1,152 @@ +import './ScrollableArea.styl'; + +import React, { Component } from 'react'; + +import { Icon } from './../elements/Icon'; +import PropTypes from 'prop-types'; +import getScrollbarSize from '../utils/getScrollbarSize.js'; +import throttled from '../utils/throttled.js'; + +export class ScrollableArea extends Component { + static propTypes = { + children: PropTypes.node.isRequired, + class: PropTypes.string, + scrollableClass: PropTypes.string, + scrollX: PropTypes.bool, + scrollY: PropTypes.bool, + hideScrollbar: PropTypes.bool, + scrollStep: PropTypes.number, + }; + + static defaultProps = { + hideScrollbar: true, + class: 'flex-grow fit', + scrollY: true, + scrollX: false, + scrollStep: 100, + }; + + constructor(props) { + super(props); + + this.state = { + scrollAreaClasses: '', + }; + + this.scrollHandlerThrottled = throttled(150, this.scrollHandler); + } + + render() { + let scrollableClass = 'scrollable'; + if (this.props.scrollableClass) { + scrollableClass += ` ${this.props.scrollableClass}`; + } + if (this.props.scrollX) { + scrollableClass += ` scrollX`; + } + if (this.props.scrollY) { + scrollableClass += ` scrollY`; + } + + return ( +
+
{ + this.scrollableElement = element; + }} + onScroll={this.scrollHandlerThrottled} + onMouseEnter={this.scrollHandlerThrottled} + onTransitionEnd={this.scrollHandlerThrottled} + > + {this.props.children} +
+
+ {/* */} + +
+
+ +
+
+ ); + } + + componentDidMount() { + this.adjustMargins(); + this.scrollHandler(); + window.addEventListener('resize', this.adjustMargins); + } + + componentDidUpdate() { + this.adjustMargins(); + this.scrollHandler(); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.adjustMargins); + } + + scrollNavDown = event => { + const { + scrollTop: currentTop, + offsetHeight: height, + scrollHeight, + } = this.scrollableElement; + + const limit = scrollHeight - height; + let scrollTop = currentTop + this.props.scrollStep; + scrollTop = scrollTop > limit ? limit : scrollTop; + this.scrollableElement.scrollTop = scrollTop; + }; + + scrollNavUp = event => { + const { scrollTop: currentTop } = this.scrollableElement; + + let scrollTop = currentTop - this.props.scrollStep; + scrollTop = scrollTop < 0 ? 0 : scrollTop; + + this.scrollableElement.scrollTop = scrollTop; + }; + + adjustMargins = () => { + if (this.props.hideScrollbar) { + const x = this.props.scrollX ? 1 : 0; + const y = this.props.scrollY ? 1 : 0; + const scrollbarSize = getScrollbarSize(); + this.scrollableElement.style.marginRight = `${0 - + scrollbarSize[0] * y}px`; + this.scrollableElement.style.marginBottom = `${0 - + scrollbarSize[1] * x}px`; + } + }; + + scrollHandler = () => { + const { + offsetHeight: height, + scrollTop: scrollTop, + scrollHeight, + } = this.scrollableElement; + let scrollAreaClasses = ''; + + // Check if can scroll up + if (scrollTop) { + scrollAreaClasses += 'canScrollUp'; + } + + // Check if can scroll down + if (scrollTop + height < scrollHeight) { + scrollAreaClasses += ' canScrollDown'; + } + + if (this.state.scrollAreaClasses !== scrollAreaClasses) { + this.setState({ + scrollAreaClasses, + }); + } + }; +} diff --git a/packages/_components/src/ScrollableArea/ScrollableArea.styl b/packages/_components/src/ScrollableArea/ScrollableArea.styl new file mode 100644 index 000000000..6c49f8197 --- /dev/null +++ b/packages/_components/src/ScrollableArea/ScrollableArea.styl @@ -0,0 +1,69 @@ +.scrollArea + overflow: hidden + position: relative + + .scrollable + max-height: inherit + overflow: hidden + zoom: 1 + + &.scrollX + overflow-x: scroll + + &.scrollY + overflow-y: scroll + + &.fit + height: 100% + width: 100% + + .scrollable + bottom: 0 + left: 0 + max-height: none + position: absolute + right: 0 + top: 0 + + .scrollNav + background-color: rgba(0, 0, 0, 0.75) + box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.75) + cursor: pointer + height: 24px + left: 10px + opacity: 0 + position: absolute + right: 10px + transition: all 0.3s ease + + .scrollNavIcon + color: var(--active-color); + display: block; + width: 18px; + height: 18px; + margin: 0 auto; + transition: color 0.3s ease; + + &:hover + background-color: rgba(0, 0, 0, 0.9) + box-shadow: 0 0 10px 10px rgba(0, 0, 0, 0.9) + + .scrollNavIcon + color: var(--hover-color) + + .scrollNavUp + border-bottom-left-radius: 12px + border-bottom-right-radius: 12px + top: 0 + transform: translateY(-24px) + + .scrollNavDown + border-top-left-radius: 12px + border-top-right-radius: 12px + bottom: 0 + transform: translateY(24px) + + &.canScrollUp .scrollNavUp, + &.canScrollDown .scrollNavDown + opacity: 1 + transform: translateY(0) diff --git a/packages/_components/src/__docs__/NameSpace.js b/packages/_components/src/__docs__/NameSpace.js new file mode 100644 index 000000000..81e1bfa34 --- /dev/null +++ b/packages/_components/src/__docs__/NameSpace.js @@ -0,0 +1,17 @@ +import React, { Fragment } from 'react'; +import { Link } from 'docz'; +import PropTypes from 'prop-types'; + +const NameSpace = ({ name }) => ( + + The namespace used on this component was:
{name}
Check the{' '} + translation docs{' '} + to see how to override it. +
+); + +NameSpace.propTypes = { + name: PropTypes.string.isRequired, +}; + +export default NameSpace; diff --git a/packages/_components/src/__docs__/assets/github.png b/packages/_components/src/__docs__/assets/github.png new file mode 100644 index 000000000..bc8b2c2fe Binary files /dev/null and b/packages/_components/src/__docs__/assets/github.png differ diff --git a/packages/_components/src/__docs__/assets/npm.png b/packages/_components/src/__docs__/assets/npm.png new file mode 100644 index 000000000..f80979758 Binary files /dev/null and b/packages/_components/src/__docs__/assets/npm.png differ diff --git a/packages/_components/src/__docs__/assets/react.png b/packages/_components/src/__docs__/assets/react.png new file mode 100644 index 000000000..675ba2893 Binary files /dev/null and b/packages/_components/src/__docs__/assets/react.png differ diff --git a/packages/_components/src/__docs__/assets/typescript.png b/packages/_components/src/__docs__/assets/typescript.png new file mode 100644 index 000000000..0b3cdeb17 Binary files /dev/null and b/packages/_components/src/__docs__/assets/typescript.png differ diff --git a/packages/_components/src/__docs__/compatibility.mdx b/packages/_components/src/__docs__/compatibility.mdx new file mode 100644 index 000000000..cd95c68d1 --- /dev/null +++ b/packages/_components/src/__docs__/compatibility.mdx @@ -0,0 +1,15 @@ +--- +name: Compatibility +route: /compatibility +--- + +# Compatibility + +## Browser Support + +React Viewerbase relies on +[browsers supported by Styled Components](https://www.styled-components.com/docs/faqs#which-browsers-are-supported) +or browsers supported by emotion. + +Supported Browsers: IE11, IE 9+ (with Map + Set polyfills), Chrome, Firefox (and +derivatives), Edge, and Safari. diff --git a/packages/_components/src/__docs__/config/Wrapper.js b/packages/_components/src/__docs__/config/Wrapper.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/__docs__/docs-settings.mdx b/packages/_components/src/__docs__/docs-settings.mdx new file mode 100644 index 000000000..200133a85 --- /dev/null +++ b/packages/_components/src/__docs__/docs-settings.mdx @@ -0,0 +1,13 @@ +--- +name: Docs Settings +route: /docs-settings +--- + +import LanguageSwitcher from '../components/languageSwitcher'; + +# Components Language + +You can change the language of the components displayed in this documentation by +choosing an option in the selector below: + + diff --git a/packages/_components/src/__docs__/getting-started.mdx b/packages/_components/src/__docs__/getting-started.mdx new file mode 100644 index 000000000..dd3cc7900 --- /dev/null +++ b/packages/_components/src/__docs__/getting-started.mdx @@ -0,0 +1,76 @@ +--- +name: Getting Started +route: /getting-started +--- + +# Getting Started + +## Installation + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +Install `react-viewerbase` from npm: + +```shell +// with npm +npm i react-viewerbase --save-exact + +// with yarn +yarn add react-viewerbase --exact +``` + +## Usage + +There is some setup required for `react-viewerbase`. We are working to eliminate +these requirements with the goal of providing ready to use components. + +### External Dependencies + +We have tried to keep the number of external dependencies limited to make setup +as easy as possible. You will, however, need to include either the default font +or the font you set for your theme: + +```js +// Google Fonts, Sanchez & Roboto +'https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap'; +``` + +React Viewerbase also looks for theme CSS variabled defined on `:root`. You can +find a list of variables [on the theming page](/styling-and-theming) + +### Import & Use + +You can use components in your React app: + +```js +import React, { Component } from 'react'; +import { LayoutButton } from 'react-viewerbase'; + +class Example extends Component { + constructor(props) { + super(props); + + this.state = { + selectedCell: { + className: 'hover', + col: 1, + row: 1, + }, + }; + } + + render() { + return ( + this.setState({ selectedCell: cell })} + /> + ); + } +} +``` + +> NOTE: It is heavily recommended that you use a CSS reset or normalize. As well +> as the following style: `* { Box-sizing: Border-box }` diff --git a/packages/_components/src/__docs__/index.html b/packages/_components/src/__docs__/index.html new file mode 100644 index 000000000..263c5b2f1 --- /dev/null +++ b/packages/_components/src/__docs__/index.html @@ -0,0 +1,80 @@ + + + + + + + + + {{ title }} + + + + + + + {{ head }} + + +
+ {{ footer }} + + diff --git a/packages/_components/src/__docs__/introduction.mdx b/packages/_components/src/__docs__/introduction.mdx new file mode 100644 index 000000000..efdde28a1 --- /dev/null +++ b/packages/_components/src/__docs__/introduction.mdx @@ -0,0 +1,76 @@ +--- +name: Introduction +route: / +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { TableList, TableListItem } from './../index.js' + +# Introduction + +**React Viewerbase is a collection of components and utilities** that power +OHIF's [zero-footprint DICOM viewer](https://github.com/OHIF/Viewers) +([demo][demo-url]). We maintain them as a separate component library to: + +- Decouple presentation from business logic +- Test and develop components in isolation +- Provide well documented, reusable components +- Aid rapid application development for context specific viewers + +## Quick Example + +> This component library is pre- v1.0. All realeases until a v1.0 have the +> possibility of introducing breaking changes. Please depend on an "exact" +> version in your projects to prevent issues caused by loose versioning. + +```js +// Add 'react-viewerbase' as a dependency +yarn add react-viewerbase + +// Import and use components +import { TableList } from 'react-viewerbase' +``` + + + + {({ state, setState }) => ( + + {state.listItems.map((item, index) => { + return ( + alert('item clicked')} + > + + + ) + })} + + )} + + + +## FAQ + +... + + + + +[demo-url]: https://docs.ohif.org/demo/ + diff --git a/packages/_components/src/__docs__/styling-and-theming.mdx b/packages/_components/src/__docs__/styling-and-theming.mdx new file mode 100644 index 000000000..ce98dcde0 --- /dev/null +++ b/packages/_components/src/__docs__/styling-and-theming.mdx @@ -0,0 +1,56 @@ +--- +name: Styling & Theming +route: /styling-and-theming +--- + +# Styling & Theming + +```css +:root { + /* Interface UI Colors */ + --default-color: #9ccef9; + --hover-color: #ffffff; + --active-color: #20a5d6; + --ui-border-color: #44626f; + --ui-border-color-dark: #3c5d80; + --ui-border-color-active: #00a4d9; + --primary-background-color: #000000; + --box-background-color: #3e5975; + + --text-primary-color: #ffffff; + --text-secondary-color: #91b9cd; + --input-background-color: #2c363f; + --input-placeholder-color: #d3d3d3; + + --table-hover-color: #2c363f; + --table-text-primary-color: #ffffff; + --table-text-secondary-color: #91b9cd; + + --large-numbers-color: #6fbde2; + + --state-error: #ffcccc; + --state-error-border: #ffcccc; + --state-error-text: #ffcccc; + + /* Common palette */ + --ui-yellow: #e29e4a; + --ui-sky-blue: #6fbde2; + + /* State palette */ + --ui-state-error: #ffcccc; + --ui-state-error-border: #993333; + --ui-state-error-text: #661111; + --ui-gray-lighter: #436270; + --ui-gray-light: #516873; + --ui-gray: #263340; + --ui-gray-dark: #16202b; + --ui-gray-darker: #151a1f; + --ui-gray-darkest: #14202a; + + --calendar-day-color: #d3d3d3; + --calendar-day-border-color: #d3d3d3; + --calendar-day-active-hover-background-color: #516873; + --calendar-main-color: #263340; + --viewport-border-thickness: 1px; +} +``` diff --git a/packages/_components/src/__docs__/translating.mdx b/packages/_components/src/__docs__/translating.mdx new file mode 100644 index 000000000..44e810751 --- /dev/null +++ b/packages/_components/src/__docs__/translating.mdx @@ -0,0 +1,87 @@ +--- +name: Translating +route: /translating +--- + +# Translating + +**React Viewerbase** supports internationalization using +[i18next](https://www.i18next.com/) through the npm package +[@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), where is the main +instance of i18n containing several languages and tools. + +## How to translate components + +All you need is to use the internal custom superset of +[withTranslation HOC](https://react.i18next.com/latest/withtranslation-hoc), +available at `src/utils/LanguageProvider.js` + +e.g.: + +```js +import React from 'react'; +import { withTranslation } from '../utils/LanguageProvider.js'; + +function MyComponent({ t }) { + return

{t('my translated text')}

; +} + +export default withTranslation('MyNameSpace')(MyComponent); +``` + +--- + +## Namespaces + +For further information about namespaces and how they work, please visit +[OHIF Namespaces docs](https://docs.ohif.org/essentials/translating.html#namespaces). + +### How to get right the Name Space of a react-viewerbase component? + +Check the component's page in this site, the NameSpace information should be on +the bottom line. + +Also, a nameSpace is defined when exporting a component, this information can be +found on exports trough the code, in case of missing in docs. + +E.g. + +``` +export default withTranslation('TheNamespaceYouAreLookingFor')(TheComponentYouWantToUse); +``` + +### How to use another NameSpace inside the current NameSpace? + +i18next provides a parsing feature able to get translations strings from any +NameSpace, like this following example getting data from `InnerSpace` NameSpace: + +``` +$t(InnerSpace:Reset) +export default withTranslation('OuterSpace')(TheComponent); +``` + +--- + +## I18next Provider + +**React Viewerbase** pass down the `t` function trough the custom +[withTranslation HOC](https://github.com/OHIF/react-viewerbase/blob/master/src/utils/LanguageProvider.js). + +--- + +## Extending and Overriding languages + +[How to Extend languages with @ohif/i18n](https://docs.ohif.org/essentials/translating.html#extending-languages-in-ohifi18n) + +## Changing the language + +The language choosing method is the same implemented in +[OHIF Viewers](https://docs.ohif.org/essentials/translating.html#changing-the-language) + +> You can also change the language of the components displayed in this +> documentation by choosing an option in the +> [Docs Settings](https://react.ohif.org/docs-settings) section. + +## Debugging translations + +[How to Debug @ohif/i18n](https://docs.ohif.org/essentials/translating.html#debugging-translations) diff --git a/packages/_components/src/__docs__/wrapper.js b/packages/_components/src/__docs__/wrapper.js new file mode 100644 index 000000000..0e94c5559 --- /dev/null +++ b/packages/_components/src/__docs__/wrapper.js @@ -0,0 +1,22 @@ +import React from 'react'; + +import PropTypes from 'prop-types'; +import LanguageSwitcher from '../components/languageSwitcher'; + +import './wrapper.styl'; + +const Wrapper = ({ children }) => ( + +
+ Display components in: + +
+ {children} +
+); + +Wrapper.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default Wrapper; diff --git a/packages/_components/src/__docs__/wrapper.styl b/packages/_components/src/__docs__/wrapper.styl new file mode 100644 index 000000000..b1a4dae6d --- /dev/null +++ b/packages/_components/src/__docs__/wrapper.styl @@ -0,0 +1,15 @@ +[class*="Sidebar__Menus"] + @media (min-width: 1120px) + margin-top: 80px + +.sidebarLanguageSwitcher + position: fixed + top: 155px + left 0 + z-index: 1049 + padding: 0 24px 20px + border-bottom: 1px dotted #CED4DE + background: #F5F6F7 + transition: all 200ms ease + @media (max-width: 1119px) + left -100% diff --git a/packages/_components/src/components/checkbox/__docs__/checkbox.mdx b/packages/_components/src/components/checkbox/__docs__/checkbox.mdx new file mode 100644 index 000000000..dd74a6af5 --- /dev/null +++ b/packages/_components/src/components/checkbox/__docs__/checkbox.mdx @@ -0,0 +1,42 @@ +--- +name: Checkbox +menu: Elements +route: /components/checkbox +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { Checkbox } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +# Checkbox + +## Basic usage + + + + {({ state, setState }) => ( + +
+
{JSON.stringify(state, null, 2)}
+
+
+ +
+
+ )} +
+
+ +## API + + + +## Translation Namespace + + \ No newline at end of file diff --git a/packages/_components/src/components/checkbox/checkbox.css b/packages/_components/src/components/checkbox/checkbox.css new file mode 100644 index 000000000..96311b6fd --- /dev/null +++ b/packages/_components/src/components/checkbox/checkbox.css @@ -0,0 +1,56 @@ +.ohif-check-container { + --check-button-dim: 15px; +} + +.ohif-check-container input { + position: absolute; + opacity: 0; + height: inherit; + width: inherit; + cursor: default; +} + +.ohif-check-container { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.ohif-checkbox { + width: var(--check-button-dim); + height: var(--check-button-dim); + position: absolute; + top: 20%; + left: 5%; + cursor: pointer; + background-color: var(--ui-gray-lighter); +} + +.ohif-checkbox:hover { + background-color: var(--default-color); +} + +.ohif-checkbox:after { + content: ''; + position: absolute; + width: 70%; + height: 70%; +} + +.ohif-checkbox.ohif-checked:after { + display: block; + top: 14%; + left: 14%; + background: white; +} + +.ohif-check-label { + font-size: 12px; + font-weight: 500; +} diff --git a/packages/_components/src/components/checkbox/checkbox.js b/packages/_components/src/components/checkbox/checkbox.js new file mode 100644 index 000000000..5f1f16ea3 --- /dev/null +++ b/packages/_components/src/components/checkbox/checkbox.js @@ -0,0 +1,46 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './checkbox.css'; + +export class Checkbox extends Component { + static propTypes = { + label: PropTypes.string.isRequired, + checked: PropTypes.bool, + }; + + constructor(props) { + super(props); + this.state = { checked: props.checked, label: props.label }; + } + + handleChange(e) { + this.setState({ checked: e.target.checked }); + } + + render() { + let checkbox; + if (this.state.checked) { + checkbox = ; + } else { + checkbox = ; + } + + return ( +
+
+ +
+
+ ); + } +} diff --git a/packages/_components/src/components/checkbox/index.js b/packages/_components/src/components/checkbox/index.js new file mode 100644 index 000000000..9d1514175 --- /dev/null +++ b/packages/_components/src/components/checkbox/index.js @@ -0,0 +1 @@ +export { Checkbox } from './checkbox.js'; diff --git a/packages/_components/src/components/cineDialog/CineDialog.js b/packages/_components/src/components/cineDialog/CineDialog.js new file mode 100644 index 000000000..854f80695 --- /dev/null +++ b/packages/_components/src/components/cineDialog/CineDialog.js @@ -0,0 +1,202 @@ +import './CineDialog.styl'; + +import React, { PureComponent } from 'react'; +import { withTranslation } from '../../utils/LanguageProvider'; +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; + +class CineDialog extends PureComponent { + constructor(props) { + super(props); + + this.state = { + cineFrameRate: props.cineFrameRate, + isPlaying: props.isPlaying, + }; + } + + static propTypes = { + /** Minimum value for range slider */ + cineMinFrameRate: PropTypes.number.isRequired, + /** Maximum value for range slider */ + cineMaxFrameRate: PropTypes.number.isRequired, + /** Increment range slider can "step" in either direction. */ + cineStepFrameRate: PropTypes.number.isRequired, + cineFrameRate: PropTypes.number.isRequired, + /** 'True' if playing, 'False' if paused. */ + isPlaying: PropTypes.bool.isRequired, + onPlayPauseChanged: PropTypes.func, + onFrameRateChanged: PropTypes.func, + onClickNextButton: PropTypes.func, + onClickBackButton: PropTypes.func, + onClickSkipToStart: PropTypes.func, + onClickSkipToEnd: PropTypes.func, + /** i18next translation function */ + t: PropTypes.func.isRequired, + }; + + static defaultProps = { + cineMinFrameRate: 1, + cineMaxFrameRate: 90, + cineStepFrameRate: 1, + cineFrameRate: 24, + isPlaying: false, + }; + + componentDidUpdate(prevProps) { + // TODO: Not sure if we should just switch this to a stateless + // fully-controlled component instead + if ( + this.props.isPlaying !== prevProps.isPlaying || + this.props.isPlaying !== this.state.isPlaying + ) { + this.setState({ + isPlaying: this.props.isPlaying, + }); + } + + if ( + this.props.cineFrameRate !== prevProps.cineFrameRate || + this.props.cineFrameRate !== this.state.cineFrameRate + ) { + this.setState({ + cineFrameRate: this.props.cineFrameRate, + }); + } + } + + handleInputChange = event => { + const target = event.target; + + let value = target.value; + + if (target.type === 'range') { + value = parseFloat(target.value); + } + + const name = target.name; + + this.setState({ + [name]: value, + }); + + if (name === 'cineFrameRate' && this.props.onFrameRateChanged) { + this.props.onFrameRateChanged(parseFloat(value)); + } + }; + + onClickPlayPause = () => { + const value = !this.state.isPlaying; + + this.setState({ + isPlaying: value, + }); + + if (this.props.onPlayPauseChanged) { + this.props.onPlayPauseChanged(value); + } + }; + + onClickNextButton = event => { + if (this.props.onClickNextButton) { + this.props.onClickNextButton(event); + } + }; + + onClickBackButton = event => { + if (this.props.onClickBackButton) { + this.props.onClickBackButton(event); + } + }; + + onClickSkipToStart = event => { + if (this.props.onClickSkipToStart) { + this.props.onClickSkipToStart(event); + } + }; + + onClickSkipToEnd = event => { + if (this.props.onClickSkipToEnd) { + this.props.onClickSkipToEnd(event); + } + }; + + // TODO: + // - Add next / previous display set buttons which just call + // onClickNextDisplaySet and onClickPreviousDisplaySet which are passed in as props. + // See https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.html#L38 + // - Add 'isEnabled' prop: https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.js#L301 + render() { + const { t } = this.props; + return ( +
+
+
+
+ + + + + +
+
+
+
+ +
+ + {this.state.cineFrameRate.toFixed(1)} {t('fps')} + +
+
+
+ ); + } +} + +const connectedComponent = withTranslation('CineDialog')(CineDialog); +export { connectedComponent as CineDialog }; +export default connectedComponent; diff --git a/packages/_components/src/components/cineDialog/CineDialog.styl b/packages/_components/src/components/cineDialog/CineDialog.styl new file mode 100644 index 000000000..76aeb4076 --- /dev/null +++ b/packages/_components/src/components/cineDialog/CineDialog.styl @@ -0,0 +1,169 @@ +.CineDialog + background: var(--ui-gray-darkest); + color: var(--text-secondary-color); + filter: drop-shadow(0 0 3px var(--ui-gray-darkest)) + border: none + border-radius: 8px + width: inherit; + + h5 + font-size: 20px + line-height: 35px + margin: 0 + + h5, label + font-weight: 400 + + .cine-navigation, .cine-controls, .cine-options + cursor: default + + .fps-section + input[type="range"] + background-color: transparent + border: 0 none + outline: 0 none + + &::-ms-tooltip + display: none + + .double-row-style + box-sizing: border-box + width: 290px + height: 70px + padding: 10px + top: 2% + left: 35% + + .cine-navigation + position: absolute + right: 16px + top: 10px + + .btn + padding: 0 4px + + i + font-size: 32px + line-height: 32px + + .cine-controls + left: 0px + + .cine-options + padding: 0px 0 + + .fps-section + width: 175px + float: left + + .fps + float: right + + + .single-row-style + box-sizing: content-box + width: 425px + height: 45px + padding: 4px 8px + top: 2% + left: 35% + + .cine-navigation + float: right + overflow: hidden + padding-right: 12px + position: relative + + .btn + padding: 0 2px + margin: 0 0 0 2px + border: 0 none + &:first-of-type + margin-left: 0 + + i + font-size: 32px + line-height: 45px + + .cine-controls + position: relative + float: left + overflow: hidden + + .btn + font-size: 22px + line-height: 45px + min-width: 28px + padding: 0 0px + margin: 0 4px + border: 0 none + + .cine-options + display: block + position: relative + width: 150px + float: left + overflow: hidden + padding: 0 + margin: 0 0 0 10px + + .fps-section + display: block + float: left + width: 80px + input[type="range"] + line-height: 45px + height: 45px + + .fps + display: block + width: 68px + float: left + padding: 0 + margin: 0 0 0 2px + text-align: center + line-height: 45px + font-size: 14px; + line-height: 1.42857143; + + .btn + color: var(--text-secondary-color); + background-color: transparent + border: 0 + border-radius: 4px + font-size: 15px + width: 45px + font-weight: normal + height: 37px + line-height: 37px + padding: 0 12px + + &:hover, &:active, &.active + color: var(--hover-color); + + &[disabled] + &:hover + color: inherit + &:active + color: var(--text-secondary-color); + + &.btn-primary, &.btn-secondary, &.btn-danger + transition: background-color 0.3s ease + + &:hover, &:active, &:focus + opacity: 0.8 + + &.btn-primary + background-color: var(--active-color); + color: var(--primary-background-color) + + &.btn-secondary + background-color: var(--text-secondary-color); + color: var(--primary-background-color) + + &.btn-danger + background-color: var(--text-destructive-color); + color: var(--text-primary-color); + + &.active + box-shadow: inset 1px 1px 2px var(--ui-gray-darkest) diff --git a/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx b/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx new file mode 100644 index 000000000..48ecfa0fd --- /dev/null +++ b/packages/_components/src/components/cineDialog/__docs__/cineDialog.mdx @@ -0,0 +1,57 @@ +--- +name: CINE Dialog +menu: Components +route: /components/cine-dialog +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { CineDialog } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +# CINE Dialog + +## Basic usage + + + + {({ state, setState }) => ( + +
+
{JSON.stringify(state, null, 2)}
+
+
+ + setState({ lastChange: 'Clicked SkipToStart' }) + } + onClickSkipToEnd={() => + setState({ lastChange: 'Clicked SkipToEnd' }) + } + onClickNextButton={() => setState({ lastChange: 'Clicked Next' })} + onClickBackButton={() => setState({ lastChange: 'Clicked Back' })} + onLoopChanged={value => setState({ isLoopEnabled: value })} + onFrameRateChanged={value => setState({ cineFrameRate: value })} + onPlayPauseChanged={() => setState({ isPlaying: !state.isPlaying })} + /> +
+
+ )} +
+
+ +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/cineDialog/index.js b/packages/_components/src/components/cineDialog/index.js new file mode 100644 index 000000000..5827ce588 --- /dev/null +++ b/packages/_components/src/components/cineDialog/index.js @@ -0,0 +1,2 @@ +import CineDialog from './CineDialog'; +export { CineDialog }; diff --git a/packages/_components/src/components/index.js b/packages/_components/src/components/index.js new file mode 100644 index 000000000..bc9beb74a --- /dev/null +++ b/packages/_components/src/components/index.js @@ -0,0 +1,48 @@ +import { + ExampleDropTarget, + StudyBrowser, + ThumbnailEntry, +} from './studyBrowser'; +import { LayoutButton, LayoutChooser } from './layoutButton'; +import { MeasurementTable, MeasurementTableItem } from './measurementTable'; +import { Overlay, OverlayTrigger } from './overlayTrigger'; +import { TableList, TableListItem } from './tableList'; +import { + AboutModal, + UserPreferences, + UserPreferencesModal, +} from './userPreferencesModal'; + +import { CineDialog } from './cineDialog'; +import { QuickSwitch } from './quickSwitch'; +import { RoundedButtonGroup } from './roundedButtonGroup'; +import { SelectTree } from './selectTree'; +import { SimpleDialog } from './simpleDialog'; +import { StudyList } from './studyList'; +import { ToolbarSection } from './toolbarSection'; +import { Tooltip } from './tooltip'; + +export { + CineDialog, + ExampleDropTarget, + LayoutButton, + LayoutChooser, + MeasurementTable, + MeasurementTableItem, + Overlay, + OverlayTrigger, + QuickSwitch, + RoundedButtonGroup, + SelectTree, + SimpleDialog, + StudyBrowser, + StudyList, + TableList, + TableListItem, + ThumbnailEntry, + ToolbarSection, + Tooltip, + AboutModal, + UserPreferences, + UserPreferencesModal, +}; diff --git a/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js new file mode 100644 index 000000000..2d17305bd --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.js @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react'; +import i18n from '@ohif/i18n'; + +import './LanguageSwitcher.styl'; +import { withTranslation } from '../../utils/LanguageProvider'; + +const LanguageSwitcher = () => { + const getCurrentLanguage = (language = i18n.language) => + language.split('-')[0]; + + const [currentLanguage, setCurrentLanguage] = useState(getCurrentLanguage()); + const languages = [ + // TODO: list of available languages should come from i18n.options.resources + { + value: 'en', + label: 'English', + }, + { + value: 'es', + label: 'Spanish', + }, + ]; + + const onChange = () => { + const { value } = event.target; + const language = getCurrentLanguage(value); + setCurrentLanguage(language); + + i18n.init({ + fallbackLng: language, + lng: language, + }); + }; + + useEffect(() => { + let mounted = true; + + i18n.on('languageChanged', () => { + if (mounted) { + setCurrentLanguage(getCurrentLanguage()); + } + }); + + return () => { + mounted = false; + }; + }, []); + + return ( + + ); +}; + +export default withTranslation('UserPreferencesModal')(LanguageSwitcher); diff --git a/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl new file mode 100644 index 000000000..51284c874 --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/LanguageSwitcher.styl @@ -0,0 +1,4 @@ +.language-select + color: var(--primary-background-color) + display: block + min-width: 150px diff --git a/packages/_components/src/components/languageSwitcher/index.js b/packages/_components/src/components/languageSwitcher/index.js new file mode 100644 index 000000000..31505c03b --- /dev/null +++ b/packages/_components/src/components/languageSwitcher/index.js @@ -0,0 +1 @@ +export { default } from './LanguageSwitcher'; diff --git a/packages/_components/src/components/layoutButton/LayoutButton.js b/packages/_components/src/components/layoutButton/LayoutButton.js new file mode 100644 index 000000000..fd6bdee6a --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutButton.js @@ -0,0 +1,64 @@ +import React, { PureComponent } from 'react'; + +import { LayoutChooser } from './LayoutChooser.js'; +import PropTypes from 'prop-types'; +import ToolbarButton from '../../viewer/ToolbarButton'; + +export class LayoutButton extends PureComponent { + static defaultProps = { + dropdownVisible: false, + }; + + static propTypes = { + dropdownVisible: PropTypes.bool.isRequired, + /** Called with the selectedCell number when grid sell is selected */ + onChange: PropTypes.func, + /** The cell to show as selected */ + selectedCell: PropTypes.object, + }; + + state = { + dropdownVisible: this.props.dropdownVisible, + }; + + componentDidUpdate(prevProps) { + if (this.props.dropdownVisible !== prevProps.dropdownVisible) { + this.setState({ + dropdownVisible: this.props.dropdownVisible, + }); + } + } + + onClick = () => { + this.setState({ + dropdownVisible: !this.state.dropdownVisible, + }); + }; + + onChange = selectedCell => { + if (this.props.onChange) { + this.props.onChange(selectedCell); + } + }; + + render() { + return ( +
+ + +
+ ); + } +} + +export default LayoutButton; diff --git a/packages/_components/src/components/layoutButton/LayoutChooser.js b/packages/_components/src/components/layoutButton/LayoutChooser.js new file mode 100644 index 000000000..1603788b1 --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutChooser.js @@ -0,0 +1,125 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './LayoutChooser.styl'; + +/** + * Adds the 'hover' class to cells above and to the left of the current cell + * This is used to "fill in" the grid that the user will change the layout to, + * if they click on a specific table cell. + **/ + +class LayoutChooser extends PureComponent { + static propTypes = { + rows: PropTypes.number.isRequired, + columns: PropTypes.number.isRequired, + visible: PropTypes.bool.isRequired, + selectedCell: PropTypes.object, + boxSize: PropTypes.number.isRequired, + cellBorder: PropTypes.number.isRequired, + onClick: PropTypes.func, + onChange: PropTypes.func, + }; + + static defaultProps = { + rows: 3, + columns: 3, + visible: true, + boxSize: 20, + cellBorder: 1, + selectedCell: { + row: -1, + col: -1, + }, + }; + + constructor(props) { + super(props); + this.emptyCell = { + row: -1, + column: -1, + }; + this.state = { + table: [[]], + selectedCell: this.props.selectedCell, + }; + } + componentDidMount() { + this.highlightCells(this.emptyCell); + } + onClick(currentCell) { + this.setState({ + selectedCell: currentCell, + }); + this.highlightCells(currentCell); + if (this.props.onClick) { + this.props.onClick(currentCell); + } + if (this.props.onChange) { + this.props.onChange(currentCell); + } + } + isRange = (cell, parentCell) => { + return cell.row <= parentCell.row && cell.col <= parentCell.col; + }; + highlightCells = currentCell => { + let table = []; + for (let row = 0; row < this.props.rows; row++) { + let newRow = []; + for (let col = 0; col < this.props.columns; col++) { + let cell = { row: row, col: col }; + if (this.isRange(cell, currentCell)) { + cell.className = 'hover'; + } else if ( + this.isRange(currentCell, this.emptyCell) && + this.isRange(cell, this.state.selectedCell) + ) { + cell.className = 'selectedBefore'; + } + newRow.push(cell); + } + table.push(newRow); + } + this.setState({ table: table }); + }; + + render() { + let columns = this.props.columns; + const style = { + display: this.props.visible ? 'block' : 'none', + minWidth: + columns * this.props.boxSize + (columns + 5) * this.props.cellBorder, + }; + return ( +
+ + + {this.state.table.map((row, i) => { + return ( + + {row.map((cell, j) => { + return ( + + ); + })} + +
this.highlightCells(cell)} + onMouseLeave={() => this.highlightCells(this.emptyCell)} + onClick={() => this.onClick(cell)} + /> + ); + })} +
+
+ ); + } +} + +export { LayoutChooser }; diff --git a/packages/_components/src/components/layoutButton/LayoutChooser.styl b/packages/_components/src/components/layoutButton/LayoutChooser.styl new file mode 100644 index 000000000..2b5b0fc04 --- /dev/null +++ b/packages/_components/src/components/layoutButton/LayoutChooser.styl @@ -0,0 +1,46 @@ +@import './../../design/styles/common/button.styl' + +$borderColor = rgba(77, 99, 110, 0.81) + +.dropdown-menu + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175); + box-shadow: 0 6px 12px rgba(0,0,0,.175); + +.layoutChooser + .selectedBefore + background-color: #5cc3eb + + border: 1px solid $borderColor; + border-radius: 8px + padding: 5px 0; + position: absolute; + z-index: 5000 + + table + margin: 0 auto + border-spacing: 0; + border-collapse: collapse; + td + cursor: pointer + transition(background-color 0.1s ease) + + &:hover, &.hover // Add the hover class here to be triggered by mouseenter/mouseleave + background-color: #209ac9 diff --git a/packages/_components/src/components/layoutButton/__docs__/layoutButton.css b/packages/_components/src/components/layoutButton/__docs__/layoutButton.css new file mode 100644 index 000000000..21ec59d7e --- /dev/null +++ b/packages/_components/src/components/layoutButton/__docs__/layoutButton.css @@ -0,0 +1,10 @@ +.LayoutExample { + background-color: var(--primary-background-color); + width: 160px; + height: 160px; +} + +.GridLayout td { + padding: 5px; + border: 1px solid black; +} diff --git a/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx b/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx new file mode 100644 index 000000000..fc6a5a6ac --- /dev/null +++ b/packages/_components/src/components/layoutButton/__docs__/layoutButton.mdx @@ -0,0 +1,49 @@ +--- +name: Layout Button +menu: Components +route: /components/layout-button +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { LayoutButton } from './../index.js' +import './layoutButton.css' +import NameSpace from '../../../__docs__/NameSpace' + +# Layout Button + +## Basic usage + + + + {({ state, setState }) => ( + + {/* STATE */} +
{JSON.stringify(state, null, 2) }
+ + {/* COMPONENT */} +
+ setState({ selectedCell: cell })} + /> +
+
+ )} +
+ +
+ +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/layoutButton/index.js b/packages/_components/src/components/layoutButton/index.js new file mode 100644 index 000000000..b7615590a --- /dev/null +++ b/packages/_components/src/components/layoutButton/index.js @@ -0,0 +1,2 @@ +export { LayoutButton } from './LayoutButton.js'; +export { LayoutChooser } from './LayoutChooser.js'; diff --git a/packages/_components/src/components/measurementTable/MeasurementTable.js b/packages/_components/src/components/measurementTable/MeasurementTable.js new file mode 100644 index 000000000..6532e0baa --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTable.js @@ -0,0 +1,178 @@ +import './MeasurementTable.styl'; + +import React, { Component } from 'react'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import { Icon } from './../../elements/Icon'; +import { MeasurementTableItem } from './MeasurementTableItem.js'; +import { OverlayTrigger } from './../overlayTrigger'; +import PropTypes from 'prop-types'; +import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js'; +import { TableList } from './../tableList'; +import { Tooltip } from './../tooltip'; + +class MeasurementTable extends Component { + static propTypes = { + measurementCollection: PropTypes.array.isRequired, + timepoints: PropTypes.array.isRequired, + overallWarnings: PropTypes.object.isRequired, + readOnly: PropTypes.bool, + onItemClick: PropTypes.func, + onRelabelClick: PropTypes.func, + onDeleteClick: PropTypes.func, + onEditDescriptionClick: PropTypes.func, + selectedMeasurementNumber: PropTypes.number, + overwallWarnings: PropTypes.object, + t: PropTypes.func, + }; + + static defaultProps = { + overallWarnings: { + warningList: [], + }, + readOnly: false, + }; + + state = { + selectedKey: null, + }; + + render() { + const hasOverallWarnings = + this.props.overallWarnings.warningList.length > 0; + return ( +
+
+ {hasOverallWarnings && ( + +
+ {this.props.t('Criteria nonconformities')} +
+
+ {this.getWarningContent()} +
+ + } + > + + + + + +
+ )} + {this.getTimepointsHeader()} +
+ +
{this.getMeasurementsGroups()}
+
+
+ ); + } + + getMeasurementsGroups = () => { + return this.props.measurementCollection.map((measureGroup, index) => { + return ( + + {this.getMeasurements(measureGroup)} + + ); + }); + }; + + getMeasurements = measureGroup => { + const selectedKey = this.props.selectedMeasurementNumber + ? this.props.selectedMeasurementNumber + : this.state.selectedKey; + return measureGroup.measurements.map((measurement, index) => { + const key = measurement.measurementNumber; + const itemIndex = measurement.itemNumber || index + 1; + const itemClass = + selectedKey === key && !this.props.readOnly ? 'selected' : ''; + + return ( + + ); + }); + }; + + onItemClick = (event, measurementData) => { + if (this.props.readOnly) return; + + this.setState({ + selectedKey: measurementData.measurementNumber, + }); + + if (this.props.onItemClick) { + this.props.onItemClick(event, measurementData); + } + }; + + getCustomHeader = measureGroup => { + return ( + +
+ {this.props.t(measureGroup.groupName)} +
+ {measureGroup.maxMeasurements && ( +
+ {this.props.t('MAX')} {measureGroup.maxMeasurements} +
+ )} +
{measureGroup.measurements.length}
+
+ ); + }; + + getTimepointsHeader = () => { + return this.props.timepoints.map((timepoint, index) => { + return ( +
+
{timepoint.label}
+
{timepoint.date}
+
+ ); + }); + }; + + getWarningContent = () => { + const { warningList = '' } = this.props.overwallWarnings; + + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; +} + +const connectedComponent = withTranslation('MeasurementTable')( + MeasurementTable +); +export { connectedComponent as MeasurementTable }; +export default connectedComponent; diff --git a/packages/_components/src/components/measurementTable/MeasurementTable.styl b/packages/_components/src/components/measurementTable/MeasurementTable.styl new file mode 100644 index 000000000..14cf50e4d --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTable.styl @@ -0,0 +1,92 @@ +.measurementTable + background-color: var(--primary-background-color) + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + + .maxMeasurements + border-radius: 3px; + color: var(--primary-background-color); + font-size: 12px; + font-weight: 500; + height: 19px; + line-height: 17px; + margin-top: 22px; + margin-right: 15px; + padding: 2px 6px 0; + text-transform: uppercase; + transition: all 0.3s ease; + background-color: var(--default-color); + + .measurementTableHeader + padding-left: 45px; + + .measurementTableHeaderItem + display: inline-block; + margin-top: 9px; + margin-left: 9px; + padding-left: 9px; + width: 90px; + font-size: 14px; + line-height: 20px; + border-left: 1px solid var(--text-secondary-color); + color: var(--text-primary-color); + + .timepointLabel + color: var(--text-secondary-color); + font-size: 12px; + + .warning-status + display: block; + position: absolute; + width: 50px; + height: 40px; + padding-left: 7px; + left: 15px; + top: 7px; + + .warning-border + display: block; + border: 2px solid #e29e4a; + border-radius: 36px; + padding: 6px 7px; + width: 40px; + height: 40px; + + svg + color: #e29e4a; + width: 22px; + height: 20px; + pointer-events: inherit; + +.tooltip-warning + &.in + opacity 1; + + .tooltip-inner + padding: 0 + + .warningTitle + text-align: left + background-color: var(--ui-gray-darkest); + padding: 7px 15px; + border-top-left-radius: 7px; + border-top-right-radius: 7px; + + .warningContent + text-align center + background-color: var(--ui-gray); + border-bottom-left-radius: 7px; + border-bottom-right-radius: 7px; + + ol + margin-top: auto; + padding-inline-start: 20px; + padding-top: 10px; + padding-bottom: 10px; + padding-left: 25px; + text-align: left; + + li + padding-right: 10px; diff --git a/packages/_components/src/components/measurementTable/MeasurementTableItem.js b/packages/_components/src/components/measurementTable/MeasurementTableItem.js new file mode 100644 index 000000000..b1e42e038 --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTableItem.js @@ -0,0 +1,162 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import { Icon } from './../../elements/Icon'; +import { OverlayTrigger } from './../overlayTrigger'; +import { Tooltip } from './../tooltip'; +import { TableListItem } from './../tableList/TableListItem.js'; + +import './MeasurementTableItem.styl'; + +class MeasurementTableItem extends Component { + static propTypes = { + measurementData: PropTypes.object.isRequired, + onItemClick: PropTypes.func.isRequired, + onRelabel: PropTypes.func, + onDelete: PropTypes.func, + onEditDescription: PropTypes.func, + itemClass: PropTypes.string, + itemIndex: PropTypes.number, + t: PropTypes.func, + }; + + render() { + const { warningTitle = '', hasWarnings } = this.props.measurementData; + + return ( + + {hasWarnings ? ( + +
    {this.props.t(warningTitle)}
    +
    {this.getWarningContent()}
    + + } + > +
    {this.getTableListItem()}
    +
    + ) : ( + {this.getTableListItem()} + )} +
    + ); + } + + getActionButton = (btnLabel, onClickCallback) => { + return ( + + ); + }; + + getTableListItem = () => { + const hasWarningClass = this.props.measurementData.hasWarnings + ? 'hasWarnings' + : ''; + + const actionButtons = []; + + if (typeof this.props.onRelabel === 'function') { + const relabelButton = this.getActionButton( + 'Relabel', + this.onRelabelClick + ); + actionButtons.push(relabelButton); + } + if (typeof this.props.onEditDescription === 'function') { + const descriptionButton = this.getActionButton( + 'Description', + this.onEditDescriptionClick + ); + actionButtons.push(descriptionButton); + } + if (typeof this.props.onDelete === 'function') { + const deleteButton = this.getActionButton('Delete', this.onDeleteClick); + actionButtons.push(deleteButton); + } + + return ( + +
    +
    + {this.props.t(this.props.measurementData.label)} +
    +
    {this.getDataDisplayText()}
    +
    {actionButtons}
    +
    +
    + ); + }; + + onItemClick = event => { + this.props.onItemClick(event, this.props.measurementData); + }; + + onRelabelClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onRelabel(event, this.props.measurementData); + }; + + onEditDescriptionClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onEditDescription(event, this.props.measurementData); + }; + + onDeleteClick = event => { + // Prevent onItemClick from firing + event.stopPropagation(); + + this.props.onDelete(event, this.props.measurementData); + }; + + getDataDisplayText = () => { + return this.props.measurementData.data.map((data, index) => { + return ( +
    + {data.displayText ? data.displayText : '...'} +
    + ); + }); + }; + + getWarningContent = () => { + const { warningList = '' } = this.props.measurementData; + + if (Array.isArray(warningList)) { + const listedWarnings = warningList.map((warn, index) => { + return
  • {warn}
  • ; + }); + + return
      {listedWarnings}
    ; + } else { + return {warningList}; + } + }; +} + +const connectedComponent = withTranslation('MeasurementTable')( + MeasurementTableItem +); +export { connectedComponent as MeasurementTableItem }; +export default connectedComponent; diff --git a/packages/_components/src/components/measurementTable/MeasurementTableItem.styl b/packages/_components/src/components/measurementTable/MeasurementTableItem.styl new file mode 100644 index 000000000..8f1e07bef --- /dev/null +++ b/packages/_components/src/components/measurementTable/MeasurementTableItem.styl @@ -0,0 +1,61 @@ +.measurementItem + .rowActions + background-color: var(--ui-gray-darker) + height: 0 + overflow: hidden + transition: all 0.3s ease + visibility: hidden + padding-left: 14px + + .btnAction + background-color: transparent; + border: none; + color: var(--default-color) + cursor: pointer + line-height: 35px + height: 35px + transition: all 0.3s ease + + &:hover, &:active + color: var(--text-primary-color) + + i + margin-right: 4px; + + &.selected + .rowActions + height: 35px + visibility: visible + + .measurementLocation + margin-left: 9px; + margin-top: 9px; + + .measurementDisplayText + display: inline-block; + margin-top: 9px; + margin-left: 9px; + padding-left: 9px; + width: 90px; + border-left: 1px solid var(--text-secondary-color); + color: var(--text-primary-color); + + .itemIndex + -webkit-box-sizing: initial; + -moz-box-sizing: initial; + box-sizing: initial; + + &.hasWarnings + .itemIndex + opacity: 1 + background-color: #e29e4a; + color: #fff; + + .warning-icon + display: block; + margin: 7px auto 0; + + svg + width: 22px; + height: 20px; + pointer-events: inherit; diff --git a/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx b/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx new file mode 100644 index 000000000..d85d87490 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/measurementTable.mdx @@ -0,0 +1,49 @@ +--- +name: Measurement Table +menu: Components +route: /components/measurement-table +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { MeasurementTable } from './../index.js' +import NameSpace from '../../../__docs__/NameSpace' +// +import measurements from './measurements.js' +import timepoints from './timepoints.js' +import warnings from './warnings.js' + +# Measurement Table + +## Basic usage + + + + {({ state, setState }) => ( +
    + alert('onItemClick')} + onRelabelClick={() => alert('onRelabelClick')} + onDeleteClick={() => alert('onDeleteClick')} + onEditDescriptionClick={() => alert('onEditDescriptionClick')} + /> +
    + )} +
    +
    + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/measurementTable/__docs__/measurements.js b/packages/_components/src/components/measurementTable/__docs__/measurements.js new file mode 100644 index 000000000..2a0507902 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/measurements.js @@ -0,0 +1,93 @@ +export default [ + { + maxMeasurements: 5, + groupName: 'Targets', + measurements: [ + { + measurementId: '123', + measurementNumber: '123', + itemNumber: 1, + label: 'Chest Wall Posterior', + hasWarnings: true, + warningTitle: 'Criteria nonconformities', + isSplitLesion: false, + warningList: [ + 'All measurements should have a location', + 'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR', + ], + data: [ + { + displayText: '25.7 x 12.9', + }, + { + displayText: '24.7 x 11.5', + }, + {}, + ], + }, + { + measurementId: '124', + measurementNumber: '124', + itemNumber: 2, + label: 'Bone Extremity', + data: [ + { + displayText: '24.7 x 11.1', + }, + { + displayText: '21.2 x 10.9', + }, + {}, + ], + }, + ], + }, + { + maxMeasurements: 3, + groupName: 'NonTargets', + measurements: [ + { + measurementId: '125', + measurementNumber: '125', + itemNumber: 1, + label: 'Chest Wall Single Site', + data: [ + { + displayText: 'MD', + }, + { + displayText: 'NM', + }, + {}, + ], + }, + { + measurementId: '126', + measurementNumber: '126', + itemNumber: 2, + label: 'Extremity Multiple Sites', + data: [ + { + displayText: 'CR', + }, + {}, + {}, + ], + }, + { + measurementId: '127', + measurementNumber: '127', + label: 'Extremity Site', + data: [ + { + displayText: 'CR', + }, + {}, + { + displayText: 'NM', + }, + ], + }, + ], + }, +]; diff --git a/packages/_components/src/components/measurementTable/__docs__/timepoints.js b/packages/_components/src/components/measurementTable/__docs__/timepoints.js new file mode 100644 index 000000000..e42b44156 --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/timepoints.js @@ -0,0 +1,14 @@ +export default [ + { + label: 'Follow-up 2', + date: '20-Dec-18', + }, + { + label: 'Follow-up 1', + date: '15-Jun-18', + }, + { + label: 'Baseline', + date: '10-Apr-18', + }, +]; diff --git a/packages/_components/src/components/measurementTable/__docs__/warnings.js b/packages/_components/src/components/measurementTable/__docs__/warnings.js new file mode 100644 index 000000000..a2da7f90d --- /dev/null +++ b/packages/_components/src/components/measurementTable/__docs__/warnings.js @@ -0,0 +1,6 @@ +export default { + warningList: [ + 'All measurements should have a location', + 'Nodal lesions must be >= 15mm short axis AND >= double the acquisition slice thickness by CT and MR', + ], +}; diff --git a/packages/_components/src/components/measurementTable/index.js b/packages/_components/src/components/measurementTable/index.js new file mode 100644 index 000000000..e70bd51eb --- /dev/null +++ b/packages/_components/src/components/measurementTable/index.js @@ -0,0 +1,2 @@ +export { MeasurementTable } from './MeasurementTable.js'; +export { MeasurementTableItem } from './MeasurementTableItem.js'; diff --git a/packages/_components/src/components/overlayTrigger/Fade.js b/packages/_components/src/components/overlayTrigger/Fade.js new file mode 100644 index 000000000..34a5cad79 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/Fade.js @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import Transition, { + ENTERED, + ENTERING, +} from 'react-transition-group/Transition'; + +const propTypes = { + /** + * Show the component; triggers the fade in or fade out animation + */ + in: PropTypes.bool, + + /** + * Wait until the first "enter" transition to mount the component (add it to the DOM) + */ + mountOnEnter: PropTypes.bool, + + /** + * Unmount the component (remove it from the DOM) when it is faded out + */ + unmountOnExit: PropTypes.bool, + + /** + * Run the fade in animation when the component mounts, if it is initially + * shown + */ + appear: PropTypes.bool, + + /** + * Duration of the fade animation in milliseconds, to ensure that finishing + * callbacks are fired even if the original browser transition end events are + * canceled + */ + timeout: PropTypes.number, + + /** + * Callback fired before the component fades in + */ + onEnter: PropTypes.func, + /** + * Callback fired after the component starts to fade in + */ + onEntering: PropTypes.func, + /** + * Callback fired after the has component faded in + */ + onEntered: PropTypes.func, + /** + * Callback fired before the component fades out + */ + onExit: PropTypes.func, + /** + * Callback fired after the component starts to fade out + */ + onExiting: PropTypes.func, + /** + * Callback fired after the component has faded out + */ + onExited: PropTypes.func, +}; + +const defaultProps = { + in: false, + timeout: 300, + mountOnEnter: false, + unmountOnExit: false, + appear: false, +}; + +const fadeStyles = { + [ENTERING]: 'in', + [ENTERED]: 'in', +}; + +class Fade extends React.Component { + render() { + const { className, children, ...props } = this.props; + + return ( + + {(status, innerProps) => + React.cloneElement(children, { + ...innerProps, + className: classNames( + 'fade', + className, + children.props.className, + fadeStyles[status] + ), + }) + } + + ); + } +} + +Fade.propTypes = propTypes; +Fade.defaultProps = defaultProps; + +export default Fade; diff --git a/packages/_components/src/components/overlayTrigger/Overlay.js b/packages/_components/src/components/overlayTrigger/Overlay.js new file mode 100644 index 000000000..e824045d5 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/Overlay.js @@ -0,0 +1,102 @@ +import classNames from 'classnames'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import { Overlay as BaseOverlay } from 'react-overlays'; +import elementType from 'prop-types-extra/lib/elementType'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import Fade from './Fade'; + +const propTypes = { + /** + * Set the visibility of the Overlay + */ + show: PropTypes.bool, + /** + * Specify whether the overlay should trigger onHide when the user clicks outside the overlay + */ + rootClose: PropTypes.bool, + /** + * A callback invoked by the overlay when it wishes to be hidden. Required if + * `rootClose` is specified. + */ + onHide: PropTypes.func, + + /** + * Use animation + */ + animation: PropTypes.oneOfType([PropTypes.bool, elementType]), + + /** + * Callback fired before the Overlay transitions in + */ + onEnter: PropTypes.func, + + /** + * Callback fired as the Overlay begins to transition in + */ + onEntering: PropTypes.func, + + /** + * Callback fired after the Overlay finishes transitioning in + */ + onEntered: PropTypes.func, + + /** + * Callback fired right before the Overlay transitions out + */ + onExit: PropTypes.func, + + /** + * Callback fired as the Overlay begins to transition out + */ + onExiting: PropTypes.func, + + /** + * Callback fired after the Overlay finishes transitioning out + */ + onExited: PropTypes.func, + + /** + * Sets the direction of the Overlay. + */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), +}; + +const defaultProps = { + animation: Fade, + rootClose: false, + show: false, + placement: 'right', +}; + +class Overlay extends React.Component { + render() { + const { animation, children, ...props } = this.props; + + const transition = animation === true ? Fade : animation || null; + + let child; + + if (!transition) { + child = cloneElement(children, { + className: classNames(children.props.className, 'in'), + }); + } else { + child = children; + } + + return ( + + {child} + + ); + } +} + +Overlay.propTypes = propTypes; +Overlay.defaultProps = defaultProps; + +const connectedComponent = withTranslation()(Overlay); +export { connectedComponent as Overlay }; +export default connectedComponent; diff --git a/packages/_components/src/components/overlayTrigger/OverlayTrigger.js b/packages/_components/src/components/overlayTrigger/OverlayTrigger.js new file mode 100644 index 000000000..b9216fcb3 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/OverlayTrigger.js @@ -0,0 +1,330 @@ +import contains from 'dom-helpers/query/contains'; +import React, { cloneElement } from 'react'; +import PropTypes from 'prop-types'; +import ReactDOM from 'react-dom'; +import warning from 'warning'; + +import { Overlay } from './Overlay'; + +import createChainedFunction from './createChainedFunction'; + +/** + * Check if value one is inside or equal to the of value + * + * @param {string} one + * @param {string|array} of + * @returns {boolean} + */ +function isOneOf(one, of) { + if (Array.isArray(of)) { + return of.indexOf(one) >= 0; + } + return one === of; +} + +const triggerType = PropTypes.oneOf(['click', 'hover', 'focus']); + +const propTypes = { + /** + * Specify which action or actions trigger Overlay visibility + */ + trigger: PropTypes.oneOfType([triggerType, PropTypes.arrayOf(triggerType)]), + + /** + * A millisecond delay amount to show and hide the Overlay once triggered + */ + delay: PropTypes.number, + /** + * A millisecond delay amount before showing the Overlay once triggered. + */ + delayShow: PropTypes.number, + /** + * A millisecond delay amount before hiding the Overlay once triggered. + */ + delayHide: PropTypes.number, + + // FIXME: This should be `defaultShow`. + /** + * The initial visibility state of the Overlay. For more nuanced visibility + * control, consider using the Overlay component directly. + */ + defaultOverlayShown: PropTypes.bool, + + /** + * An element or text to overlay next to the target. + */ + overlay: PropTypes.node.isRequired, + + /** + * A function to be called once the hide is triggered + */ + handleHide: PropTypes.func, + + /** + * @private + */ + onBlur: PropTypes.func, + /** + * @private + */ + onClick: PropTypes.func, + /** + * @private + */ + onFocus: PropTypes.func, + /** + * @private + */ + onMouseOut: PropTypes.func, + /** + * @private + */ + onMouseOver: PropTypes.func, + + // Overridden props from ``. + /** + * @private + */ + target: PropTypes.oneOf([null]), + /** + * @private + */ + onHide: PropTypes.oneOf([null]), + /** + * @private + */ + show: PropTypes.oneOf([null]), +}; + +const defaultProps = { + defaultOverlayShown: false, + trigger: ['hover', 'focus'], +}; + +class OverlayTrigger extends React.Component { + constructor(props, context) { + super(props, context); + + this.handleToggle = this.handleToggle.bind(this); + this.handleDelayedShow = this.handleDelayedShow.bind(this); + this.handleDelayedHide = this.handleDelayedHide.bind(this); + this.handleHide = createChainedFunction( + this.handleHide.bind(this), + props.handleHide + ); + + this.handleMouseOver = e => + this.handleMouseOverOut(this.handleDelayedShow, e, 'fromElement'); + this.handleMouseOut = e => + this.handleMouseOverOut(this.handleDelayedHide, e, 'toElement'); + + this._mountNode = null; + + this.state = { + show: props.defaultOverlayShown, + }; + } + + componentDidMount() { + this._mountNode = document.createElement('div'); + this.renderOverlay(); + } + + componentDidUpdate() { + this.renderOverlay(); + } + + componentWillUnmount() { + ReactDOM.unmountComponentAtNode(this._mountNode); + this._mountNode = null; + + clearTimeout(this._hoverShowDelay); + clearTimeout(this._hoverHideDelay); + } + + handleDelayedHide() { + if (this._hoverShowDelay != null) { + clearTimeout(this._hoverShowDelay); + this._hoverShowDelay = null; + return; + } + + if (!this.state.show || this._hoverHideDelay != null) { + return; + } + + const delay = + this.props.delayHide != null ? this.props.delayHide : this.props.delay; + + if (!delay) { + this.hide(); + return; + } + + this._hoverHideDelay = setTimeout(() => { + this._hoverHideDelay = null; + this.hide(); + }, delay); + } + + handleDelayedShow() { + if (this._hoverHideDelay != null) { + clearTimeout(this._hoverHideDelay); + this._hoverHideDelay = null; + return; + } + + if (this.state.show || this._hoverShowDelay != null) { + return; + } + + const delay = + this.props.delayShow != null ? this.props.delayShow : this.props.delay; + + if (!delay) { + this.show(); + return; + } + + this._hoverShowDelay = setTimeout(() => { + this._hoverShowDelay = null; + this.show(); + }, delay); + } + + handleHide() { + this.hide(); + } + + // Simple implementation of mouseEnter and mouseLeave. + // React's built version is broken: https://github.com/facebook/react/issues/4251 + // for cases when the trigger is disabled and mouseOut/Over can cause flicker + // moving from one child element to another. + handleMouseOverOut(handler, e, relatedNative) { + const target = e.currentTarget; + const related = e.relatedTarget || e.nativeEvent[relatedNative]; + + if ((!related || related !== target) && !contains(target, related)) { + handler(e); + } + } + + handleToggle() { + if (this.state.show) { + this.hide(); + } else { + this.show(); + } + } + + hide() { + this.setState({ show: false }); + } + + makeOverlay(overlay, props) { + return ( + + {overlay} + + ); + } + + show() { + this.setState({ show: true }); + } + + renderOverlay() { + ReactDOM.unstable_renderSubtreeIntoContainer( + this, + this._overlay, + this._mountNode + ); + } + + render() { + const { + trigger, + overlay, + children, + onBlur, + onClick, + onFocus, + onMouseOut, + onMouseOver, + ...props + } = this.props; + + delete props.delay; + delete props.delayShow; + delete props.delayHide; + delete props.defaultOverlayShown; + + const child = React.Children.only(children); + const childProps = child.props; + const triggerProps = {}; + + if (this.state.show) { + triggerProps['aria-describedby'] = overlay.props.id; + } + + // FIXME: The logic here for passing through handlers on this component is + // inconsistent. We shouldn't be passing any of these props through. + + triggerProps.onClick = createChainedFunction(childProps.onClick, onClick); + + if (isOneOf('click', trigger)) { + triggerProps.onClick = createChainedFunction( + triggerProps.onClick, + this.handleToggle + ); + } + + if (isOneOf('hover', trigger)) { + warning( + !(trigger === 'hover'), + '[react-bootstrap] Specifying only the `"hover"` trigger limits the ' + + 'visibility of the overlay to just mouse users. Consider also ' + + 'including the `"focus"` trigger so that touch and keyboard only ' + + 'users can see the overlay as well.' + ); + + triggerProps.onMouseOver = createChainedFunction( + childProps.onMouseOver, + onMouseOver, + this.handleMouseOver + ); + triggerProps.onMouseOut = createChainedFunction( + childProps.onMouseOut, + onMouseOut, + this.handleMouseOut + ); + } + + if (isOneOf('focus', trigger)) { + triggerProps.onFocus = createChainedFunction( + childProps.onFocus, + onFocus, + this.handleDelayedShow + ); + triggerProps.onBlur = createChainedFunction( + childProps.onBlur, + onBlur, + this.handleDelayedHide + ); + } + + this._overlay = this.makeOverlay(overlay, props); + + return cloneElement(child, triggerProps); + } +} + +OverlayTrigger.propTypes = propTypes; +OverlayTrigger.defaultProps = defaultProps; + +export { OverlayTrigger }; diff --git a/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx b/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx new file mode 100644 index 000000000..4fc48ee56 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/__docs__/OverlayTrigger.mdx @@ -0,0 +1,84 @@ +--- +name: Overlay Trigger +menu: Components +route: /components/overlay-trigger +--- + +import { Playground, Props } from 'docz' +import { Tooltip } from './../../tooltip' +import { OverlayTrigger } from './../index.js' + +# Overlay Trigger + +## Basic usage + +### On Hover + + + +
    + { + ['right', 'left', 'bottom', 'top'].map((placement) => { + return ( + + + Here I am! + + } + > + + + + ) + }) + } +
    + +
    + +### On Click + + +
    + {['right', 'left', 'bottom', 'top'].map(placement => { + return ( + + Here I am! + + } + > + + + ) + })} +
    +
    + +## API + + diff --git a/packages/_components/src/components/overlayTrigger/createChainedFunction.js b/packages/_components/src/components/overlayTrigger/createChainedFunction.js new file mode 100644 index 000000000..c5e231bc6 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/createChainedFunction.js @@ -0,0 +1,31 @@ +/** + * Safe chained function + * + * Will only create a new function if needed, + * otherwise will pass back existing functions or null. + * + * @param {function} functions to chain + * @returns {function|null} + */ +function createChainedFunction(...funcs) { + return funcs + .filter(f => f != null) + .reduce((acc, f) => { + if (typeof f !== 'function') { + throw new Error( + 'Invalid Argument Type, must only provide functions, undefined, or null.' + ); + } + + if (acc === null) { + return f; + } + + return function chainedFunction(...args) { + acc.apply(this, args); + f.apply(this, args); + }; + }, null); +} + +export default createChainedFunction; diff --git a/packages/_components/src/components/overlayTrigger/index.js b/packages/_components/src/components/overlayTrigger/index.js new file mode 100644 index 000000000..68a4d0874 --- /dev/null +++ b/packages/_components/src/components/overlayTrigger/index.js @@ -0,0 +1,2 @@ +export { Overlay } from './Overlay.js'; +export { OverlayTrigger } from './OverlayTrigger.js'; diff --git a/packages/_components/src/components/quickSwitch/QuickSwitch.js b/packages/_components/src/components/quickSwitch/QuickSwitch.js new file mode 100644 index 000000000..a29e57438 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/QuickSwitch.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { StudiesList } from './StudiesList.js'; +import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js'; +import { SeriesList } from './SeriesList.js'; + +import './QuickSwitch.styl'; + +export class QuickSwitch extends Component { + static propTypes = { + side: PropTypes.string, + studyListData: PropTypes.array.isRequired, + onSeriesSelected: PropTypes.func.isRequired, + seriesListData: PropTypes.array, + onStudySelected: PropTypes.func, + activeStudyInstanceUid: PropTypes.string, + activeDisplaySetInstanceUid: PropTypes.string, + }; + + constructor(props) { + super(props); + + this.state = { + seriesQuickSwitchOpen: false, + sideClass: this.props.side || '', + activeStudyInstanceUid: this.props.activeStudyInstanceUid, + activeDisplaySetInstanceUid: this.props.activeDisplaySetInstanceUid, + }; + } + + componentDidUpdate(prevProps) { + const props = this.props; + + if (props.activeStudyInstanceUid !== prevProps.activeStudyInstanceUid) { + this.setState({ + activeStudyInstanceUid: props.activeStudyInstanceUid, + }); + } + + if ( + props.activeDisplaySetInstanceUid !== + prevProps.activeDisplaySetInstanceUid + ) { + this.setState({ + activeDisplaySetInstanceUid: props.activeDisplaySetInstanceUid, + }); + } + } + + render() { + const quickSwitchClass = this.state.seriesQuickSwitchOpen + ? 'series-triggered' + : ''; + + return ( +
    +
    +
    Series
    +
    + {this.getSmallListItems()} + + + +
    +
    +
    +
    Study
    +
    + + + +
    +
    +
    + ); + } + + getSeriesItems = () => { + let seriesListData; + + if (this.props.seriesListData) { + seriesListData = this.props.seriesListData; + } else if (this.state.activeStudyInstanceUid) { + const study = this.props.studyListData.find( + study => study.studyInstanceUid === this.state.activeStudyInstanceUid + ); + + seriesListData = study.thumbnails; + } else { + seriesListData = this.props.studyListData[0].thumbnails; + } + + return seriesListData || []; + }; + + getSmallListItems = () => { + const seriesItems = this.getSeriesItems() || []; + return seriesItems.map((seriesData, index) => { + const active = + seriesData.displaySetInstanceUid === + this.state.activeDisplaySetInstanceUid; + return ( +
    + ); + }); + }; + + onStudyClick = studyDataSelected => { + if (this.props.onStudySelected) { + this.props.onStudySelected(studyDataSelected); + } + this.setState({ + activeStudyInstanceUid: studyDataSelected.studyInstanceUid, + seriesQuickSwitchOpen: true, + }); + }; + + onSeriesClick = seriesDataSelected => { + this.setState({ + activeDisplaySetInstanceUid: seriesDataSelected.displaySetInstanceUid, + }); + + this.props.onSeriesSelected(seriesDataSelected); + }; + + showSeriesSwitch = () => { + this.setState({ + seriesQuickSwitchOpen: true, + }); + }; + + hideSeriesSwitch = () => { + this.setState({ + seriesQuickSwitchOpen: false, + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/QuickSwitch.styl b/packages/_components/src/components/quickSwitch/QuickSwitch.styl new file mode 100644 index 000000000..b24314b14 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/QuickSwitch.styl @@ -0,0 +1,209 @@ +.series-quick-switch + position: relative; + + &:before + box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.8) + transition: opacity 0.3s ease + opacity: 0 + content: '' + + &:hover + z-index: 50 + + &:before + display: table; + opacity: 1 + content: '' + + &:not(.left):not(.right) + left: 50% + transform: translateX(-50%) + + &.left + border-right: 1px solid var(--ui-border-color) + right: 0 + + &.right + left: 0 + + .series-switch + float: right + + .study-switch + float: left + + .study-browser + left: auto + right: 0 + transform-origin: calc(100% - 1em) calc(1em + 17px) + + .series-browser + left: 0 + right: auto + transform-origin: 1em calc(1em + 15px) + + .thumbnails-wrapper + transform-origin: 0% 0% + + .ThumbnailEntry + float: left + + .series-item + float: left + + .title-label + color: var(--text-secondary-color) + font-size: 12px + font-weight: 500 + line-height: 12px + opacity: 1 + padding-bottom: 3px + text-align: center + transition: opacity 0.3s ease + + .series-switch + padding-right: 10px; + + .series-switch, + .study-switch + float: left + position: relative + + .study-switch:hover:after + content: '' + display: block + height: 100px + left: 0 + position: absolute + top: 0 + width: 300px + + .study-browser + left: 0 + padding-top: 60px; + max-height: calc(100vh - 60px); + transform-origin: 1em calc(1em + 17px) + width: 300px + + .study-browser-list + padding: 0 10px + + .series-browser + max-height: 100vh + max-width: 720px + right: 0 + transform-origin: calc(100% - 1em) calc(1em + 15px) + + .thumbnails-wrapper + transition: transform 0.3s ease + transform-origin: 100% 0% + + .ThumbnailEntry + float: right + + .series-details + opacity: 0 + transform: translateY(-100%) + transition: transform 0.3s ease, opacity 0.3s ease + + .scrollable + padding-top: 15px + transition: padding-bottom 0.3s ease + + .study-browser-series { + overflow hidden + } + + .series-browser, + .study-browser + min-height: 120px + opacity: 0 + position: absolute + top: 0 + transform: scale(0) + transition: transform 0.3s ease , opacity 0.3s ease + z-index: 3 + + .series-box, + .study-box + height: 57px + width: 57px + + .study-box + background-color: var(--ui-gray-dark) + border: solid 2px var(--ui-border-color-dark) + border-radius: 11px + + .series-item + background-color: var(--box-background-color) + border-radius: 3px + float: right + height: 15px + margin: 2px + transition: opacity 0.3s ease + width: 15px + + &.count + color: var(--text-primary-color) + background-color: transparent + font-size: 12px + font-weight: 500 + line-height: 17px + text-align: center + + &.active + background-color: var(--active-color) + + &.series-triggered .series-browser, + .series-switch:hover .series-browser, + .study-switch:hover .study-browser + opacity: 1 + transform: scale(1) + + &.series-triggered .series-item, + .series-switch:hover .title-label + opacity: 0 + + &.series-triggered + .thumbnails-wrapper + transform: scale(0.9) translateY(80px) + + .series-browser:not(:hover) + &>.scrollable + padding-bottom: 80px + + &>.scrollNav + opacity: 0 + + .series-switch:hover .series-browser + .thumbnails-wrapper + transform: scale(1) translateY(0) + + .ThumbnailEntry .series-details + opacity: 1 + transform: translateY(0) + transition-delay: 0.3s + + .study-browser-item.active .study-item-box + box-shadow: inset 0 0 0 3px var(--active-color) + +@media screen and (max-width: 1599px) + .series-quick-switch + + .series-box, + .study-box + width: 36px + height: 36px + + .study-box + border-radius: 7px + + .series-item + height: 10px + margin: 1px + width: 10px + + &.count + font-size: 10px + font-weight: 300 + line-height: 10px diff --git a/packages/_components/src/components/quickSwitch/SeriesList.js b/packages/_components/src/components/quickSwitch/SeriesList.js new file mode 100644 index 000000000..f38d95d63 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/SeriesList.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { ThumbnailEntry } from './../studyBrowser'; +import './SeriesList.styl'; + +export class SeriesList extends Component { + static propTypes = { + seriesItems: PropTypes.array.isRequired, + onClick: PropTypes.func.isRequired, + activeDisplaySetInstanceUid: PropTypes.string, + }; + + render() { + return ( + +
    +
    {this.getSeriesItems()}
    +
    +
    + ); + } + + getSeriesItems = () => { + return this.props.seriesItems.map((seriesData, index) => { + return ( + this.props.onClick(seriesData)} + /> + ); + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/SeriesList.styl b/packages/_components/src/components/quickSwitch/SeriesList.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/quickSwitch/StudiesItem.js b/packages/_components/src/components/quickSwitch/StudiesItem.js new file mode 100644 index 000000000..c43fe281c --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesItem.js @@ -0,0 +1,60 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './StudiesItem.styl'; + +export class StudiesItem extends Component { + static propTypes = { + onClick: PropTypes.func.isRequired, + studyData: PropTypes.object.isRequired, + active: PropTypes.bool, + }; + + render() { + const { + studyDate, + studyDescription, + modalities, + studyAvailable, + } = this.props.studyData; + const activeClass = this.props.active ? ' active' : ''; + const hasDescriptionAndDate = studyDate && studyDescription; + return ( +
    +
    +
    +
    + {modalities} +
    +
    +
    + {hasDescriptionAndDate ? ( + +
    {studyDate}
    +
    {studyDescription}
    +
    + ) : ( +
    + {studyAvailable ? ( + N/A + ) : ( + Click to load + )} +
    + )} +
    +
    +
    + ); + } + + getModalitiesStyle = () => { + return {}; + }; +} diff --git a/packages/_components/src/components/quickSwitch/StudiesItem.styl b/packages/_components/src/components/quickSwitch/StudiesItem.styl new file mode 100644 index 000000000..98c494844 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesItem.styl @@ -0,0 +1,133 @@ +$boxHoverBackgroundColor = #14191E +$boxHoverBorderColor = #2d4660 +$boxActiveBorderColor = #131D29 +$boxWidth = 50px +$borderThickness = 2px +$boxBorderRadius = 8px +$nestingMargin = 6px +$spacerX = 7px +$spacerY = 12px + +.studyBrowseItem + margin-bottom: 10px; + position: relative + // required transformation to make inner fixed elements relative to this one + transform: scale(1) + + &.active + .studyItemBox + border-color: var(--active-color) + + .studyModality + color: var(--primary-background-color) + & + &:before + &:after + background-color: var(--active-color) + border-color: $boxActiveBorderColor + + &:not(.active) .studyBrowserSeries + max-height: 0 !important + + .studySeriesContainer + opacity: 0 + transform: translateY(-100%) + + .studyBrowserSeries + overflow: hidden + transition: max-height 0.3s ease + + .studySeriesContainer + opacity: 1 + transition: opacity 0.3s ease, transform 0.3s ease + transform: translateY(0) + transform-origin: 50% 0% + +.studyItemBox + border: 3px solid var(--ui-border-color-dark) + border-radius: 12px + cursor: pointer + padding: $spacerY $spacerX ($spacerY - 1) + position: relative + transition: $sidebarTransition + background-color: $boxHoverBackgroundColor + z-index: 0 + + &:hover + border-color: var(--active-color) + + &.additional + color: var(--text-secondary-color) + + .studyModality + color: var(--primary-background-color) + & + &:before + &:after + background-color: var(--active-color) + + &.additional + color: var(--text-primary-color) + font-size: 16px + font-weight: normal + height: 91px + line-height: 91px + padding: 0 + text-align: center + + .studyText + font-size: 13px + left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3) + line-height: 14px + position: absolute + right: $spacerX + top: $spacerY + .studyDate + margin-top: 8px + color: var(--text-secondary-color) + .studyDescription + margin-top: 8px + color: var(--text-primary-color) + .studyAvailability + margin-top: 24px + color: var(--text-primary-color) + + .studyModality + color: var(--text-secondary-color) + font-size: 20px + line-height: $boxWidth + margin-left: $nestingMargin * 2 + margin-top: $nestingMargin * 2 + position: relative + + .studyModalityText + height: 100% + overflow: hidden + text-align: center + text-transform: uppercase + + & + &:before + &:after + border: $borderThickness solid var(--primary-background-color) + background-color: var(--box-background-color) + border-radius: $boxBorderRadius + height: $boxWidth + ($borderThickness * 2) + transition($sidebarTransition) + width: $boxWidth + ($borderThickness * 2) + + &:before + &:after + display: block + content: '' + position: absolute + + &:before + top: -($borderThickness + $nestingMargin) + left: -($borderThickness + $nestingMargin) + z-index: -1 + + &:after + top: -($borderThickness + ($nestingMargin * 2)) + left: -($borderThickness + ($nestingMargin * 2)) + z-index: -2 diff --git a/packages/_components/src/components/quickSwitch/StudiesList.js b/packages/_components/src/components/quickSwitch/StudiesList.js new file mode 100644 index 000000000..414b46944 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/StudiesList.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { StudiesItem } from './StudiesItem.js'; +import './StudiesList.styl'; + +export class StudiesList extends Component { + static propTypes = { + class: PropTypes.string, + studyListData: PropTypes.array.isRequired, + onClick: PropTypes.func.isRequired, + activeStudyInstanceUid: PropTypes.string, + }; + + render() { + return ( +
    + {this.getBrowserItems()} +
    + ); + } + + getBrowserItems = () => { + return this.props.studyListData.map((studyData, index) => { + return ( + this.props.onClick(studyData)} + /> + ); + }); + }; +} diff --git a/packages/_components/src/components/quickSwitch/StudiesList.styl b/packages/_components/src/components/quickSwitch/StudiesList.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx b/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx new file mode 100644 index 000000000..c14670d9d --- /dev/null +++ b/packages/_components/src/components/quickSwitch/__docs__/quickSwitch.mdx @@ -0,0 +1,61 @@ +--- +name: Quick Switch +menu: Components +route: /components/quick-switch +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { QuickSwitch } from './../index.js' +// +import studies from './studies.js' + +# Quick Switch + +## Basic usage + + + + {({ state, setState }) => ( + { + return !!study.active; + }).thumbnails} + onSeriesSelected={(selectedSeries) => { + const { studyListData } = state; + const selectedId = selectedSeries.displaySetInstanceUid; + + studyListData.forEach(study => { + study.thumbnails.forEach(series => { + series.active = series.displaySetInstanceUid === selectedId; + }) + }); + + setState({ studyListData }); + }} + onStudySelected={(selectedStudy) => { + const { studyListData } = state; + const selectedId = selectedStudy.studyInstanceUid; + + studyListData.forEach(study => { + study.active = study.studyInstanceUid === selectedId; + }); + + setState({ studyListData }); + }} + /> + )} + + + + + +## API + + diff --git a/packages/_components/src/components/quickSwitch/__docs__/studies.js b/packages/_components/src/components/quickSwitch/__docs__/studies.js new file mode 100644 index 000000000..c4c5250d9 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/__docs__/studies.js @@ -0,0 +1,76 @@ +export default [ + { + studyInstanceUid: '10001', + studyDescription: 'Anti-PD-1', + modalities: 'CT', + studyDate: '18-nov-2018', + active: false, + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg', + seriesDescription: 'Anti-PD-1_Lung', + seriesNumber: '2', + numImageFrames: 512, + displaySetInstanceUid: '10001-1', + stackPercentComplete: 30, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg', + seriesDescription: 'Anti-PD-1_MELANOMA', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + displaySetInstanceUid: '10001-2', + stackPercentComplete: 70, + }, + ], + }, + { + studyInstanceUid: '10002', + studyDescription: 'CPTAC', + modalities: 'CT', + studyDate: '16-aug-2017', + active: true, + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg', + seriesDescription: 'CPTAC-GBM', + active: true, + seriesNumber: '2', + numImageFrames: 512, + displaySetInstanceUid: '10002-1', + stackPercentComplete: 100, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg', + seriesDescription: 'CPTAC-CM', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-2', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg', + seriesDescription: 'CPTAC-HNSCC', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-3', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg', + seriesDescription: 'CPTAC-LSCC', + seriesNumber: '2', + instanceNumber: '1', + displaySetInstanceUid: '10002-4', + numImageFrames: 256, + }, + ], + }, +]; diff --git a/packages/_components/src/components/quickSwitch/index.js b/packages/_components/src/components/quickSwitch/index.js new file mode 100644 index 000000000..c92ec79d5 --- /dev/null +++ b/packages/_components/src/components/quickSwitch/index.js @@ -0,0 +1 @@ +export { QuickSwitch } from './QuickSwitch.js'; diff --git a/packages/_components/src/components/radioButtonList/RadioButtonList.css b/packages/_components/src/components/radioButtonList/RadioButtonList.css new file mode 100644 index 000000000..d7c6305dc --- /dev/null +++ b/packages/_components/src/components/radioButtonList/RadioButtonList.css @@ -0,0 +1,62 @@ +.ohif-radio-button-container { + --radio-button-dim: 20px; +} + +.ohif-radio-button-container input { + position: absolute; + opacity: 0; + z-index: 10000; + cursor: pointer; + height: inherit; + width: inherit; +} + +.ohif-radio-button-container { + display: block; + position: relative; + padding-left: 35px; + margin-bottom: 12px; + cursor: pointer; + font-size: 22px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.ohif-radio-button { + width: var(--radio-button-dim); + height: var(--radio-button-dim); + position: absolute; + top: 25%; + left: 0; + background-color: var(--ui-gray-lighter); + border-radius: 50%; + z-index: 1; +} + +.ohif-radio-button:hover { + background-color: var(--default-color); + border-radius: 100%; +} + +.ohif-radio-button:after { + content: ''; + position: absolute; + display: none; + width: 70%; + height: 70%; +} + +.ohif-radio-button-container .ohif-radio-button.ohif-selected:after { + display: block; + top: 14%; + left: 14%; + border-radius: 50%; + background: white; +} + +.ohif-radio-button-label { + font-size: 12px; + font-weight: 500; +} diff --git a/packages/_components/src/components/radioButtonList/RadioButtonList.js b/packages/_components/src/components/radioButtonList/RadioButtonList.js new file mode 100644 index 000000000..0bbcf8b59 --- /dev/null +++ b/packages/_components/src/components/radioButtonList/RadioButtonList.js @@ -0,0 +1,74 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './RadioButtonList.css'; + +export class RadioButtonList extends Component { + static className = 'RadioButtonList'; + + //TODO: Add fields to propTypes.description? + //These would be label (required), id (required), and checked (optional). + static propTypes = { + description: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + checked: PropTypes.bool, + }) + ), + }; + + constructor(props) { + super(props); + this.state = {}; + + for (let button of props.description) { + if (button.checked) { + this.state.checked = button.id; + } + } + } + + handleChange(e) { + this.setState({ checked: e.target.value }); + } + + render() { + let buttons = this.props.description.map(button => { + let input = ( + { + this.handleChange(e); + }} + value={button.id} + /> + ); + + //needed to style the custom radio check + let inputSpan; + if (this.state.checked === button.id) { + inputSpan = ( + {input} + ); + } else { + inputSpan = {input}; + } + + return ( + + + + ); + }); + + return ( +
    +
    {buttons}
    +
    + ); + } +} diff --git a/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx b/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx new file mode 100644 index 000000000..2ad2b205d --- /dev/null +++ b/packages/_components/src/components/radioButtonList/__docs__/radioButtonList.mdx @@ -0,0 +1,74 @@ +--- +name: Radio Button List +menu: Elements +route: /components/radio-button-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { RadioButtonList } from './../index' +import NameSpace from '../../../__docs__/NameSpace' + +#Radio Button List + +## Basic usage + + + + {({ state, setState }) => ( + +
    +
    {JSON.stringify(state, null, 2)}
    +
    +
    + +
    +
    + )} +
    +
    + +## API + + + +## Translation Namespace + + \ No newline at end of file diff --git a/packages/_components/src/components/radioButtonList/index.js b/packages/_components/src/components/radioButtonList/index.js new file mode 100644 index 000000000..f8361a3b0 --- /dev/null +++ b/packages/_components/src/components/radioButtonList/index.js @@ -0,0 +1 @@ +export { RadioButtonList } from './RadioButtonList.js'; diff --git a/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css new file mode 100644 index 000000000..fc0e7e509 --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.css @@ -0,0 +1,86 @@ +.RoundedButtonGroup { + --height: 25px; + + position: relative; + z-index: 0; +} + +.roundedButtonWrapper { + cursor: pointer; + display: inline-block; + float: left; + margin-left: -2px; + text-decoration: none; + text-align: center; +} + +.roundedButtonWrapper.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.RoundedButtonGroup .roundedButtonWrapper .roundedButton { + align-items: center; + background-color: var(--ui-gray-dark); + border: 2px solid var(--ui-border-color-dark); + color: var(--text-secondary-color); + display: flex; + font-size: 15px; + font-weight: 500; + justify-content: center; + height: var(--height); + line-height: var(--height); + padding: 0 22px; + position: relative; + text-transform: uppercase; + transition: var(--sidebar-transition); + z-index: 1; +} + +.roundedButtonWrapper + .roundedButton + svg + .roundedButtonWrapper + .roundedButton + span { + margin: 0 2px; +} + +.roundedButtonWrapper .roundedButton i { + line-height: 15px; + font-size: 15px; +} + +.bottomLabel { + color: var(--text-secondary-color); + font-size: 12px; + font-weight: 500; + line-height: 12px; + margin-top: 8px; +} + +.roundedButtonWrapper:first-child { + margin-left: 0; +} + +.RoundedButtonGroup .roundedButtonWrapper:first-child .roundedButton { + border-bottom-left-radius: var(--height); + border-top-left-radius: var(--height); +} + +.RoundedButtonGroup .roundedButtonWrapper:last-child .roundedButton { + border-bottom-right-radius: var(--height); + border-top-right-radius: var(--height); +} + +.roundedButtonWrapper:hover .roundedButton { + background-color: var(--box-background-color); + color: var(--ui-gray-dark); +} + +.roundedButtonWrapper.active .roundedButton { + background-color: var(--active-color); + border-color: var(--ui-border-color-active); + color: var(--ui-gray-dark); + z-index: 2; +} diff --git a/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js new file mode 100644 index 000000000..d17a65fb6 --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/RoundedButtonGroup.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { Icon } from './../../elements/Icon'; +import './RoundedButtonGroup.css'; + +// TODO: Rename to Toggle? +class RoundedButtonGroup extends Component { + static className = 'RoundedButtonGroup'; + + static propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.any, + text: PropTypes.string, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + name: PropTypes.string.isRequired, + }), + ]), + }) + ), + value: PropTypes.string, + onValueChanged: PropTypes.func, + }; + + static defaultProps = { + options: [], + value: null, + }; + + onClickOption = value => { + let newValue = value; + if (this.props.value === value) { + newValue = null; + } + + if (this.props.onValueChanged) { + this.props.onValueChanged(newValue); + } + }; + + render() { + let className = classnames( + RoundedButtonGroup.className, + 'clearfix center-table' + ); + + const buttons = this.props.options.map((option, index) => { + const className = classnames({ + roundedButtonWrapper: true, + noselect: true, + active: this.props.value === option.value, + }); + + const optionText = option.text && {option.text}; + const iconProps = + typeof option.icon === 'string' ? { name: option.icon } : option.icon; + + const bottomLabel = option.bottomLabel && ( +
    {option.bottomLabel}
    + ); + + return ( +
    this.onClickOption(option.value)} + > +
    + {optionText} + {iconProps && } +
    + {bottomLabel} +
    + ); + }); + + return
    {buttons}
    ; + } +} + +export { RoundedButtonGroup }; diff --git a/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx b/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx new file mode 100644 index 000000000..5d00b32fc --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/__docs__/roundedButtonGroup.mdx @@ -0,0 +1,51 @@ +--- +name: Rounded Button Group +menu: Components +route: /components/rounded-button-group +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { RoundedButtonGroup } from './../index.js'; + +# Rounded Button Group + +A basic styled toggle switch + +## Basic usage + + + + {({ state, setState }) => ( + +
    +
    value: {JSON.stringify(state.value, null, 2)}
    +
    + setState({ value })} + /> +
    + )} +
    +
    + +## API + + diff --git a/packages/_components/src/components/roundedButtonGroup/index.js b/packages/_components/src/components/roundedButtonGroup/index.js new file mode 100644 index 000000000..e33e0490c --- /dev/null +++ b/packages/_components/src/components/roundedButtonGroup/index.js @@ -0,0 +1 @@ +export { RoundedButtonGroup } from './RoundedButtonGroup.js'; diff --git a/packages/_components/src/components/selectTree/InputRadio.js b/packages/_components/src/components/selectTree/InputRadio.js new file mode 100644 index 000000000..e4de02a06 --- /dev/null +++ b/packages/_components/src/components/selectTree/InputRadio.js @@ -0,0 +1,37 @@ +import { Component } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class InputRadio extends Component { + static propTypes = { + value: PropTypes.string, + label: PropTypes.string.isRequired, + itemData: PropTypes.object.isRequired, + labelClass: PropTypes.string, + id: PropTypes.string.isRequired, + onSelected: PropTypes.func.isRequired, + }; + + render() { + const labelClass = this.props.labelClass ? this.props.labelClass : ''; + return ( + + ); + } + + onSelected = evt => { + this.props.onSelected(evt, this.props.itemData); + }; +} diff --git a/packages/_components/src/components/selectTree/SelectTree.js b/packages/_components/src/components/selectTree/SelectTree.js new file mode 100644 index 000000000..7a53e46ca --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTree.js @@ -0,0 +1,182 @@ +import './SelectTree.styl'; + +import React, { Component } from 'react'; + +import { Icon } from './../../elements/Icon'; +import InputRadio from './InputRadio.js'; +import PropTypes from 'prop-types'; +import SelectTreeBreadcrumb from './SelectTreeBreadcrumb.js'; +import cloneDeep from 'lodash.clonedeep'; + +export class SelectTree extends Component { + static propTypes = { + autoFocus: PropTypes.bool, + searchEnabled: PropTypes.bool, + selectTreeFirstTitle: PropTypes.string, + selectTreeSecondTitle: PropTypes.string, + /** Called when 'componentDidUpdate' is triggered */ + onComponentChange: PropTypes.func, + /** [{ label, value, items[]}] - An array of items than can be expanded to show child items */ + items: PropTypes.array.isRequired, + /** fn(evt, item) - Called when a child item is selected; receives event and selected item */ + onSelected: PropTypes.func.isRequired, + }; + + static defaultProps = { + searchEnabled: true, + autoFocus: true, + selectTreeFirstTitle: 'First Level itens', + items: [], + }; + + constructor(props) { + super(props); + + this.state = { + searchTerm: null, + currentNode: null, + value: null, + }; + } + + render() { + const treeItems = this.getTreeItems(); + + return ( +
    +
    + {this.headerItem()} +
    + {this.state.currentNode && ( + + )} +
    +
    {treeItems}
    +
    +
    +
    +
    + ); + } + + componentDidUpdate = () => { + if (this.props.onComponentChange) { + this.props.onComponentChange(); + } + }; + + isLeafSelected = item => item && !Array.isArray(item.items); + + getLabelClass = item => { + let labelClass = 'treeLeaf'; + if (this.state.searchTerm || Array.isArray(item.items)) { + labelClass = 'treeNode'; + } + return labelClass; + }; + + getTreeItems() { + const storageKey = 'SelectTree'; + let treeItems; + + if (this.state.searchTerm) { + const filteredItems = []; + const rawItems = cloneDeep(this.props.items); + rawItems.forEach(item => { + if (Array.isArray(item.items)) { + item.items.forEach(item => { + const label = item.label.toLowerCase(); + const searchTerm = this.state.searchTerm.toLowerCase(); + if (label.indexOf(searchTerm) !== -1) { + filteredItems.push(item); + } + }); + } + }); + treeItems = filteredItems; + } else if (this.state.currentNode) { + treeItems = cloneDeep(this.state.currentNode.items); + } else { + treeItems = cloneDeep(this.props.items); + } + + return treeItems.map((item, index) => { + let itemKey = index; + if (this.state.currentNode) { + itemKey += `_${this.state.currentNode.value}`; + } + return ( + + ); + }); + } + + headerItem = () => { + let title = this.props.selectTreeFirstTitle; + if (this.state.currentNode && this.props.selectTreeSecondTitle) { + title = this.props.selectTreeSecondTitle; + } + + return ( +
    +
    {title}
    + {this.props.searchEnabled && ( +
    +
    + +
    + +
    + )} +
    + ); + }; + + searchLocations = evt => { + this.setState({ + currentNode: null, + searchTerm: evt.currentTarget.value, + }); + }; + + onSelected = (event, item) => { + if (this.isLeafSelected(item)) { + this.setState({ + searchTerm: null, + currentNode: null, + value: null, + }); + return this.props.onSelected(event, item); + } else { + this.setState({ + currentNode: item, + }); + } + }; + + onBreadcrumbSelected = () => { + this.setState({ + currentNode: null, + }); + }; +} diff --git a/packages/_components/src/components/selectTree/SelectTree.styl b/packages/_components/src/components/selectTree/SelectTree.styl new file mode 100644 index 000000000..f5ddaf63f --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTree.styl @@ -0,0 +1,106 @@ +.selectTreeRoot + text-align: initial; + width: 320px; + max-height: inherit; + font-size: 14px; + line-height: 26px; + + .treeContent + max-height: inherit; + overflow: hidden; + border: 1px solid #c4c4c4; + border-radius: 5px; + background-color: #ffffff; + display: flex; + flex-direction: column; + + .treeHeader + background-color: #c0c0c0; + border-bottom: 1px solid #c4c4c4; + color: #303030; + padding: 5px; + text-align: center; + + .wrapperText + font-weight: bold; + padding-bottom: 2px; + + .wrapperSearch + width: 100%; + + .searchIcon + position: absolute; + width: 18px; + height: 15px; + margin: 7px 10px 13px 10px; + color: #a3a3a3; + + .searchInput + width: 100%; + padding: 6px 12px 6px 35px; + background-color: #fff; + background-image: none; + border: 1px solid #9b9b9b; + border-radius: 4px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; + + &:focus + outline: 0; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 8px rgba(102, 175, 233, 0.6); + + .wrapperText + white-space: nowrap; + cursor: pointer; + + .selectTreeBreadcrumb + .wrapperLabel + cursor: pointer; + padding: 0 12px; + height: 41px; + width: 100%; + line-height: 41px; + + .backIcon + padding-right: 10px; + + svg + width: 14px; + height: 14px; + position: relative; + top: 2px; + color: #457fb0; + + .treeOptions + overflow: auto; + height: 100%; + + .selectTreeBreadcrumb + .treeInputsWrapper .treeInputs .treeLeaf + border: none; + height: initial; + line-height: 26px; + + & > .treeInputsWrapper > .treeInputs .treeLeaf + border-bottom: 1px solid #ccc; + height: 41px; + line-height: 41px; + + .treeNode, .treeLeaf + cursor: pointer; + display: block; + overflow: hidden; + font-weight: normal; + box-shadow: 0 0 0 200px transparent; + padding: 0 12px; + + .treeNode + border-bottom: 1px solid #ccc; + height: 41px; + line-height: 41px; + + .radioLabel .radioInput + display: none; diff --git a/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js b/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js new file mode 100644 index 000000000..1bcb5ea20 --- /dev/null +++ b/packages/_components/src/components/selectTree/SelectTreeBreadcrumb.js @@ -0,0 +1,37 @@ +import { Component } from 'react'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { Icon } from './../../elements/Icon'; + +export default class SelectTreeBreadcrumb extends Component { + static propTypes = { + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + onSelected: PropTypes.func.isRequired, + }; + + render() { + return ( +
    + +
    + ); + } +} diff --git a/packages/_components/src/components/selectTree/__docs__/selectTree.mdx b/packages/_components/src/components/selectTree/__docs__/selectTree.mdx new file mode 100644 index 000000000..68ee9882e --- /dev/null +++ b/packages/_components/src/components/selectTree/__docs__/selectTree.mdx @@ -0,0 +1,42 @@ +--- +name: Select Tree +menu: Components +route: /components/select-tree +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { SelectTree } from './../index.js' + +import selectTreeItems from './selectTreeItems.js' + +# SelectTree + +## Basic usage + + + + {({ state, setState }) => ( + +
    +                {JSON.stringify(state.items, null, 2)}
    +            
    + + setState({ itemLabelSelected: item.label }) } + selectTreeFirstTitle="Select Tree Example Header" + autoFocus={false} + /> +
    + )} + +
    + +
    + +## API + + diff --git a/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js b/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js new file mode 100644 index 000000000..0c7466966 --- /dev/null +++ b/packages/_components/src/components/selectTree/__docs__/selectTreeItems.js @@ -0,0 +1,30 @@ +export default [ + { + label: 'A', + value: 'A', + items: [ + { + label: 'Abdominal Wall', + value: 'AbdominalWall', + }, + { + label: 'Adrenal Left', + value: 'AdrenalLeft', + }, + ], + }, + { + label: 'B', + value: 'B', + items: [ + { + label: 'Brain', + value: 'Brain', + }, + { + label: 'Breast', + value: 'Breast', + }, + ], + }, +]; diff --git a/packages/_components/src/components/selectTree/index.js b/packages/_components/src/components/selectTree/index.js new file mode 100644 index 000000000..58dad4997 --- /dev/null +++ b/packages/_components/src/components/selectTree/index.js @@ -0,0 +1 @@ +export { SelectTree } from './SelectTree'; diff --git a/packages/_components/src/components/simpleDialog/SimpleDialog.js b/packages/_components/src/components/simpleDialog/SimpleDialog.js new file mode 100644 index 000000000..2c8ce887d --- /dev/null +++ b/packages/_components/src/components/simpleDialog/SimpleDialog.js @@ -0,0 +1,69 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './SimpleDialog.styl'; + +class SimpleDialog extends Component { + static propTypes = { + children: PropTypes.node, + componentRef: PropTypes.any, + componentStyle: PropTypes.object, + rootClass: PropTypes.string, + isOpen: PropTypes.bool, + headerTitle: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + }; + + static defaultProps = { + isOpen: true, + componentStyle: {}, + rootClass: '', + }; + + render() { + return ( + + {this.props.isOpen && ( +
    +
    +
    + + x + +

    {this.props.headerTitle}

    +
    +
    {this.props.children}
    +
    + + +
    +
    +
    + )} +
    + ); + } + + onClose = event => { + event.preventDefault(); + event.stopPropagation(); + this.props.onClose(); + }; + + onConfirm = event => { + event.preventDefault(); + event.stopPropagation(); + this.props.onConfirm(); + }; +} + +export { SimpleDialog }; diff --git a/packages/_components/src/components/simpleDialog/SimpleDialog.styl b/packages/_components/src/components/simpleDialog/SimpleDialog.styl new file mode 100644 index 000000000..0b0147022 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/SimpleDialog.styl @@ -0,0 +1,108 @@ +@import './../../design/styles/common/button.styl' +@import './../../design/styles/common/global.styl' + +.simpleDialog + position: fixed; + top: 0px; + left: 0px; + z-index: 1000; + border: 0; + border-radius: 6px; + background-color: var(--ui-gray-darker); + + .header + border-bottom: 3px solid var(--primary-background-color); + padding: 19px 22px 17px; + position: relative; + + .title + font-size: 20px; + font-weight: 600; + line-height: 24px; + padding-right: 40px; + color: var(--text-secondary-color); + margin: 0; + + .closeBtn + -webkit-transform: translateY(-50%); + color: var(--text-secondary-color); + height: 20px; + opacity: 1; + overflow: hidden; + padding: 2px; + position: absolute; + right: 21px; + text-align: center; + text-shadow: none; + top: 50%; + transform: translateY(-50%); + transition: color 0.3s ease; + width: 20px; + + .closeIcon + color: transparent; + display: block; + font-size: 0; + height: 100%; + line-height: 0; + overflow: hidden; + position: relative; + width: 100%; + + &:after, &:before + content: ' '; + display: block; + height: 2px; + transition: background-color 0.3s ease; + width: 19px; + background-color: var(--text-secondary-color); + + &:before + left: 1px; + position: absolute; + top: 1px; + -webkit-transform: rotate(45deg); + transform: rotate(45deg); + -webkit-transform-origin: 1px 50%; + transform-origin: 1px 50%; + + &:after + right: 1px; + position: absolute; + top: 1px; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + -webkit-transform-origin: calc(100% - 1px) 50%; + transform-origin: calc(100% - 1px) 50%; + + .content + padding: 16px 22px 25px; + position: relative; + color: var(--text-primary-color); + + .simpleDialogSelect, .simpleDialogInput + background-color: var(--input-background-color); + color: var(--text-primary-color); + border: 0; + border-radius: 2px; + font-size: 14px; + height: 30px; + width: 100%; + line-height: 16px; + padding: 8px 9px 6px; + margin-top: 10px; + display: block; + + .simpleDialogLabelFor + font-size: 14px; + font-weight: 700; + line-height: 16px; + margin: 10px 0 0 0; + color: var(--text-primary-color); + + .footer + padding: 15px; + text-align: right; + + .btn + margin-left: 5px; diff --git a/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx b/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx new file mode 100644 index 000000000..764074b10 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/__docs__/simpleDialog.mdx @@ -0,0 +1,83 @@ +--- +name: Simple Dialog +menu: Components +route: /components/simple-dialog +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { SimpleDialog } from './../index.js' + +# Simple Dialog + +Its similar to a modal but we can pass its style as props and manipulate it as +we want. Initially used to display tool data changes in our Viewport. + +## Basic usage + + + + {({ state, setState }) => ( + +
    {JSON.stringify(state, null, 2) }
    + + {/* Toggle Dialog */} + + + setState({ isDialogOpen: false })} + onConfirm={() => setState({ + isDialogOpen: false, + // If you don't want to update external state until complete, + // This is where you could grab current field values and update parent + // With setState + // + // inputValue: this.input.value, + // selectValue: this.select.value + })} + componentStyle={state.dialogStyle} + onValueChanged={(value) => setState({value}) } + > + + setState({inputValue: e.target.value})} + /> + + + +
    + )} +
    + +
    + +## API + + diff --git a/packages/_components/src/components/simpleDialog/index.js b/packages/_components/src/components/simpleDialog/index.js new file mode 100644 index 000000000..df8327865 --- /dev/null +++ b/packages/_components/src/components/simpleDialog/index.js @@ -0,0 +1 @@ +export { SimpleDialog } from './SimpleDialog.js'; diff --git a/packages/_components/src/components/studyBrowser/DragPreview.js b/packages/_components/src/components/studyBrowser/DragPreview.js new file mode 100644 index 000000000..0334e3483 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/DragPreview.js @@ -0,0 +1,85 @@ +import React, { PureComponent, memo } from 'react'; +import { DragLayer } from 'react-dnd'; +import PropTypes from 'prop-types'; +import { ThumbnailEntry } from './ThumbnailEntry'; +import './DragPreview.styl'; + +let subscribedToOffsetChange = false; +let dragPreviewRef = null; + +const collector = monitor => { + if (!subscribedToOffsetChange) { + monitor.subscribeToOffsetChange(onOffsetChange(monitor)); + subscribedToOffsetChange = true; + } + + if (dragPreviewRef) { + const offset = + monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); + + if (offset) { + const transform = `translate(${offset.x}px, ${offset.y}px)`; + dragPreviewRef.style['transform'] = transform; + dragPreviewRef.style['-webkit-transform'] = transform; + } + } + + const item = monitor.getItem(); + let newItem = {}; + if (item) { + newItem = { + active: item.active, + altImageText: item.altImageText, + id: item.id, + imageSrc: item.imageSrc, + imageId: item.imageId, + instanceNumber: item.instanceNumber, + error: item.error, + numImageFrames: item.numImageFrames, + seriesDescription: item.seriesDescription, + seriesNumber: item.seriesNumber, + stackPercentComplete: item.stackPercentComplete, + }; + } + + return { + ...newItem, + isDragging: monitor.isDragging(), + }; +}; + +const onOffsetChange = monitor => () => { + if (!dragPreviewRef) return; + + const offset = + monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset(); + if (!offset) return; + + const transform = `translate(${offset.x}px, ${offset.y}px)`; + dragPreviewRef.style['transform'] = transform; + dragPreviewRef.style['-webkit-transform'] = transform; +}; + +const updateRef = ref => { + dragPreviewRef = ref; +}; + +class DragPreview extends PureComponent { + render() { + const { isDragging } = this.props; + if (!isDragging) return null; + return ( +
    +
    + +
    +
    + ); + } +} + +DragPreview.propTypes = { + isDragging: PropTypes.bool, +}; + +export default DragLayer(collector)(memo(DragPreview)); diff --git a/packages/_components/src/components/studyBrowser/DragPreview.styl b/packages/_components/src/components/studyBrowser/DragPreview.styl new file mode 100644 index 000000000..4866733d4 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/DragPreview.styl @@ -0,0 +1,14 @@ +.DragPreview + + .source-preview { + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.5); + position: fixed; + opacity: 0.5; + z-index: 9999; + left: 0; + top: 0; + transition: none; + pointer-events: none; + -webkit-touch-callout: none; + } diff --git a/packages/_components/src/components/studyBrowser/ExampleDropTarget.css b/packages/_components/src/components/studyBrowser/ExampleDropTarget.css new file mode 100644 index 000000000..3d5fc34c6 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ExampleDropTarget.css @@ -0,0 +1,30 @@ +.ExampleDropTarget { + display: flex; + justify-content: center; + flex-direction: column; + + min-height: 100px; + + border: 2px dashed black; + padding: 20px; + transition: 0.2s all ease; +} + +.ExampleDropTarget h4, +.ExampleDropTarger p { + text-align: center; + word-break: break-word; +} + +.ExampleDropTarget.can-drop { + border: 2px dashed #20a5d6; + background-color: #fafafa; + color: black; +} + +.ExampleDropTarget.hovered { + border: 2px dashed #20a5d6; + background-color: black; + color: #91b9cd; + cursor: copy; +} diff --git a/packages/_components/src/components/studyBrowser/ExampleDropTarget.js b/packages/_components/src/components/studyBrowser/ExampleDropTarget.js new file mode 100644 index 000000000..a8ce6327f --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ExampleDropTarget.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import { DropTarget } from 'react-dnd'; +import PropTypes from 'prop-types'; +import './ExampleDropTarget.css'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const divTarget = { + drop(props, monitor, component) { + // Note: For this example we use setState, but in + // OHIF we will update the redux store instead + const item = monitor.getItem(); + + component.setState({ + item: { + id: item.id, + seriesDescription: item.seriesDescription, + }, + }); + return { id: 'ExampleDropTarget' }; + }, +}; + +// TODO: Find out why we can't move this into the Example app instead. +// It looks like the context isn't properly shared. +class CustomDropTarget extends Component { + static className = 'ExampleDropTarget'; + + state = { + item: null, + }; + + static defaultProps = { + isOver: false, + }; + + static propTypes = { + connectDropTarget: PropTypes.func.isRequired, + canDrop: PropTypes.bool.isRequired, + isOver: PropTypes.bool.isRequired, + }; + + render() { + const { canDrop, isOver, connectDropTarget } = this.props; + const isActive = canDrop && isOver; + + let className = ExampleDropTarget.className; + + if (isActive) { + className += ' hovered'; + } else if (canDrop) { + className += ' can-drop'; + } + + return connectDropTarget( +
    +

    + {isActive + ? 'Release to drop' + : 'Drag / Drop something from the Study Browser here'} +

    +

    + {this.state.item && JSON.stringify(this.state.item)} +

    +
    + ); + } +} + +const collect = (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), +}); + +const ExampleDropTarget = DropTarget(Types.THUMBNAIL, divTarget, collect)( + CustomDropTarget +); + +export { ExampleDropTarget }; diff --git a/packages/_components/src/components/studyBrowser/ImageThumbnail.js b/packages/_components/src/components/studyBrowser/ImageThumbnail.js new file mode 100644 index 000000000..95a5e34a8 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ImageThumbnail.js @@ -0,0 +1,136 @@ +import './ImageThumbnail.styl'; + +import React, { PureComponent } from 'react'; + +import PropTypes from 'prop-types'; +import ViewportErrorIndicator from '../../viewer/ViewportErrorIndicator'; +import ViewportLoadingIndicator from '../../viewer/ViewportLoadingIndicator'; + +// TODO: How should we have this component depend on Cornerstone? +// - Passed in as a prop? +// - Set as external dependency? +// - Pass in the entire load and render function as a prop? +//import cornerstone from 'cornerstone-core'; + +/** + * Asynchronous wrapper around Cornerstone's renderToCanvas method. + * + * @param {HTMLElement} canvasElement An HTML element + * @param {Image} image A Cornerstone Image + * + * @return {Promise} A promise tracking the progress of the rendering. Resolves empty. + */ +function renderAsync(canvasElement, image) { + return new Promise((resolve, reject) => { + try { + cornerstone.renderToCanvas(canvasElement, image); + resolve(); + } catch (error) { + reject(error); + } + }); +} + +export default class ImageThumbnail extends PureComponent { + static propTypes = { + imageSrc: PropTypes.string, + imageId: PropTypes.string, + error: PropTypes.bool.isRequired, + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired, + stackPercentComplete: PropTypes.number.isRequired, + }; + + static defaultProps = { + error: false, + stackPercentComplete: 0, + width: 217, + height: 123, + }; + + constructor(props) { + super(props); + + this.canvas = React.createRef(); + + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + this.state = { + loading: renderIntoCanvas, + }; + } + + componentDidMount() { + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + if (renderIntoCanvas) { + const { imageId } = this.props; + const canvas = this.canvas.current; + + cornerstone.loadAndCacheImage(imageId).then( + image => { + renderAsync(canvas, image).then( + () => { + this.setState({ + loading: false, + }); + }, + error => { + // TODO: Set state? + throw new Error(error); + } + ); + }, + error => { + // TODO: Set state? + throw new Error(error); + } + ); + } + } + + render() { + let loadingOrError; + if (this.props.error) { + loadingOrError = ; + } else if (this.state.loading) { + loadingOrError = ; + } + + const showStackLoadingProgressBar = + this.props.stackPercentComplete !== undefined; + + const renderIntoCanvas = this.props.imageId && !this.props.imageSrc; + + return ( +
    +
    + {renderIntoCanvas ? ( + + ) : ( + {''} + )} +
    + {loadingOrError} + {showStackLoadingProgressBar && ( +
    +
    +
    + )} +
    + ); + } +} diff --git a/packages/_components/src/components/studyBrowser/ImageThumbnail.styl b/packages/_components/src/components/studyBrowser/ImageThumbnail.styl new file mode 100644 index 000000000..606ffb9d1 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ImageThumbnail.styl @@ -0,0 +1,70 @@ +:root { + --text-secondary-color: #91B9CD; + --active-color: #20A5D6; + --primary-background-color: black; + --ui-border-color-dark: #3C5D80 + --sidebar-transition: all 0.3s ease; +} + +.ThumbnailEntry.active .ImageThumbnail + border-color: var(--active-color); + box-shadow: none + transition: var(--sidebar-transition); + +.ImageThumbnail + background-color: var(--primary-background-color); + box-shadow: inset 0 0 0 1px var(--ui-border-color-dark); + border: 5px solid transparent + border-radius: 12px + height: 135px + margin: 0 auto + padding 5px + position: relative + transition: var(--sidebar-transition); + width: 217px + -moz-background-clip: padding + -webkit-background-clip: padding + background-clip: padding-box + +.image-thumbnail-canvas + height: 100% + overflow: hidden + display: flex + justify-content: center + + img + -webkit-user-drag: none + pointer-events: none + + +.thumbnailLoadingIndicator + display: none + pointer-events: none + color: var(--text-secondary-color); + height: 20px + width: 100% + top: 0 + left: 0 + right: 0 + bottom: 0 + margin: auto + position: absolute + + &.d-block + display: block; + + p + text-align: center + font-size: 10pt + +.image-thumbnail-progress-bar + position: relative + width: 100% + height: 3px + top: -5px + + .image-thumbnail-progress-bar-inner + height: 100% + width: 0 + border-radius: 5px + background-color: var(--active-color); diff --git a/packages/_components/src/components/studyBrowser/StudyBrowser.js b/packages/_components/src/components/studyBrowser/StudyBrowser.js new file mode 100644 index 000000000..59cda8c17 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/StudyBrowser.js @@ -0,0 +1,62 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ThumbnailEntry } from './ThumbnailEntry'; +import ThumbnailEntryDragSource from './ThumbnailEntryDragSource.js'; +import './StudyBrowser.styl'; + +class StudyBrowser extends Component { + static defaultProps = { + studies: [], + supportsDragAndDrop: true, + }; + + static propTypes = { + studies: PropTypes.array.isRequired, + supportsDragAndDrop: PropTypes.bool.isRequired, + onThumbnailClick: PropTypes.func, + onThumbnailDoubleClick: PropTypes.func, + }; + + render() { + const studies = this.props.studies; + + const thumbnails = studies.map((study, studyIndex) => { + return study.thumbnails.map((thumb, thumbIndex) => { + if (this.props.supportsDragAndDrop) { + return ( + + ); + } else { + return ( +
    + +
    + ); + } + }); + }); + + const components = thumbnails.flat(); + return ( +
    +
    {components}
    +
    + ); + } +} + +export { StudyBrowser }; diff --git a/packages/_components/src/components/studyBrowser/StudyBrowser.styl b/packages/_components/src/components/studyBrowser/StudyBrowser.styl new file mode 100644 index 000000000..c4915f826 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/StudyBrowser.styl @@ -0,0 +1,40 @@ +.StudyBrowser + float: left + height: 100% + width: 100% + overflow: hidden + background-color: black + padding-bottom: 20px + padding-top: 10px + + .scrollable-study-thumbnails + height: 100% + overflow-y: auto + overflow-x: hidden + padding-bottom: 50px + padding-right: 16px + padding-left: 4px + margin-right: -16px + + display: flex; + flex-direction: column; + + &::-webkit-scrollbar + display: none + + .ThumbnailEntryContainer + margin: 0 auto; + padding-bottom: 2.5rem; + + .noselect + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer */ + -khtml-user-select: none; /* KHTML browsers (e.g. Konqueror) */ + -webkit-user-select: none; /* Chrome, Safari, and Opera */ + -webkit-touch-callout: none; /* Disable Android and iOS callouts*/ + + +.draggable + cursor: copy + cursor: -webkit-grab + cursor: -moz-grab diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntry.js b/packages/_components/src/components/studyBrowser/ThumbnailEntry.js new file mode 100644 index 000000000..8756dc542 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntry.js @@ -0,0 +1,113 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ImageThumbnail from './ImageThumbnail'; +import './ThumbnailEntry.styl'; +import classnames from 'classnames'; + +class ThumbnailEntry extends Component { + static defaultProps = { + active: false, + error: false, + stackPercentComplete: 0, + }; + + static propTypes = { + id: PropTypes.string.isRequired, + imageSrc: PropTypes.string, + imageId: PropTypes.string, + error: PropTypes.bool.isRequired, + active: PropTypes.bool.isRequired, + stackPercentComplete: PropTypes.number, + /** + altImageText will be used when no imageId or imageSrc is provided. + It will be displayed inside the
    . This is useful when it is difficult + to make a preview for a type of DICOM series (e.g. DICOM-SR) + */ + altImageText: PropTypes.string, + seriesDescription: PropTypes.string, + seriesNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + instanceNumber: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + numImageFrames: PropTypes.number, + onDoubleClick: PropTypes.func, + onClick: PropTypes.func, + }; + + render() { + const hasInstanceNumber = this.props.instanceNumber !== undefined; + + let className = classnames('ThumbnailEntry noselect', { + active: this.props.active, + }); + const infoOnly = false; + + let contents = null; + if (this.props.imageSrc || this.props.imageId) { + contents = ( +
    + +
    + ); + } else if (this.props.altImageText) { + contents = ( +
    +

    {this.props.altImageText}

    +
    + ); + } + + return ( +
    + {contents} +
    +
    + {this.props.seriesDescription} +
    +
    +
    +
    S:
    +
    {this.props.seriesNumber}
    +
    + {hasInstanceNumber && ( +
    +
    I:
    +
    {this.props.instanceNumber}
    +
    + )} +
    +
    +
    +
    +
    {this.props.numImageFrames}
    +
    +
    +
    +
    + ); + } + + onClick = () => { + if (this.props.onClick) { + this.props.onClick(); + } + }; + + onDoubleClick = () => { + if (this.props.onDoubleClick) { + this.props.onDoubleClick(); + } + }; +} + +export { ThumbnailEntry }; diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl b/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl new file mode 100644 index 000000000..f7fc4d8df --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntry.styl @@ -0,0 +1,125 @@ +:root { + --series-count-background-color: #678696; +} + +.ThumbnailEntry + cursor: pointer + display: table + + .p-x-1 + padding: 0 1rem + + .alt-image-text + align-items: center; + justify-content: center; + background-color: var(--primary-background-color); + box-shadow: inset 0 0 0 1px var(--ui-border-color-dark); + border: 5px solid transparent; + border-radius: 12px; + height: 135px; + margin: 0 auto; + padding: 5px; + position: relative; + transition: var(--sidebar-transition); + width: 217px; + display: flex; + + h1 + text-align: center + color: var(--text-primary-color); + + .series-details + display: flex; + flex-direction: row; + color: var(--text-primary-color); + font-size: 14px + line-height: 1.3em + margin-top: 5px + margin-left: 1rem + margin-right: 1rem + max-width: 217px + min-height: 36px + position: relative + word-wrap: break-word + + .series-description + flex-grow: 1 + + &.info-only + .series-description + display: none + + .series-information + display: flex + flex-grow: 1 + float: none + max-width: none + padding-right: 0 + + .item + flex: 1 + text-align: center + + .icon, .value + display: inline + float: none + line-height: 25px + + .value + margin-left: 0 + width: auto + + .series-information + padding-right: 4px + max-width: 50px + + .item-frames .icon + height: 18px + + .value + color: var(--text-secondary-color); + display: inline-block + float: right + font-size: 12px + margin-left: 4px + overflow: hidden + text-overflow: ellipsis + white-space: nowrap + width: calc(100% - 15px) + + .icon + color: var(--active-color); + display: inline-block + float: left + font-size: 10px + font-weight: 900 + text-align: right + width: 11px + + div + background-color: var(--series-count-background-color); + margin-top: 6px + position: relative + + &:after + background-color: var(--active-color); + box-shadow: 1px 1px rgba(0, 0, 0, .115) + left: -5px + position: absolute + top: -5px + + & + &:after + border: 1px solid var(--series-count-background-color) + content: '' + display: inline-block + height: 11px + width: 11px + +.image-thumbnail-clone + z-index: 100000 + pointer-events: none + + &:hover + border-color: var(--hover-color); + box-shadow: none diff --git a/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js b/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js new file mode 100644 index 000000000..35146a275 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/ThumbnailEntryDragSource.js @@ -0,0 +1,64 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { DragSource } from 'react-dnd'; +import DragPreview from './DragPreview'; +import { ThumbnailEntry } from './ThumbnailEntry.js'; + +// Drag sources and drop targets only interact +// if they have the same string type. +const Types = { + THUMBNAIL: 'thumbnail', +}; + +const thumbnailSource = { + /*canDrag(props) { + return props.error === false; + },*/ + + beginDrag(props) { + return props; + }, + + endDrag(props, monitor) { + //const item = monitor.getItem(); + const dropResult = monitor.getDropResult(); + + if (dropResult) { + //console.log(`You dropped ${item.id} into ${dropResult.id}!`); + //console.log(item); + } + }, +}; + +class ThumbnailEntryDragSource extends Component { + static propTypes = { + connectDragSource: PropTypes.func.isRequired, + isDragging: PropTypes.bool.isRequired, + }; + + static defaultProps = { + isDragging: false, + }; + + render() { + const { connectDragSource } = this.props; + const dropEffect = 'copy'; + + return connectDragSource( +
    + + +
    , + { dropEffect } + ); + } +} + +const collect = (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), +}); + +export default DragSource(Types.THUMBNAIL, thumbnailSource, collect)( + ThumbnailEntryDragSource +); diff --git a/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js b/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js new file mode 100644 index 000000000..ee6a6d321 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/exampleStudies.js @@ -0,0 +1,75 @@ +export const studies = [ + { + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_Lung.jpg', + seriesDescription: 'Anti-PD-1_Lung', + active: true, + seriesNumber: '2', + numImageFrames: 512, + stackPercentComplete: 30, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/Anti-PD-1_MELANOMA.jpg', + seriesDescription: 'Anti-PD-1_MELANOMA', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + stackPercentComplete: 70, + }, + { + altImageText: 'SR', + seriesDescription: 'Imaging Measurement Report', + seriesNumber: '3', + stackPercentComplete: 100, + }, + ], + }, + { + thumbnails: [ + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-GBM.jpg', + seriesDescription: 'CPTAC-GBM', + active: true, + seriesNumber: '2', + numImageFrames: 512, + stackPercentComplete: 100, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-CM.jpg', + seriesDescription: 'CPTAC-CM', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-HNSCC.jpg', + seriesDescription: 'CPTAC-HNSCC', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + { + imageSrc: + 'https://raw.githubusercontent.com/crowds-cure/cancer/master/public/screenshots/CPTAC-LSCC.jpg', + seriesDescription: 'CPTAC-LSCC', + seriesNumber: '2', + instanceNumber: '1', + numImageFrames: 256, + }, + ], + }, +]; + +export function onThumbnailClick() { + console.warn('onThumbnailClick'); +} + +export function onThumbnailDoubleClick() { + console.warn('onThumbnailDoubleClick'); +} diff --git a/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx b/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx new file mode 100644 index 000000000..f9606fe84 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/studyBrowser.mdx @@ -0,0 +1,22 @@ +--- +name: Study Browser +menu: Components +route: /components/study-browser +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { WrappedStudyBrowser } from './wrappedStudyBrowser.js' +import { StudyBrowser } from './../index.js' + +# Study Browser + +## Basic usage + + + + + +## API + + diff --git a/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js b/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js new file mode 100644 index 000000000..9d2c29037 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/__docs__/wrappedStudyBrowser.js @@ -0,0 +1,40 @@ +import React, { Component } from 'react'; +import { DragDropContext } from 'react-dnd'; +import TouchBackend from 'react-dnd-touch-backend'; + +// +import { + studies, + onThumbnailClick, + onThumbnailDoubleClick, +} from './exampleStudies.js'; +import { ExampleDropTarget, StudyBrowser } from './../index.js'; + +class StudyBrowserContainer extends Component { + render() { + //const viewportData = [null, null, null, null]; + + return ( + + + + + ); + } +} + +// Note: +// Normally, the top level APP component is wrapped with the DragDropContext +// We wrap this component to create a simple/local example. +const WrappedStudyBrowser = DragDropContext( + TouchBackend({ enableMouseEvents: true }), + null, + true +)(StudyBrowserContainer); + +// http://react-dnd.github.io/react-dnd/docs/api/drag-drop-context +export { WrappedStudyBrowser }; diff --git a/packages/_components/src/components/studyBrowser/index.js b/packages/_components/src/components/studyBrowser/index.js new file mode 100644 index 000000000..9ed1c3194 --- /dev/null +++ b/packages/_components/src/components/studyBrowser/index.js @@ -0,0 +1,3 @@ +export { ExampleDropTarget } from './ExampleDropTarget.js'; +export { StudyBrowser } from './StudyBrowser.js'; +export { ThumbnailEntry } from './ThumbnailEntry.js'; diff --git a/packages/_components/src/components/studyList/CustomDateRangePicker.js b/packages/_components/src/components/studyList/CustomDateRangePicker.js new file mode 100644 index 000000000..b850685e1 --- /dev/null +++ b/packages/_components/src/components/studyList/CustomDateRangePicker.js @@ -0,0 +1,99 @@ +// If you want to continue using CSS stylesheets and classes... +// https://github.com/airbnb/react-dates#initialize +import 'react-dates/initialize'; +import 'react-dates/lib/css/_datepicker.css'; + +import React from 'react'; +import PropTypes from 'prop-types'; +import { DateRangePicker } from 'react-dates'; + +import './CustomDateRangePicker.styl'; + +export default class CustomDateRangePicker extends React.Component { + static propTypes = { + presets: PropTypes.arrayOf( + PropTypes.shape({ + text: PropTypes.string, + start: PropTypes.required, + end: PropTypes.required, + }) + ), + }; + + constructor(props) { + super(props); + this.state = { + startDate: this.props.startDate, + endDate: this.props.endDate, + }; + + this.renderDatePresets = this.renderDatePresets.bind(this); + this.onDatesChange = this.onDatesChange.bind(this); + } + + onDatesChange({ startDate, endDate, preset }) { + this.setState({ startDate, endDate }); + if (this.props.onDatesChange) { + this.props.onDatesChange({ startDate, endDate, preset }); + } + } + + renderDatePresets() { + const { presets } = this.props; + const { startDate, endDate } = this.state; + + return ( +
    + {presets.map(({ text, start, end }) => { + const isSelected = startDate === start && endDate === end; + + return ( + + ); + })} +
    + ); + } + + render() { + let { + autoFocus, + autoFocusEndDate, + initialStartDate, + initialEndDate, + stateDateWrapper, + onDatesChange, + startDate, + endDate, + presets, + ...dateRangePickerProps + } = this.props; + + return ( +
    + +
    + ); + } +} diff --git a/packages/_components/src/components/studyList/CustomDateRangePicker.styl b/packages/_components/src/components/studyList/CustomDateRangePicker.styl new file mode 100644 index 000000000..1112a0c90 --- /dev/null +++ b/packages/_components/src/components/studyList/CustomDateRangePicker.styl @@ -0,0 +1,84 @@ +.CalendarDay__selected_span { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border: 1px solid; + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__default:hover { + color: var(--calendar-main-color); +} + +.CalendarDay__selected_span:active, .CalendarDay__selected_span:hover { + background: var(--calendar-day-active-hover-background-color); + border-color: var(--calendar-day-border-color); + color: var(--calendar-day-color); +} + +.CalendarDay__selected { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__selected:hover { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color: var(--calendar-day-border-color); +} + +.CalendarDay__hovered_span:hover, +.CalendarDay__hovered_span { + background: var(--calendar-main-color); + color: var(--calendar-day-color); + border-color:var(--calendar-day-color); +} + +.DateInput { + width: 97px +} + +.DateInput_input { + background-color: var(--input-background-color) + border-color: var(--input-background-color) + color: var(--input-placeholder-color) + height: 38px + font-size: 10pt +} + +.DateRangePickerInput { + background-color: var(--input-background-color) + border-color: var(--input-background-color) + color: var(--input-placeholder-color) + height: 40px +} + +.PresetDateRangePicker_panel: { + padding: 0 22px 11px 22px; +} + +.PresetDateRangePicker_button { + position: relative; + height: 100%; + textAlign: center; + background: none; + border: 2px solid var(--calendar-main-color); + color: var(--calendar-main-color); + padding: 4px 12px; + marginRight: 8; + font: inherit; + fontWeight: 700; + lineHeight: normal; + overflow: visible; + boxSizing: border-box; + cursor: pointer; + + :active: { + outline: 0; + } +} + +.PresetDateRangePicker_button__selected: { + color: var(--calendar-day-color); + background: var(--calendar-main-color); +}; diff --git a/packages/_components/src/components/studyList/PaginationArea.js b/packages/_components/src/components/studyList/PaginationArea.js new file mode 100644 index 000000000..3c4fb7997 --- /dev/null +++ b/packages/_components/src/components/studyList/PaginationArea.js @@ -0,0 +1,109 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import './PaginationArea.styl'; + +class PaginationArea extends PureComponent { + static defaultProps = { + pageOptions: [5, 10, 25, 50, 100], + rowsPerPage: 25, + currentPage: 0, + }; + + static propTypes = { + pageOptions: PropTypes.array.isRequired, + rowsPerPage: PropTypes.number.isRequired, + currentPage: PropTypes.number.isRequired, + nextPageFunc: PropTypes.func, + prevPageFunc: PropTypes.func, + onRowsPerPageChange: PropTypes.func, + recordCount: PropTypes.number.isRequired, + }; + + nextPage = () => { + this.props.nextPageFunc(this.props.currentPage); + }; + + prevPage = () => { + this.props.prevPageFunc(this.props.currentPage); + }; + + onRowsPerPageChange = event => { + this.props.onRowsPerPageChange(parseInt(event.target.value)); + }; + + renderPaginationButtons() { + return ( +
    +
    + +
    +
    + ); + } + + renderRowsPerPageDropdown() { + return ( +
    + Show + + rows per page +
    + ); + } + + render() { + return ( +
    +
    +
    +
    + {this.renderRowsPerPageDropdown()} +
    +
    +
    + {this.renderPaginationButtons()} +
    +
    +
    +
    +
    + ); + } +} + +export { PaginationArea }; diff --git a/packages/_components/src/components/studyList/PaginationArea.styl b/packages/_components/src/components/studyList/PaginationArea.styl new file mode 100644 index 000000000..9a918c00f --- /dev/null +++ b/packages/_components/src/components/studyList/PaginationArea.styl @@ -0,0 +1,51 @@ +.pagination-area + color: var(--text-secondary-color); + font-size: 13px + font-weight: normal !important + + label + font-weight: normal + + select + margin: 5px + background-color: var(--primary-background-color) + color: white + + .rows-per-page label.wrapperLabel + display: inline-table !important + margin: 0 4px + + select + margin: 0px 4px 0px 4px + width: 42px + + .page-buttons + margin: 0 + text-align: right + + label + font-weight: normal + + ul.pagination-control + margin: 0 + + li + display: table-cell + padding: 5px 2px + + button + padding: 4px 8px + background-color: var(--primary-background-color) + border-color: var(--ui-gray) + color: var(--ui-gray-darkest) + color: white + text-decoration: none + + &:hover:enabled + color: var(--active-color) + + .active + button + background-color: var(--ui-gray) + border-color: #ddd + color: white diff --git a/packages/_components/src/components/studyList/StudyList.js b/packages/_components/src/components/studyList/StudyList.js new file mode 100644 index 000000000..1f9c7ca8e --- /dev/null +++ b/packages/_components/src/components/studyList/StudyList.js @@ -0,0 +1,428 @@ +import './StudyList.styl'; + +import React, { Component } from 'react'; + +import CustomDateRangePicker from './CustomDateRangePicker.js'; +import { Icon } from './../../elements/Icon'; +import { PaginationArea } from './PaginationArea.js'; +import PropTypes from 'prop-types'; +import { StudyListLoadingText } from './StudyListLoadingText.js'; +import { StudylistToolbar } from './StudyListToolbar.js'; +import { isInclusivelyBeforeDay } from 'react-dates'; +import moment from 'moment'; +import debounce from 'lodash.debounce'; + +const today = moment(); +const lastWeek = moment().subtract(7, 'day'); +const lastMonth = moment().subtract(1, 'month'); + +class StudyList extends Component { + static propTypes = { + studies: PropTypes.array.isRequired, + onSelectItem: PropTypes.func.isRequired, + onSearch: PropTypes.func.isRequired, + currentPage: PropTypes.number, + rowsPerPage: PropTypes.number, + studyListDateFilterNumDays: PropTypes.number, + studyListFunctionsEnabled: PropTypes.bool, + defaultSort: PropTypes.shape({ + field: PropTypes.string.isRequired, + order: PropTypes.oneOf(['desc', 'asc']).isRequired, + }), + onImport: PropTypes.func, + pageOptions: PropTypes.array, + }; + + static defaultProps = { + currentPage: 0, + rowsPerPage: 25, + studyListDateFilterNumDays: 7, + }; + + static studyDatePresets = [ + { + text: 'Today', + start: today, + end: today, + }, + { + text: 'Last 7 days', + start: lastWeek, + end: today, + }, + { + text: 'Last 30 days', + start: lastMonth, + end: today, + }, + ]; + + constructor(props) { + super(props); + + const sortData = { + field: undefined, + order: undefined, + }; + + // init from props + if (props.defaultSort) { + sortData.field = props.defaultSort.field; + // todo: -1, 0, 1? + sortData.order = props.defaultSort.order; // asc, desc + } + + this.defaultStartDate = moment().subtract( + this.props.studyListDateFilterNumDays, + 'days' + ); + this.defaultEndDate = moment(); + + this.state = { + loading: false, + error: false, + searchData: { + sortData, + currentPage: this.props.currentPage, + rowsPerPage: this.props.rowsPerPage, + studyDateFrom: this.defaultStartDate, + studyDateTo: this.defaultEndDate, + }, + highlightedItem: '', + }; + + this.getChangeHandler = this.getChangeHandler.bind(this); + this.getBlurHandler = this.getBlurHandler.bind(this); + this.onInputKeydown = this.onInputKeydown.bind(this); + this.nextPage = this.nextPage.bind(this); + this.prevPage = this.prevPage.bind(this); + this.onRowsPerPageChange = this.onRowsPerPageChange.bind(this); + this.delayedSearch = debounce(this.search, 250); + } + + getChangeHandler(key) { + return event => { + this.delayedSearch.cancel(); + this.setSearchData(key, event.target.value, this.delayedSearch); + }; + } + + getBlurHandler(key) { + return event => { + this.delayedSearch.cancel(); + this.setSearchData(key, event.target.value, this.search); + }; + } + + setSearchData(key, value, callback) { + const searchData = this.state.searchData; + searchData[key] = value; + this.setState({ searchData }, callback); + } + + setSearchDataBatch(keyValues, callback) { + const searchData = this.state.searchData; + + Object.keys(keyValues).forEach(key => { + searchData[key] = keyValues[key]; + }); + + this.setState({ searchData }, callback); + } + + async onInputKeydown(event) { + if (event.key === 'Enter') { + event.preventDefault(); + event.stopPropagation(); + + this.delayedSearch.cancel(); + // reset the page because user is doing a new search + this.setSearchData('currentPage', 0, this.search); + } + } + + async search() { + try { + this.setState({ loading: true, error: false }); + await this.props.onSearch(this.state.searchData); + } catch (error) { + this.setState({ error: true }); + throw new Error(error); + } finally { + this.setState({ loading: false }); + } + } + + renderNoMachingResults() { + if (!this.props.studies.length && !this.state.error) { + return
    No matching results
    ; + } + } + + renderHasError() { + if (this.state.error) { + return ( +
    There was an error fetching studies
    + ); + } + } + + renderIsLoading() { + if (this.state.loading) { + return ( +
    + +
    + ); + } + } + + nextPage(currentPage) { + currentPage = currentPage + 1; + this.delayedSearch.cancel(); + this.setSearchData('currentPage', currentPage, this.search); + } + + prevPage(currentPage) { + currentPage = currentPage - 1; + this.delayedSearch.cancel(); + this.setSearchData('currentPage', currentPage, this.search); + } + + onRowsPerPageChange(rowsPerPage) { + this.delayedSearch.cancel(); + this.setSearchDataBatch({ rowsPerPage, currentPage: 0 }, this.search); + } + + onSortClick(field) { + return () => { + let order; + const sort = this.state.searchData.sortData; + const isSortedField = sort.field === field; + + if (isSortedField) { + if (sort.order === 'asc') { + order = 'desc'; + } else { + order = undefined; + field = undefined; + } + } else { + order = 'asc'; + } + + this.delayedSearch.cancel(); + this.setSearchData('sortData', { field, order }, this.search); + }; + } + + onHighlightItem(studyItemUid) { + this.setState({ highlightedItem: studyItemUid }); + } + + renderTableRow(study) { + return ( + { + // middle/wheel click + if (event.button === 1) { + this.props.onSelectItem(study.studyInstanceUid); + } + }} + onClick={() => { + this.onHighlightItem(study.studyInstanceUid); + this.props.onSelectItem(study.studyInstanceUid); + }} + > + + {study.patientName || '(empty)'} + + + {study.patientId} + {study.accessionNumber} + {study.studyDate} + {study.modalities} + {study.studyDescription} + + ); + } + + render() { + const tableMeta = { + patientName: { + displayText: 'Patient Name', + sort: 0, + }, + patientId: { + displayText: 'MRN', + sort: 0, + }, + accessionNumber: { + displayText: 'Accession #', + sort: 0, + }, + studyDate: { + displayText: 'Study Date', + inputType: 'date-range', + sort: 0, + }, + modalities: { + displayText: 'Modality', + sort: 0, + }, + studyDescription: { + displayText: 'Description', + sort: 0, + }, + }; + + // Apply sort + const sortedFieldName = this.state.searchData.sortData.field; + const sortedField = tableMeta[sortedFieldName]; + + if (sortedField) { + const sortOrder = this.state.searchData.sortData.order; + sortedField.sort = sortOrder === 'asc' ? 1 : 2; + } + + // Sort Icons + const sortIcons = ['sort', 'sort-up', 'sort-down']; + + return ( +
    +
    +
    Study List
    +
    + {this.props.studies.length} +
    +
    + { + + } +
    +
    +
    +
    + + + + {Object.keys(tableMeta).map((fieldName, i) => { + const field = tableMeta[fieldName]; + + return ( + + + + ); + })} + + + + {this.props.studies.map(study => { + return this.renderTableRow(study); + })} + +
    +
    + {field.displayText} + +
    + {!field.inputType && ( + + )} + {field.inputType === 'date-range' && ( +
    + + !isInclusivelyBeforeDay(day, moment()) + } + onDatesChange={({ + startDate, + endDate, + preset = false, + }) => { + if ( + startDate && + endDate && + (this.state.focusedInput === 'endDate' || + preset) + ) { + this.setSearchDataBatch( + { + studyDateFrom: startDate.toDate(), + studyDateTo: endDate.toDate(), + }, + this.search + ); + this.setState({ focusedInput: false }); + } else if (!startDate && !endDate) { + this.setSearchDataBatch( + { + studyDateFrom: null, + studyDateTo: null, + }, + this.search + ); + } + }} + focusedInput={this.state.focusedInput} + onFocusChange={focusedInput => { + this.setState({ focusedInput }); + }} + /> +
    + )} +
    + + {this.renderIsLoading()} + {this.renderHasError()} + {this.renderNoMachingResults()} + + +
    +
    + ); + } +} + +export { StudyList }; diff --git a/packages/_components/src/components/studyList/StudyList.styl b/packages/_components/src/components/studyList/StudyList.styl new file mode 100644 index 000000000..9cefa7a5d --- /dev/null +++ b/packages/_components/src/components/studyList/StudyList.styl @@ -0,0 +1,282 @@ +@import './../../design/styles/common/form.styl' +@import './../../design/styles/common/global.styl' + +$study-list-padding = 8%; +$study-list-toolbar-height = 75px; +$body-cell-height = 40px; + +$tablePaddingMediumScreen = 5px +$tablePaddingBigScreen = 3% +$tablePaddingLargeScreen = 5% + +// TODO: move this to a common mixin files +placeholder-color(c) + &::-webkit-input-placeholder + color: c + &:-moz-placeholder + color: c + &::-moz-placeholder + color: c + &:-ms-input-placeholder + color: c + +.DateRangePicker + height: 40px + margin: 0 5px 20px 5px + cursor: pointer + border: none + position: block + +.studyListToolbar + background-color: var(--ui-gray-darker) + height: $study-list-toolbar-height + margin-bottom: 2px + padding: 0 $study-list-padding + + &>div + display: inline-block + + .header + font-size: 22px + font-weight: 300 + color: var(--table-text-secondary-color) + line-height: $study-list-toolbar-height + + .studylistToolbar + height: $study-list-toolbar-height + line-height: $study-list-toolbar-height + + .studyCount + color: var(--large-numbers-color) + font-size: 40px + font-weight: 100 + line-height: $study-list-toolbar-height + +.theadBackground + height: 121px + position: absolute + width: 100% + + &:before, &:after + content: '' + display: block + left: 0 + position: absolute + width: 100% + + &:before + background-color: var(--ui-gray-darker) + height: 100% + top: 0 + z-index: 1 + + &:after + background-color: var(--ui-border-color-active) + bottom: -1px + height: 1px + z-index: 2 + +#studyListContainer + width: 100% + padding: 0 $study-list-padding + position: absolute + z-index: 2 + + .loading-text + color: var(--table-text-secondary-color) + font-size: 30px + + .notFound + color: var(--table-text-secondary-color) + font-size: 30px + font-weight: 200 + text-align: center + + table#tblStudyList + width: 100%; + max-width: 100%; + margin-bottom: 20px; + border-spacing: 0; + border-collapse: collapse; + + > tr + height: 20px + + > thead + white-space: nowrap + + tr + th + padding: 0 + border-bottom: 1px solid var(--ui-border-color-active); + width: 100%; + text-align: left; + border-top: 0; + + &.studyDate + min-width: 230px + + .display-text + display: inline-block + cursor: pointer + width: 100% + min-width: 95px + margin: 0 auto + color: var(--table-text-primary-color) + font-weight: 400 + padding: 20px + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + user-select: none + + span + font-size: 15px + float: left + + i + margin: 0 5px + + &:hover + color: var(--active-color) + + &.active, &:active + color: var(--active-color) + + input.studylist-search + height: 40px + margin: 0 5px 20px 5px + padding: 0 20px + cursor: pointer + border: none + background-color: var(--input-background-color) + color: var(--input-placeholder-color) + font-size: 10pt + font-weight: normal + width: calc(100% - 10px) + border-radius: 4px + + transition(all 0.15s ease) + placeholder-color(var(--input-placeholder-color)) + + &.invisible + visibility: hidden + + &:active, &:hover + background-color: var(--input-background-color) + + ::-webkit-datetime-edit-year-field:not([aria-valuenow]), + ::-webkit-datetime-edit-month-field:not([aria-valuenow]), + ::-webkit-datetime-edit-day-field:not([aria-valuenow]) + color: transparent + + > tbody + tr + padding: 5px + background-color: black + + &:nth-child(even) + background-color: var(--ui-gray-darker) + + td + padding: 8px; + height: $body-cell-height + line-height: $body-cell-height + color: var(--table-text-primary-color) + font-weight: 300 + border-top: 1px solid var(--ui-gray-lighter) + border-bottom: 1px solid var(--ui-gray-lighter) + white-space: nowrap + transition(all 0.1s ease) + + &.emptyCell + color: #516873 + + &.emptyValue + color: var(--ui-gray-light) + + &:hover, &:active, &.active + background-color: var(--table-hover-color) + color: var(--text-primary-color) + + td + // This selector is necessary to override bootstrap's 'table' class + border-top: 1px solid var(--ui-gray-lighter) + border-bottom: 1px solid var(--ui-gray-lighter) + background-color: var(--table-hover-color) + +@media only screen and (max-width: 1362px) + #studyListContainer + padding: 0 $tablePaddingLargeScreen + + table#tblStudyList + thead, + tbody + tr + th, + td + &:first-child + padding-left: $tablePaddingLargeScreen + + &:last-child + padding-right: $tablePaddingLargeScreen + +@media only screen and (max-width: 1161px) + #studyListContainer + padding: 0 $tablePaddingBigWidth + + table#tblStudyList + thead, + tbody + tr + th, + td + &:first-child + padding-left: $tablePaddingBigScreen + + &:last-child + padding-right: $tablePaddingBigScreen + +@media only screen and (max-width: 1069px) + .theadBackground + height: 101px + + .studylist-pagination > .row + margin-right: 0; + + .studyListToolbar + padding: 0 $tablePaddingMediumScreen + + #studyListContainer + padding: 0 + + table#tblStudyList + thead > tr > th + + &:first-child + padding-left: $tablePaddingMediumScreen + + &:last-child + padding-right: $tablePaddingMediumScreen + + input.worklist-search + padding: 10px + + .display-text + padding: 10px 5px + + i + width: auto + + tbody > tr > td + + &:first-child + padding-left: $tablePaddingMediumScreen + + &:last-child + padding-right: $tablePaddingMediumScreen + + .worklistPagination + .row + margin-left: 0 + margin-right: 0 diff --git a/packages/_components/src/components/studyList/StudyListLoadingText.js b/packages/_components/src/components/studyList/StudyListLoadingText.js new file mode 100644 index 000000000..deab2e331 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListLoadingText.js @@ -0,0 +1,12 @@ +import { Icon } from './../../elements/Icon'; +import React from 'react'; + +function StudyListLoadingText() { + return ( +
    + Loading... +
    + ); +} + +export { StudyListLoadingText }; diff --git a/packages/_components/src/components/studyList/StudyListToolbar.js b/packages/_components/src/components/studyList/StudyListToolbar.js new file mode 100644 index 000000000..c024ea937 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListToolbar.js @@ -0,0 +1,50 @@ +import './StudyListToolbar.styl'; + +import React, { PureComponent } from 'react'; + +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; + +class StudylistToolbar extends PureComponent { + static propTypes = { + studyListFunctionsEnabled: PropTypes.bool.isRequired, + onImport: PropTypes.func, + }; + + static defaultProps = { + studyListFunctionsEnabled: true, + }; + + clear(e) { + e.target.value = null; + } + + getImportTool() { + if (this.props.onImport && this.props.studyListFunctionsEnabled) { + return ( +
    + + +
    + ); + } + } + + render() { + return
    {this.getImportTool()}
    ; + } +} + +export { StudylistToolbar }; diff --git a/packages/_components/src/components/studyList/StudyListToolbar.styl b/packages/_components/src/components/studyList/StudyListToolbar.styl new file mode 100644 index 000000000..c810bdb31 --- /dev/null +++ b/packages/_components/src/components/studyList/StudyListToolbar.styl @@ -0,0 +1,26 @@ +.studyListToolbar + .addNewStudy + margin: 0 10px + + label + font-weight: 400 + cursor: pointer + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + + input + width: 0.1px; + height: 0.1px + opacity: 0 + overflow: hidden + position: absolute + z-index: -1 + + color: var(--text-secondary-color) + + &:hover + color: var(--hover-color) + + &:active + color: var(--active-color) diff --git a/packages/_components/src/components/studyList/__docs__/onSearch.js b/packages/_components/src/components/studyList/__docs__/onSearch.js new file mode 100644 index 000000000..1101e76c9 --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/onSearch.js @@ -0,0 +1,72 @@ +import moment from 'moment'; + +export default function(searchData) { + this.setState({ searchData }); + + const filter = (key, searchData, study) => { + if (key === 'studyDateFrom' && searchData[key] && study['studyDate']) { + const studyDate = moment(study['studyDate'], 'YYYYMMDD'); + return studyDate.isBetween( + searchData['studyDateFrom'], + searchData['studyDateTo'], + 'days', + '[]' + ); + } else if (searchData[key] && !study[key].includes(searchData[key])) { + return false; + } else { + return true; + } + }; + + const { field, order } = searchData.sortData; + + // just a example of local filtering + let filteredStudies = this.defaultStudies + .filter(function(study) { + const all = [ + 'patientName', + 'patientId', + 'accessionNumber', + 'modalities', + 'studyDescription', + 'studyDateFrom', + ].every(key => { + return filter(key, searchData, study); + }); + + return all; + }) + .sort(function(a, b) { + if (order === 'desc') { + if (a[field] < b[field]) { + return -1; + } + if (a[field] > b[field]) { + return 1; + } + return 0; + } else { + if (a[field] > b[field]) { + return -1; + } + if (a[field] < b[field]) { + return 1; + } + return 0; + } + }); + + // User can notice the loading icon + return new Promise(resolve => { + setTimeout(() => { + const first = searchData.currentPage * searchData.rowsPerPage; + let last = + searchData.currentPage * searchData.rowsPerPage + + searchData.rowsPerPage; + last = last >= filteredStudies.length ? filteredStudies.length : last; + this.setState({ studies: filteredStudies.slice(first, last) }); + resolve(); + }, 500); + }); +} diff --git a/packages/_components/src/components/studyList/__docs__/studies.js b/packages/_components/src/components/studyList/__docs__/studies.js new file mode 100644 index 000000000..5538d634b --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/studies.js @@ -0,0 +1,87 @@ +import moment from 'moment'; + +const studies = [ + { + studyInstanceUid: '11111.111111.111111.11111', + patientName: 'John Doe', + patientId: '1', + accessionNumber: '1234567', + studyDate: '19930201', + modalities: 'MR', + studyDescription: 'BRAIN', + }, + { + studyInstanceUid: '2222.222222.22222.22222', + patientName: 'José Silva', + patientId: '2', + accessionNumber: '7654321', + studyDate: moment().format('YYYYMMDD'), + modalities: 'CT', + studyDescription: 'PET CT STANDARD', + }, + { + studyInstanceUid: '3333.333333.33333.33333', + patientName: 'Antônio Jefferson', + patientId: '3', + accessionNumber: '732311', + studyDate: moment() + .subtract(14, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '444444.44444.44444.4444', + patientName: 'Antonio da Silva', + patientId: '4', + accessionNumber: '732311', + studyDate: moment() + .subtract(1, 'months') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '55555.55555.55555.55555', + patientName: 'Bezerra Souza', + patientId: '5', + accessionNumber: '5134543', + studyDate: moment() + .subtract(6, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: '0', + }, + { + studyInstanceUid: '66666.66666.66666.6666', + patientName: 'Geraldo Roger', + patientId: '6', + accessionNumber: '5315135', + studyDate: moment() + .subtract(7, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: 'US', + }, + { + studyInstanceUid: '77777.77777.77777.77777', + patientName: '', + patientId: '7', + accessionNumber: '5315136', + studyDate: moment() + .subtract(5, 'days') + .format('YYYYMMDD'), + modalities: 'US', + studyDescription: 'US', + }, +].sort(function(a, b) { + if (a.patientName < b.patientName) { + return -1; + } + if (a.patientName > b.patientName) { + return 1; + } + return 0; +}); + +export default studies; diff --git a/packages/_components/src/components/studyList/__docs__/studyList.mdx b/packages/_components/src/components/studyList/__docs__/studyList.mdx new file mode 100644 index 000000000..86c33f686 --- /dev/null +++ b/packages/_components/src/components/studyList/__docs__/studyList.mdx @@ -0,0 +1,53 @@ +--- +name: Study List +menu: Components +route: /components/study-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { StudyList } from './../index.js' +// Data +import defaultStudies from './studies.js' +import onSearch from './onSearch.js' +import moment from 'moment' + +# Study List + +## Basic usage + + + { + const studyDate = moment(study['studyDate'], 'YYYYMMDD'); + const startDate = moment().subtract(5, 'days'); + const endDate = moment(); + + return studyDate.isBetween(startDate, endDate, 'days', '[]'); + }).slice(0, 5) + }}> + {({ state, setState }) => ( +
    +
    + alert('Import study mock ' + e)} + onSelectItem={(studyInstanceUid) => { alert(studyInstanceUid + ' has selected! Now you can open your study.'); }} + rowsPerPage={5} + defaultSort={{ field: 'patientName', order: 'desc', }} + studyListDateFilterNumDays={7} + onSearch={onSearch.bind({ setState, defaultStudies })} + /> +
    +
    + )} +
    + +
    + +## API + + diff --git a/packages/_components/src/components/studyList/index.js b/packages/_components/src/components/studyList/index.js new file mode 100644 index 000000000..45f244092 --- /dev/null +++ b/packages/_components/src/components/studyList/index.js @@ -0,0 +1 @@ +export { StudyList } from './StudyList.js'; diff --git a/packages/_components/src/components/tableList/TableList.js b/packages/_components/src/components/tableList/TableList.js new file mode 100644 index 000000000..dab927b0a --- /dev/null +++ b/packages/_components/src/components/tableList/TableList.js @@ -0,0 +1,37 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import './TableList.styl'; + +export class TableList extends Component { + static propTypes = { + customHeader: PropTypes.node, + defaultItems: PropTypes.object, + children: PropTypes.node.isRequired, + headerTitle: PropTypes.string, + }; + + render() { + return ( +
    +
    + {this.getHeader()} +
    +
    {this.props.children}
    +
    + ); + } + + getHeader = () => { + if (this.props.customHeader) { + return this.props.customHeader; + } else { + return ( + +
    {this.props.headerTitle}
    +
    {this.props.children.length}
    +
    + ); + } + }; +} diff --git a/packages/_components/src/components/tableList/TableList.styl b/packages/_components/src/components/tableList/TableList.styl new file mode 100644 index 000000000..f62c7444d --- /dev/null +++ b/packages/_components/src/components/tableList/TableList.styl @@ -0,0 +1,36 @@ + +$headerRowHeight = 63px + +.tableList + background-color: var(--primary-background-color) + height: 100%; + width: 100%; + + .tableListHeader + background-color: var(--ui-gray-darker) + color: var(--text-secondary-color) + display: flex + height: $headerRowHeight + line-height: $headerRowHeight + margin-top: 2px + overflow: hidden + width: 100% + + .tableListHeaderTitle + color: var(--text-secondary-color) + font-size: 22px + font-weight: 300 + line-height: $headerRowHeight + padding: 0 10px + text-align: left + flex: 1; + + .numberOfItems + color: var(--ui-sky-blue) + float: right + font-weight: 300 + font-size: 40px + max-width: 54px + height: $headerRowHeight + line-height: 66px + flex: 1; diff --git a/packages/_components/src/components/tableList/TableListItem.js b/packages/_components/src/components/tableList/TableListItem.js new file mode 100644 index 000000000..719ed6e82 --- /dev/null +++ b/packages/_components/src/components/tableList/TableListItem.js @@ -0,0 +1,42 @@ +import './TableListItem.styl'; + +import { Component } from 'react'; +import { Icon } from './../../elements/Icon'; +import PropTypes from 'prop-types'; +import React from 'react'; + +export class TableListItem extends Component { + static propTypes = { + children: PropTypes.node, + itemClass: PropTypes.string, + itemIndex: PropTypes.number, + itemKey: PropTypes.oneOfType(['number', 'string']), + onItemClick: PropTypes.func.isRequired, + }; + + render() { + return ( +
    +
    + {this.props.itemIndex} + + + +
    +
    {this.props.children}
    +
    + ); + } + + onItemClick = event => { + if (this.props.onItemClick) { + event.preventDefault(); + event.stopPropagation(); + + this.props.onItemClick(event, this.props.itemKey); + } + }; +} diff --git a/packages/_components/src/components/tableList/TableListItem.styl b/packages/_components/src/components/tableList/TableListItem.styl new file mode 100644 index 000000000..a471d134e --- /dev/null +++ b/packages/_components/src/components/tableList/TableListItem.styl @@ -0,0 +1,46 @@ +.tableListItem + display: flex + margin-left: -6px + margin-top: 2px + padding-left: 6px + opacity: 0.7 + cursor: pointer; + width: calc(100% + 6px) + + &:hover + opacity 1 + + &.selected + opacity 1 + .itemIndex + color: $activeColor + + &.hasWarning + .warning + display: block + + .itemIndex + background-color: var(--ui-gray) + color: var(--text-secondary-color) + cursor: pointer + flex: 1 + max-width: 25px + transition(all 0.3s ease) + font-size: 14px + font-weight: 400 + margin-right: 5px + padding: 10px + text-align: center + -webkit-box-sizing: initial; + -moz-box-sizing: initial; + box-sizing: initial; + + .itemContent + flex: 1 + color: var(--text-secondary-color) + font-weight: 400 + margin-left: 9px; + margin-top: 9px; + + .warning-icon + display: none diff --git a/packages/_components/src/components/tableList/__docs__/tableList.mdx b/packages/_components/src/components/tableList/__docs__/tableList.mdx new file mode 100644 index 000000000..bea1c7db5 --- /dev/null +++ b/packages/_components/src/components/tableList/__docs__/tableList.mdx @@ -0,0 +1,48 @@ +--- +name: Table List +menu: Components +route: /components/table-list +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { TableList, TableListItem } from './../index.js' + +# Table List + +## Basic usage + + + + {({ state, setState }) => ( + + {state.listItems.map( (item, index) => { + return ( + alert('item clicked')} + > + + + )} + )} + + )} + + + + + +## API + + diff --git a/packages/_components/src/components/tableList/index.js b/packages/_components/src/components/tableList/index.js new file mode 100644 index 000000000..5a19d8d71 --- /dev/null +++ b/packages/_components/src/components/tableList/index.js @@ -0,0 +1,2 @@ +export { TableList } from './TableList'; +export { TableListItem } from './TableListItem'; diff --git a/packages/_components/src/components/toolbarSection/ToolbarSection.js b/packages/_components/src/components/toolbarSection/ToolbarSection.js new file mode 100644 index 000000000..e1fdbf213 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/ToolbarSection.js @@ -0,0 +1,65 @@ +import './ToolbarSection.styl'; + +import React, { PureComponent } from 'react'; + +import ExpandableToolMenu from '../../viewer/ExpandableToolMenu'; +import PropTypes from 'prop-types'; +import ToolbarButton from '../../viewer/ToolbarButton'; +import classnames from 'classnames'; + +class ToolbarSection extends PureComponent { + static defaultProps = { + className: '', + }; + + static propTypes = { + buttons: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + label: PropTypes.string.isRequired, + icon: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.shape({ + name: PropTypes.string.isRequired, + }), + ]), + /** Optional: Expandable Tool Menu */ + buttons: PropTypes.arrayOf(PropTypes.shape({})), + }) + ).isRequired, + /** Array of string button ids that should show as active */ + activeButtons: PropTypes.arrayOf(PropTypes.string).isRequired, + /** Class for toolbar section container */ + className: PropTypes.string, + }; + + render() { + const items = this.props.buttons.map((button, index) => { + if (button.buttons && Array.isArray(button.buttons)) { + return ( + + ); + } else { + return ( + + ); + } + }); + + return ( +
    + {items} +
    + ); + } +} + +export { ToolbarSection }; diff --git a/packages/_components/src/components/toolbarSection/ToolbarSection.styl b/packages/_components/src/components/toolbarSection/ToolbarSection.styl new file mode 100644 index 000000000..1e008acca --- /dev/null +++ b/packages/_components/src/components/toolbarSection/ToolbarSection.styl @@ -0,0 +1,4 @@ +.ToolbarSection + float: left; + background-color: var(--primary-background-color); + border-radius: 8px; diff --git a/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js b/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js new file mode 100644 index 000000000..74e08b6a2 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/__docs__/exampleButtons.js @@ -0,0 +1,41 @@ +export default [ + { + id: 'Pan', + label: 'Pan', + icon: 'arrows', + }, + { + label: 'Zoom', + icon: 'search', + }, + { + label: 'Bidirectional', + icon: 'measure-target', + }, + { + label: 'Stack Scroll', + icon: 'bars', + }, + { + label: 'Reset', + icon: 'reset', + }, + { + label: 'Manual', + icon: 'level', + }, + { + label: 'More', + icon: 'ellipse-circle', + buttons: [ + { + label: 'Reset 2', + icon: 'reset', + }, + { + label: 'Manual 2', + icon: 'adjust', + }, + ], + }, +]; diff --git a/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx b/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx new file mode 100644 index 000000000..a246a2dd5 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/__docs__/toolbarSection.docs.mdx @@ -0,0 +1,39 @@ +--- +name: Toolbar Section +menu: Components +route: /components/toolbar-section +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { ToolbarSection } from './../index.js'; +import exampleButtons from './exampleButtons.js'; +import NameSpace from '../../../__docs__/NameSpace'; + +# ToolbarSection + +A basic row of buttons for a toolbar. Active command is `WwwcTool` + +## Basic usage + + + { + alert('pressed'); + }, + }} + > + {({ state, setState }) => } + + + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/toolbarSection/index.js b/packages/_components/src/components/toolbarSection/index.js new file mode 100644 index 000000000..5dad83951 --- /dev/null +++ b/packages/_components/src/components/toolbarSection/index.js @@ -0,0 +1 @@ +export { ToolbarSection } from './ToolbarSection.js'; diff --git a/packages/_components/src/components/tooltip/Tooltip.js b/packages/_components/src/components/tooltip/Tooltip.js new file mode 100644 index 000000000..237d2cb22 --- /dev/null +++ b/packages/_components/src/components/tooltip/Tooltip.js @@ -0,0 +1,66 @@ +import './Tooltip.styl'; + +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + /** Sets the direction the Tooltip is positioned towards. */ + placement: PropTypes.oneOf(['top', 'right', 'bottom', 'left']), + + /** The "top" position value for the Tooltip. */ + positionTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** The "left" position value for the Tooltip. */ + positionLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + /** The "top" position value for the Tooltip arrow. */ + arrowOffsetTop: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** The "left" position value for the Tooltip arrow. */ + arrowOffsetLeft: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), +}; + +const defaultProps = { + placement: 'right', +}; + +class Tooltip extends React.Component { + render() { + const { + placement, + positionTop, + positionLeft, + arrowOffsetTop, + arrowOffsetLeft, + className, + style, + children, + } = this.props; + + const outerStyle = { + top: positionTop, + left: positionLeft, + ...style, + }; + + const arrowStyle = { + top: arrowOffsetTop, + left: arrowOffsetLeft, + }; + + return ( +
    +
    +
    {children}
    +
    + ); + } +} + +Tooltip.propTypes = propTypes; +Tooltip.defaultProps = defaultProps; + +export { Tooltip }; diff --git a/packages/_components/src/components/tooltip/Tooltip.styl b/packages/_components/src/components/tooltip/Tooltip.styl new file mode 100644 index 000000000..ab0822ab9 --- /dev/null +++ b/packages/_components/src/components/tooltip/Tooltip.styl @@ -0,0 +1,92 @@ +@import './../../design/styles/common/global.styl' + +.tooltip + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 1.42857143; + text-align: left; + text-align: start; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-break: normal; + word-spacing: normal; + word-wrap: normal; + white-space: normal; + opacity: 0; + line-break: auto; + + &.in + opacity: .9; + + &.top + margin-top: -3px; + padding: 5px 0; + + &.right + margin-left: 3px; + padding: 0 5px; + + &.bottom + margin-top: 3px; + padding: 5px 0; + + &.left + margin-left: -3px; + padding: 0 5px; + + &.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; + } + + &.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; + } + &.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; + } + &.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; + } + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + background-color: #000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.in + opacity: .9; diff --git a/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx b/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx new file mode 100644 index 000000000..e5adad984 --- /dev/null +++ b/packages/_components/src/components/tooltip/__docs__/tooltip.noshow-mdx @@ -0,0 +1,20 @@ +--- +name: Tooltip +menu: Components +route: /components/tooltip +--- + +import { Playground, Props } from 'docz' +import { Tooltip } from './../index.js' + +# Tooltip + +## Basic usage + + + + + +## API + + diff --git a/packages/_components/src/components/tooltip/__tests__/.githold b/packages/_components/src/components/tooltip/__tests__/.githold new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/components/tooltip/index.js b/packages/_components/src/components/tooltip/index.js new file mode 100644 index 000000000..40ede0d67 --- /dev/null +++ b/packages/_components/src/components/tooltip/index.js @@ -0,0 +1 @@ +export { Tooltip } from './Tooltip.js'; diff --git a/packages/_components/src/components/userPreferencesModal/AboutModal.js b/packages/_components/src/components/userPreferencesModal/AboutModal.js new file mode 100644 index 000000000..3d1b5380a --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/AboutModal.js @@ -0,0 +1,105 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-bootstrap-modal'; +import './AboutModal.styl'; + +import 'react-bootstrap-modal/lib/css/rbm-patch.css'; + +// TODO: Is this the only component importing these? +import './../../design/styles/common/modal.styl'; + +export class AboutModal extends Component { + constructor(props) { + super(props); + } + // TODO: Make this component more generic to allow things other than W/L and hotkeys... + static propTypes = { + isOpen: PropTypes.bool.isRequired, + onCancel: PropTypes.func, + }; + + static itemsPreset = [ + { + name: 'Repository URL', + value: 'https://github.com/OHIF/Viewers/', + }, + { + name: 'Latest Master Commits', + value: 'https://github.com/OHIF/Viewers/commits/master', + }, + ]; + + static defaultProps = { + isOpen: false, + }; + + renderTableRow(item) { + return ( + + {item.name} + + + {item.value} + + + + ); + } + + render() { + return ( + + + About + + + +
    +

    Version Information

    + + + + + + + + + {AboutModal.itemsPreset.map(item => this.renderTableRow(item))} + +
    NameValue
    +
    +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/AboutModal.styl b/packages/_components/src/components/userPreferencesModal/AboutModal.styl new file mode 100644 index 000000000..b808e2cca --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/AboutModal.styl @@ -0,0 +1,20 @@ +@import './../../design/styles/common/button.styl' +@import './../../design/styles/common/table.styl' + +.AboutModal + .btn + border-color: #ccc; + + .table + thead:first-child + tr:first-child + th, td + border-top: 0; + + thead, tbody, tfoot + tr + th, td + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; diff --git a/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js b/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js new file mode 100644 index 000000000..51d68344e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/GeneralPreferences.js @@ -0,0 +1,17 @@ +import React, { Component } from 'react'; +import LanguageSwitcher from '../languageSwitcher'; + +export class GeneralPreferences extends Component { + render() { + return ( +
    +
    + + +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js new file mode 100644 index 000000000..ea97a144b --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.js @@ -0,0 +1,277 @@ +import './HotKeysPreferences.styl'; +import React, { Component } from 'react'; +import { + allowedKeys, + disallowedCombinations, + specialKeys, +} from './hotKeysConfig.js'; + +import PropTypes from 'prop-types'; + +export class HotKeysPreferences extends Component { + static propTypes = { + hotKeysData: PropTypes.objectOf( + PropTypes.shape({ + keys: PropTypes.arrayOf(PropTypes.string).isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + onChange: PropTypes.func, + }; + + constructor(props) { + super(props); + + const hotkeyCommands = Object.keys(this.props.hotKeysData); + const localHotKeys = hotkeyCommands.map(commandName => { + const definition = this.props.hotKeysData[commandName]; + + return { + commandName, + keys: definition.keys, + label: definition.label, + }; + }); + + this.state = { + hotKeys: localHotKeys, + errorMessages: {}, + }; + + this.onInputKeyDown = this.onInputKeyDown.bind(this); + } + + /** + * Normalizes the keys used in a KeyPress event and returns an array of the + * keys pressed + * + * @param {KeyDownEvent} keyDownEvent + * @returns {string[]} + */ + getKeysPressedArray(keyDownEvent) { + const keysPressedArray = []; + const { ctrlKey, altKey, shiftKey } = keyDownEvent; + + if (ctrlKey && !altKey) { + keysPressedArray.push('CTRL'); + } + + if (shiftKey && !altKey) { + keysPressedArray.push('SHIFT'); + } + + if (altKey && !ctrlKey) { + keysPressedArray.push('ALT'); + } + + return keysPressedArray; + } + + getConflictingCommand(currentToolKey, hotKeyCommand) { + return Object.keys(this.state.hotKeys).find(tool => { + const value = this.state.hotKeys[tool].command; + return value && value === hotKeyCommand && tool !== currentToolKey; + }); + } + + /** + * + * @param {String} commandName + * @param {KeyDownEvent} keyDownEvent + * @param {Boolean} [displayPressedKey=false] + */ + updateInputText(commandName, keyDownEvent, displayPressedKey = false) { + const pressedKeys = this.getKeysPressedArray(keyDownEvent); + + if (displayPressedKey) { + const specialKeyName = specialKeys[keyDownEvent.which]; + const keyName = + specialKeyName || + keyDownEvent.key || + String.fromCharCode(keyDownEvent.keyCode); + pressedKeys.push(keyName.toUpperCase()); + } + + this.updateHotKeysState(commandName, pressedKeys.join('+')); + } + + updateHotKeysState(commandName, keys) { + const hotKeys = this.state.hotKeys; + const hotKeyIndex = this.state.hotKeys.findIndex( + x => x.commandName === commandName + ); + hotKeys[hotKeyIndex].keys[0] = keys; + this.setState({ hotKeys }); + } + + updateErrorsState(toolKey, errorMessage) { + const errorMessages = this.state.errorMessages; + errorMessages[toolKey] = errorMessage; + this.setState({ errorMessages }); + } + + onInputKeyDown(event, commandName) { + // Prevent ESC key from propagating and closing the modal + if (event.key === 'Escape') { + event.stopPropagation(); + } + + if (allowedKeys.includes(event.keyCode)) { + this.updateInputText(commandName, event, true); + } else { + this.updateInputText(commandName, event, false); + } + + event.preventDefault(); + } + + onChange(event, commandName) { + if (event.ctrlKey || event.altKey || event.shiftKey) { + return; + } + + const hotKeyIndex = this.state.hotKeys.findIndex( + x => x.commandName === commandName + ); + const hotKey = this.state.hotKeys[hotKeyIndex]; + const keys = hotKey.keys[0]; + const pressedKeys = keys.split('+'); + const lastPressedKey = pressedKeys[pressedKeys.length - 1].toUpperCase(); + + // clear the prior errors + this.setState({ errorMessages: {} }, () => { + // Check if it has a valid modifier + const isModifier = ['CTRL', 'ALT', 'SHIFT'].includes(lastPressedKey); + if (isModifier) { + this.updateHotKeysState(commandName, ''); + this.updateErrorsState( + commandName, + "It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut" + ); + return; + } + + /* + * Check if it has some conflict + */ + const conflictedCommandKey = this.getConflictingCommand( + commandName, + keys + ); + if (conflictedCommandKey) { + const conflictedCommand = this.state.hotKeys[conflictedCommandKey]; + + this.updateErrorsState( + commandName, + `"${conflictedCommand.label}" is already using the "${ + conflictedCommand.command + }" shortcut.` + ); + this.updateErrorsState(conflictedCommandKey, ''); + this.updateHotKeysState(commandName, ''); + return; + } + + /* + * Check if is a valid combination + */ + const modifierCommand = pressedKeys + .slice(0, pressedKeys.length - 1) + .join('+') + .toUpperCase(); + + const disallowedCombination = disallowedCombinations[modifierCommand]; + const hasDisallowedCombinations = disallowedCombination + ? disallowedCombination.includes(lastPressedKey) + : false; + + if (hasDisallowedCombinations) { + this.updateHotKeysState(commandName, ''); + this.updateErrorsState( + commandName, + "It's not possible to define only modifier keys (CTRL, ALT and SHIFT) as a shortcut" + ); + return; + } + }); + } + + renderRow({ commandName, label, keys }) { + return ( + + {label} + + + + + ); + } + + render() { + const halfWayThough = Math.floor(this.state.hotKeys.length / 2); + const firstHalfHotkeys = this.state.hotKeys.slice(0, halfWayThough); + const secondHalfHotkeys = this.state.hotKeys.slice( + halfWayThough, + this.state.hotKeys.length + ); + + return ( +
    + {/* */} +
    + + + + + + + + + {firstHalfHotkeys.map(hotkeyDefinition => + this.renderRow(hotkeyDefinition) + )} + +
    FunctionShortcut
    +
    + {/* */} +
    + + + + + + + + + {secondHalfHotkeys.map(hotkeyDefinition => + this.renderRow(hotkeyDefinition) + )} + +
    FunctionShortcut
    +
    +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl new file mode 100644 index 000000000..98db2e52e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/HotKeysPreferences.styl @@ -0,0 +1,14 @@ +.HotKeysPreferences + display: flex; + margin-right: -15px; + margin-left: -15px; + + .column + width: 50%; + padding-right: 15px; + padding-left: 15px; + + .column-full + width: 100%; + padding-right: 15px; + padding-left: 15px; diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferences.js b/packages/_components/src/components/userPreferencesModal/UserPreferences.js new file mode 100644 index 000000000..584137e4c --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferences.js @@ -0,0 +1,118 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { HotKeysPreferences } from './HotKeysPreferences'; +import { WindowLevelPreferences } from './WindowLevelPreferences'; +import { GeneralPreferences } from './GeneralPreferences'; +import './UserPreferences.styl'; + +export class UserPreferences extends Component { + static defaultProps = { + hotKeysData: {}, + windowLevelData: {}, + generalData: {}, + }; + + // TODO: Make this more generic. Tabs should not be restricted to these entries + static propTypes = { + hotKeysData: PropTypes.object.isRequired, + windowLevelData: PropTypes.object.isRequired, + generalData: PropTypes.object.isRequired, + }; + + state = { + tabIndex: 0, + }; + + tabClick(tabIndex) { + this.setState({ tabIndex }); + } + + renderHotkeysTab() { + return ( +
    +
    + +
    +
    + ); + } + + renderWindowLevelTab() { + if (this.props.windowLevelData) { + return ( +
    +
    + +
    +
    + ); + } + } + + renderGeneralTab() { + return ( +
    +
    + +
    +
    + ); + } + + renderTabs(tabIndex) { + switch (tabIndex) { + case 0: + return this.renderHotkeysTab(); + case 1: + return this.renderWindowLevelTab(); + case 2: + return this.renderGeneralTab(); + + default: + break; + } + } + + getTabClass(tabIndex) { + return tabIndex === this.state.tabIndex ? 'nav-link active' : 'nav-link'; + } + + render() { + return ( +
    +
    +
      +
    • { + this.tabClick(0); + }} + className={this.getTabClass(0)} + > + +
    • +
    • { + this.tabClick(1); + }} + className={this.getTabClass(1)} + > + +
    • +
    • { + this.tabClick(2); + }} + className={this.getTabClass(2)} + > + +
    • +
    +
    + {this.renderTabs(this.state.tabIndex)} +
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferences.styl b/packages/_components/src/components/userPreferencesModal/UserPreferences.styl new file mode 100644 index 000000000..f62578e88 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferences.styl @@ -0,0 +1,25 @@ +@import './../../design/styles/common/form.styl' +@import './../../design/styles/common/navbar.styl' +@import './../../design/styles/common/state.styl' +@import './../../design/styles/common/global.styl' + +.modal-body + overflow: hidden + +.errorMessage + color: var(--state-error-text) + font-size: 10px + text-transform: uppercase; + +.form-content + border-bottom: 3px solid var(--primary-background-color) + margin-bottom: 20px + margin-left: -22px + margin-right: -22px + max-height: 70vh + overflow-y: auto + padding: 22px + min-height: 500px + +.popover + width: 300px diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js new file mode 100644 index 000000000..0b54504dd --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.js @@ -0,0 +1,107 @@ +import './UserPreferencesModal.styl'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-bootstrap-modal'; +import { withTranslation } from '../../utils/LanguageProvider'; + +import 'react-bootstrap-modal/lib/css/rbm-patch.css'; +import cloneDeep from 'lodash.clonedeep'; +import isEqual from 'lodash.isequal'; +import { UserPreferences } from './UserPreferences'; + +// TODO: Is this the only component importing these? +import './../../design/styles/common/modal.styl'; + +class UserPreferencesModal extends Component { + // TODO: Make this component more generic to allow things other than W/L and hotkeys... + static propTypes = { + isOpen: PropTypes.bool.isRequired, + onCancel: PropTypes.func, + onSave: PropTypes.func, + onResetToDefaults: PropTypes.func, + windowLevelData: PropTypes.object, + hotKeysData: PropTypes.object, + t: PropTypes.func, + }; + + constructor(props) { + super(props); + + this.state = { + windowLevelData: cloneDeep(props.windowLevelData), + hotKeysData: cloneDeep(props.hotKeysData), + }; + } + + static defaultProps = { + isOpen: false, + }; + + save = () => { + this.props.onSave({ + windowLevelData: this.state.windowLevelData, + hotKeysData: this.state.hotKeysData, + }); + }; + + componentDidUpdate(prev, next) { + const newStateData = {}; + + if (!isEqual(prev.windowLevelData, next.windowLevelData)) { + newStateData.windowLevelData = prev.windowLevelData; + } + + if (!isEqual(prev.hotKeysData, next.hotKeysData)) { + newStateData.hotKeysData = prev.hotKeysData; + } + + if (newStateData.hotKeysData || newStateData.windowLevelData) { + this.setState(newStateData); + } + } + + render() { + return ( + + + {this.props.t('User Preferences')} + + + + + + + + {this.props.t('Cancel')} + + + + + ); + } +} + +const connectedComponent = withTranslation('UserPreferencesModal')( + UserPreferencesModal +); +export { connectedComponent as UserPreferencesModal }; +export default connectedComponent; diff --git a/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl new file mode 100644 index 000000000..0dea62f53 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/UserPreferencesModal.styl @@ -0,0 +1,17 @@ +@import './../../design/styles/common/navbar.styl' +@import './../../design/styles/common/global.styl' +@import './../../design/styles/common/modal.styl' +@import './../../design/styles/common/button.styl' + +.close + float: right; + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .2; + +.ModalHeader + ol, ul + margin-top: 0; diff --git a/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js new file mode 100644 index 000000000..45e931b3e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.js @@ -0,0 +1,94 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import './WindowLevelPreferences.styl'; + +export class WindowLevelPreferences extends Component { + constructor(props) { + super(props); + + this.state = { + data: this.props.windowLevelData, + }; + } + + static propTypes = { + windowLevelData: PropTypes.object.isRequired, + onChange: PropTypes.func, + }; + + onChange(event, key, field) { + const data = this.state.data; + const entry = data[key]; + entry[field] = event.target.value; + this.setState({ data }); + + if (this.props.onChange) { + this.props.onChange(data); + } + } + + getWLPreferencesRows(key) { + const entry = this.state.data[key]; + return ( + + {key} + + + + + + + + + + + ); + } + + render() { + return ( + + + + + + + + + + + {Object.keys(this.state.data).map(key => { + return this.getWLPreferencesRows(key); + })} + +
    PresetDescriptionWindowLevel
    + ); + } +} diff --git a/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl new file mode 100644 index 000000000..78864357b --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/WindowLevelPreferences.styl @@ -0,0 +1,4 @@ +@import './UserPreferences.styl' + +.presetIndex + padding: 0px 10px 0px 10px \ No newline at end of file diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx b/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx new file mode 100644 index 000000000..819648ff0 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/about.mdx @@ -0,0 +1,42 @@ +--- +name: About Modal +menu: Components +route: /components/about-modal +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { AboutModal } from './../index.js' + +# About Modal + +## Basic usage + + + + + {({ state, setState }) => ( + + + setState({ isOpen: false })} + /> + + )} + + + + + +## API + + diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js new file mode 100644 index 000000000..40f7e3387 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/generalDefaults.js @@ -0,0 +1,14 @@ +export default { + currentLanguage: 'en', + languages: [ + { + value: 'en', + label: 'English', + }, + { + value: 'es', + label: 'Spanish', + }, + ], + onChange: language => {}, +}; diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js new file mode 100644 index 000000000..1c8399977 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/hotkeyDefaults.js @@ -0,0 +1,80 @@ +export default { + defaultTool: { label: 'Default Tool', keys: ['ESC'], column: 0 }, + zoom: { label: 'Zoom', keys: ['Z'], column: 0 }, + wwwc: { label: 'W/L', keys: ['W'], column: 0 }, + pan: { label: 'Pan', keys: ['P'], column: 0 }, + angle: { label: 'Angle measurement', keys: ['A'], column: 0 }, + stackScroll: { label: 'Scroll stack', keys: ['S'], column: 0 }, + magnify: { label: 'Magnify', keys: ['M'], column: 0 }, + length: { label: 'Length measurement', keys: [''], column: 0 }, + annotate: { label: 'Annotate', keys: [''], column: 0 }, + dragProbe: { label: 'Pixel probe', keys: [''], column: 0 }, + ellipticalRoi: { label: 'Elliptical ROI', keys: [''], column: 0 }, + rectangleRoi: { label: 'Rectangle ROI', keys: [''], column: 0 }, + + // Viewport hotkeys + flipH: { label: 'Flip Horizontally', keys: ['H'], column: 0 }, + flipV: { label: 'Flip Vertically', keys: ['V'], column: 0 }, + rotateR: { label: 'Rotate Right', keys: ['R'], column: 0 }, + rotateL: { label: 'Rotate Left', keys: ['L'], column: 0 }, + invert: { label: 'Invert', keys: ['I'], column: 0 }, + zoomIn: { label: 'Zoom In', keys: [''], column: 0 }, + zoomOut: { label: 'Zoom Out', keys: [''], column: 0 }, + zoomToFit: { label: 'Zoom to Fit', keys: [''], column: 0 }, + resetViewport: { label: 'Reset', keys: [''], column: 0 }, + clearTools: { label: 'Clear Tools', keys: [''], column: 0 }, + + // 2nd column + + // Viewport navigation hotkeys + scrollDown: { label: 'Scroll Down', keys: ['DOWN'], column: 1 }, + scrollUp: { label: 'Scroll Up', keys: ['UP'], column: 1 }, + scrollLastImage: { label: 'Scroll to Last Image', keys: ['END'], column: 1 }, + scrollFirstImage: { + label: 'Scroll to First Image', + keys: ['HOME'], + column: 1, + }, + previousDisplaySet: { + label: 'Previous Series', + keys: ['PAGEUP'], + column: 1, + }, + nextDisplaySet: { label: 'Next Series', keys: ['PAGEDOWN'], column: 1 }, + nextPanel: { label: 'Next Image Viewport', keys: ['RIGHT'], column: 1 }, + previousPanel: { + label: 'Previous Image Viewport', + keys: ['LEFT'], + column: 1, + }, + + // Miscellaneous hotkeys + toggleOverlayTags: { + label: 'Toggle Image Info Overlay', + keys: ['O'], + column: 1, + }, + toggleCinePlay: { label: 'Play/Pause Cine', keys: ['SPACE'], column: 1 }, + toggleCineDialog: { + label: 'Show/Hide Cine Controls', + keys: [''], + column: 1, + }, + toggleDownloadDialog: { + label: 'Show/Hide Download Dialog', + keys: [''], + column: 1, + }, + + // Preset hotkeys + WLPreset0: { label: 'W/L Preset 0 (Soft Tissue)', keys: ['1'], column: 1 }, + WLPreset1: { label: 'W/L Preset 1 (Lung)', keys: ['2'], column: 1 }, + WLPreset2: { label: 'W/L Preset 2 (Liver)', keys: ['3'], column: 1 }, + WLPreset3: { label: 'W/L Preset 3 (Bone)', keys: ['4'], column: 1 }, + WLPreset4: { label: 'W/L Preset 4 (Brain)', keys: ['5'], column: 1 }, + WLPreset5: { label: 'W/L Preset 5', keys: ['6'], column: 1 }, + WLPreset6: { label: 'W/L Preset 6', keys: ['7'], column: 1 }, + WLPreset7: { label: 'W/L Preset 7', keys: ['8'], column: 1 }, + WLPreset8: { label: 'W/L Preset 8', keys: ['9'], column: 1 }, + WLPreset9: { label: 'W/L Preset 0', keys: ['0'], column: 1 }, +}; diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx b/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx new file mode 100644 index 000000000..60b5ff69f --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/userPreferences.mdx @@ -0,0 +1,54 @@ +--- +name: User Preferences Modal +menu: Components +route: /components/user-preferences-modal +--- + +import { Playground, Props } from 'docz' +import { State } from 'react-powerplug' +import { UserPreferencesModal } from './../index.js' +import NameSpace from '../../../__docs__/NameSpace' +// +import windowLevelDefaults from './windowLevelDefaults.js' +import hotkeyDefaults from './hotkeyDefaults.js' + +# User Preferences Modal + +## Basic usage + + + + + {({ state, setState }) => ( + + + setState({ isOpen: false })} + onSave={() => alert('on save')} + onResetToDefaults={() => alert('on reset')} + /> + + )} + + + + + +## API + + + +## Translation Namespace + + diff --git a/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js b/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js new file mode 100644 index 000000000..ba1990c84 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/__docs__/windowLevelDefaults.js @@ -0,0 +1,13 @@ +export default { + 0: { description: 'Soft tissue', window: 400, level: 40 }, + 1: { description: 'Lung', window: 1500, level: -600 }, + 2: { description: 'Liver', window: 150, level: 90 }, + 3: { description: 'Bone', window: 2500, level: 480 }, + 4: { description: 'Brain', window: 80, level: 40 }, + 5: { description: '', window: '', level: '' }, + 6: { description: '', window: '', level: '' }, + 7: { description: '', window: '', level: '' }, + 8: { description: '', window: '', level: '' }, + 9: { description: '', window: '', level: '' }, + 10: { description: '', window: '', level: '' }, +}; diff --git a/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js b/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js new file mode 100644 index 000000000..cb3cd51f8 --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/hotKeysConfig.js @@ -0,0 +1,100 @@ +const range = (start, end) => { + return new Array(end - start).fill().map((d, i) => i + start); +}; + +export const disallowedCombinations = { + '': [], + ALT: ['SPACE'], + SHIFT: [], + CTRL: [ + 'F4', + 'F5', + 'F11', + 'W', + 'R', + 'T', + 'O', + 'P', + 'A', + 'D', + 'F', + 'G', + 'H', + 'J', + 'L', + 'Z', + 'X', + 'C', + 'V', + 'B', + 'N', + 'PAGEDOWN', + 'PAGEUP', + ], + 'CTRL+SHIFT': ['Q', 'W', 'R', 'T', 'P', 'A', 'H', 'V', 'B', 'N'], +}; + +export const allowedKeys = [ + ...[8, 13, 27, 32, 46], // BACKSPACE, ENTER, ESCAPE, SPACE, DELETE + ...[12, 106, 107, 109, 110, 111], // Numpad keys + ...range(218, 220), // [\] + ...range(185, 190), // ;=,-./ + ...range(111, 131), // F1-F19 + ...range(32, 41), // arrow keys, home/end, pg dn/up + ...range(47, 58), // 0-9 + ...range(64, 91), // A-Z +]; + +export const specialKeys = { + 8: 'backspace', + 9: 'tab', + 13: 'return', + 16: 'shift', + 17: 'ctrl', + 18: 'alt', + 19: 'pause', + 20: 'capslock', + 27: 'esc', + 32: 'space', + 33: 'pageup', + 34: 'pagedown', + 35: 'end', + 36: 'home', + 37: 'left', + 38: 'up', + 39: 'right', + 40: 'down', + 45: 'insert', + 46: 'del', + 96: '0', + 97: '1', + 98: '2', + 99: '3', + 100: '4', + 101: '5', + 102: '6', + 103: '7', + 104: '8', + 105: '9', + 106: '*', + 107: '+', + 109: '-', + 110: '.', + 111: '/', + 112: 'f1', + 113: 'f2', + 114: 'f3', + 115: 'f4', + 116: 'f5', + 117: 'f6', + 118: 'f7', + 119: 'f8', + 120: 'f9', + 121: 'f10', + 122: 'f11', + 123: 'f12', + 144: 'numlock', + 145: 'scroll', + 191: '/', + 224: 'meta', +}; diff --git a/packages/_components/src/components/userPreferencesModal/index.js b/packages/_components/src/components/userPreferencesModal/index.js new file mode 100644 index 000000000..fa7580d5e --- /dev/null +++ b/packages/_components/src/components/userPreferencesModal/index.js @@ -0,0 +1,4 @@ +export { UserPreferences } from './UserPreferences.js'; +export { AboutModal } from './AboutModal.js'; +export { UserPreferencesModal } from './UserPreferencesModal.js'; +export { GeneralPreferences } from './GeneralPreferences.js'; diff --git a/packages/_components/src/design/styles/common/button.styl b/packages/_components/src/design/styles/common/button.styl new file mode 100644 index 000000000..a77e855c6 --- /dev/null +++ b/packages/_components/src/design/styles/common/button.styl @@ -0,0 +1,47 @@ +.btn-danger + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; + +.btn-primary + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; + +.btn-default + color: #333; + background-color: #fff; + border-color: #ccc; + +.btn + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: 400; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; + +button.close + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: 0 0; + border: 0; + +.btn-group, .btn-group-vertical + position: relative; + display: inline-block; + vertical-align: middle; diff --git a/packages/_components/src/design/styles/common/form.styl b/packages/_components/src/design/styles/common/form.styl new file mode 100644 index 000000000..03bbdf985 --- /dev/null +++ b/packages/_components/src/design/styles/common/form.styl @@ -0,0 +1,117 @@ +.form-themed + + table + color: var(--text-primary-color) + + th, td + font-size: 15px + font-weight: normal + + th + line-height: 60px + + td + line-height: 18px + padding: 0; + + // TODO: imported this from merged-styles.css + .p-r-1 + padding-right: 1rem !important; + + label + margin-bottom: 5px + // end TODO + + label.wrapperLabel + cursor: pointer + + label.wrapperLabel:not(.checkboxLabel) + cursor: pointer + display: flex + flex-direction: column + + .wrapperText + display: block + order: -1 + transition(color 0.3s ease) + + .btn, input[type=text], input[type=password], input[type=number] + &[disabled], &.disabled + &, &:hover, &:active + background-color: var(--ui-gray-darker) + border-color: var(--ui-gray-light) + color: var(--text-primary-color) + + & + .wrapperText + color: var(--text-primary-color) + cursor: auto + + input[type=text], input[type=password], input[type=number] + background-color: var(--ui-gray) + border-color: var(--ui-border-color) + color: var(--text-primary-color) + font-weight: normal + transition(background-color 0.3s ease\, border-color 0.3s ease) + + &:active, &:focus + background-color: var(--ui-gray-dark) + border: 2px solid var(--active-color) + box-shadow: none + + & + .wrapperText + color: var(--active-color) + + .btn-primary + background-color: var(--active-color) + border-collapse: var(--ui-border-color-active) + color: var(--ui-text-color-active) + transition(background-color 0.3s ease\, border-color 0.3s ease) + + &:hover, &:active, &:focus, &:focus:active + // TODO: 0.8 opacity + background-color: var(--ui-active-color) + border-collapse: var(--ui-border-color-active) + color: var(--ui-text-color-active) + + .form-control + background-color: #FFFFFF + border: 0 + border-radius(2px) + font-size: 14px + height: 30px + line-height: 16px + padding: 8px 9px 6px + +.form-control + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075); + + &:focus + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102,175,233,.6); + + &::-ms-expand + border: 0; + background-color: transparent; + + &[disabled], + &[readonly], + fieldset[disabled] & + background-color: @input-bg-disabled; + opacity: 1; + + &[disabled], + fieldset[disabled] & + cursor: @cursor-disabled; diff --git a/packages/_components/src/design/styles/common/global.styl b/packages/_components/src/design/styles/common/global.styl new file mode 100644 index 000000000..cba02dac8 --- /dev/null +++ b/packages/_components/src/design/styles/common/global.styl @@ -0,0 +1,78 @@ +* + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + +.full-width + width: 100% + +.full-height + height: 100% + +.flex-h + display: flex + flex-direction: row + +.flex-v + display: flex + flex-direction: column + +.flex-grow + flex-grow: 1 + +.nowrap + white-space: nowrap + +.text-right + text-align: right; + +.text-center + text-align: center; + +.text-left + text-align: left; + +h3 + margin-top: 20px; + margin-bottom: 10px; + +pre + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + +button, input, select, textarea + font-family: inherit; + font-size: inherit; + line-height: inherit; + +.row + margin-right: -15px; + margin-left: -15px; + +.pull-left + float: left!important; + +.pull-right + float: right!important; + +.fade + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; + +.fade.in + opacity: 1; + +a + color: #337ab7; + text-decoration: none; diff --git a/packages/_components/src/design/styles/common/modal.styl b/packages/_components/src/design/styles/common/modal.styl new file mode 100644 index 000000000..d409eee32 --- /dev/null +++ b/packages/_components/src/design/styles/common/modal.styl @@ -0,0 +1,202 @@ + +.modal-open + overflow: hidden; + +.modal + overflow: hidden; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + -webkit-overflow-scrolling: touch; + outline: 0; + + .modal-dialog + + .modal-content + background-color: var(--ui-gray-darker) + border-color: var(--ui-border-color) + color: var(--text-secondary-color) + border-radius(6px) + border: 0 + + .modal-header, .modal-footer + border-color: var(--ui-border-color) + + .dialog-separator, + .dialog-separator-before, + .dialog-separator-after + position: relative + height: 40px; + + .dialog-separator:before, + .dialog-separator:after, + .dialog-separator-before:before, + .dialog-separator-after:after + background-color: #000000 + box-shadow(-50px 0 0 #000000\, 50px 0 0 #000000) + content: ' ' + display: block + height: 3px + left: 0 + position: absolute + width: 100% + + .dialog-separator:before, + .dialog-separator-before:before + top: -3px + + .dialog-separator:after, + .dialog-separator-after:after + bottom: -3px + + .modal-body + color: var(--text-primary-color) + padding: 16px 22px 0px + position: relative + + ::-webkit-scrollbar { + width: 6px; + background-color: var(--ui-gray-dark); + } + + ::-webkit-scrollbar-thumb { + background-color: var(--ui-gray-light); + border-radius: 5px; + } + + .modal-header + border-bottom-width: 3px + border-bottom-style: solid + border-bottom-color: #000000 + padding: 19px 22px 17px + position: relative + + h4 + color: var(--text-secondary-color) + font-size: 20px + font-weight: 500 + line-height: 24px + padding-right: 24px + + .modal-footer + border-top: 0 + padding-top: 0 + + .card-round + background-color: var(--ui-gray-dark) + border-radius(5px) + padding: 10px + + .modal-header + position: relative + + // When fading in the modal, animate it to slide down + &.fade .modal-dialog + -webkit-transition: transform .3s ease-out; + -o-transition: transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0,-25%); + -ms-transform: translate(0,-25%); + -o-transform: translate(0,-25%); + transform: translate(0,-25%); + + &.in .modal-dialog + transform: translate(0,0) + +.modal-open .modal + overflow-x: hidden; + overflow-y: auto; + +.modal-dialog + position: relative; + width: auto; + margin: 10px; + +.modal-content + position: relative; + background-color: #fff; + border: 1px solid rgba(0,0,0,.2); + border-radius: 6px; + -webkit-box-shadow: 0 3px 9px rgba(0,0,0,.5); + box-shadow: 0 3px 9px rgba(0,0,0,.5); + background-clip: padding-box; + outline: 0; + +.modal-backdrop + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + background-color: #fff; + + &.fade + opacity: 0; + + &.in + opacity: 0; + +.modal-header + padding: 15px; + border-bottom: 1px solid #e5e5e5; + +.modal-header .close + margin-top: -2px; + +.modal-title + margin: 0; + line-height: 1.42857143; + +.modal-body + position: relative; + padding: 15px; + +.modal-footer + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; + + .btn+.btn + margin-left: 5px; + margin-bottom: 0; + + .btn-group .btn+.btn + margin-left: -1px; + + .btn-block+.btn-block + margin-left: 0; + +.modal-scrollbar-measure + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; + +@media (min-width: 768px) + .modal-dialog + width: 600px; + margin: 30px auto; + + .modal-content + -webkit-box-shadow: 0 5px 15px rgba(0,0,0,.5); + box-shadow: 0 5px 15px rgba(0,0,0,.5); + +@media (min-width: 992px) + .modal-lg + width: 900px; + +.button-close, .modal-dialog button.close + color: var(--text-secondary-color) + height: 25px + opacity: 1 + overflow: hidden + text-align: center + text-shadow: none + width: 25px + font-size:25px + font-weight: lighter diff --git a/packages/_components/src/design/styles/common/navbar.styl b/packages/_components/src/design/styles/common/navbar.styl new file mode 100644 index 000000000..dda831789 --- /dev/null +++ b/packages/_components/src/design/styles/common/navbar.styl @@ -0,0 +1,57 @@ +.nav-tabs + border-bottom: 0 + margin-bottom: 3px + position: relative + z-index: 1 + + :hover + cursor: pointer + + &>li + font-size: 14px + font-weight: normal + height: 40px + line-height: 40px + margin-bottom: 0 + + &>button + display: block + width: 100% + line-height: inherit + margin: 0 + height: inherit + padding: 0 10px 0 10px + outline: none + + &:after + background-color: transparent + bottom: -3px + content: ' ' + display: block + height: 3px + transition(background-color 0.3s ease) + width: 100% + + &>button, &.active>button + &, &:hover, &:active, &:focus + color: var(--active-color) + background-color: transparent + border: 0 + + &.active>button + font-weight: bold + + &:after + background-color: var(--active-color) + +.nav + padding-left: 0; + list-style: none; + + > li + position: relative; + display: block; + + > li + float: left; + margin-bottom: -1px; diff --git a/packages/_components/src/design/styles/common/state.styl b/packages/_components/src/design/styles/common/state.styl new file mode 100644 index 000000000..8c438f741 --- /dev/null +++ b/packages/_components/src/design/styles/common/state.styl @@ -0,0 +1,25 @@ +.form-themed .state-error, .state-error + + &.wrapperLabel input + .wrapperText + color: var(--state-error-text) + + &+.tooltip + + .tooltip-inner + color: white + background-color: var(--state-error-border) + + &.top .tooltip-arrow + border-top-color: var(--state-error-border) + &.right .tooltip-arrow + border-right-color: var(--state-error-border) + &.bottom .tooltip-arrow + border-bottom-color: var(--state-error-border) + &.left .tooltip-arrow + border-left-color: var(--state-error-border) + + + &:not(.component-group) + &.form-control, .form-control + background-color: var(--state-error-border) + border-color: var(--state-error-border) diff --git a/packages/_components/src/design/styles/common/table.styl b/packages/_components/src/design/styles/common/table.styl new file mode 100644 index 000000000..26485d4e7 --- /dev/null +++ b/packages/_components/src/design/styles/common/table.styl @@ -0,0 +1,15 @@ +.table-responsive + min-height: .01%; + overflow-x: auto; + +.table + width: 100%; + max-width: 100%; + margin-bottom: 20px; + +table + border-spacing: 0; + border-collapse: collapse; + +th + text-align: left; diff --git a/packages/_components/src/elements/Icon/Icon.js b/packages/_components/src/elements/Icon/Icon.js new file mode 100644 index 000000000..5791fee68 --- /dev/null +++ b/packages/_components/src/elements/Icon/Icon.js @@ -0,0 +1,15 @@ +import './Icon.styl'; + +import PropTypes from 'prop-types'; +import getIcon from './getIcon.js'; + +const Icon = props => { + return getIcon(props.name, props); +}; + +Icon.propTypes = { + /** The string name of the icon to display */ + name: PropTypes.string.isRequired, +}; + +export default Icon; diff --git a/packages/_components/src/elements/Icon/Icon.styl b/packages/_components/src/elements/Icon/Icon.styl new file mode 100644 index 000000000..2d7949a0e --- /dev/null +++ b/packages/_components/src/elements/Icon/Icon.styl @@ -0,0 +1,7 @@ +.icon-pulse + fa-spin 1s infinite steps(8) + +@keyframes fa-spin{ + 0%{ transform:rotate(0deg) } + to{ transform:rotate(1turn) } +} diff --git a/packages/_components/src/elements/Icon/__docs__/icon.mdx b/packages/_components/src/elements/Icon/__docs__/icon.mdx new file mode 100644 index 000000000..c5b8f07a3 --- /dev/null +++ b/packages/_components/src/elements/Icon/__docs__/icon.mdx @@ -0,0 +1,84 @@ +--- +name: Icon +menu: Elements +route: /elements/icon +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { Icon, ICONS } from './../index.js'; + +# Icon + +## Basic usage + + +
    + {Object.keys(ICONS) + .sort() + .map((name, i) => { + return ( + +
    + +
    + {name} +
    +
    +
    + ); + })} +
    +
    + +The `` component inherits the color of its parent, and sizing is based +on its parent's `font-size`. Each icon has an `aria-labelledby` to assist screen +readers. + +We use `airbnb`'s [`babel-plugin-inline-react-svg`][airbnb] to automate +optimizing SVG files with [SVGO][svgo] and to create animatable React SVG +components that can be requested asynchronously. You can configure SVGO's +plugins [by following the advice laid out here][svgo-configure] + +## API + + + +The `Icon` component takes a single prop of `name`, which indicates which Icon +should be displayed. Again, the icon responds to contextual text/font styles. + +## Attribution + +A large number of these icons are from [Font Awesome 5's free set.][fa-license] +Some attribution is likely required, depending on your usage. Sometimes as +little as a comment included side-by-side with the accompanying SVG icon. + + + + +[fa-license]: https://fontawesome.com/license +[airbnb]: https://github.com/airbnb/babel-plugin-inline-react-svg +[svgo]: https://github.com/svg/svgo/ +[svgo-configure]: https://github.com/airbnb/babel-plugin-inline-react-svg/issues/9#issuecomment-310178317) + diff --git a/packages/_components/src/elements/Icon/getIcon.js b/packages/_components/src/elements/Icon/getIcon.js new file mode 100644 index 000000000..15c85c397 --- /dev/null +++ b/packages/_components/src/elements/Icon/getIcon.js @@ -0,0 +1,178 @@ +import React from 'react'; +import adjust from './icons/adjust.svg'; +// Icons +import angleDoubleDown from './icons/angle-double-down.svg'; +import angleDoubleUp from './icons/angle-double-up.svg'; +import angleLeft from './icons/angle-left.svg'; +import arrows from './icons/arrows.svg'; +import arrowsAltH from './icons/arrows-alt-h.svg'; +import arrowsAltV from './icons/arrows-alt-v.svg'; +import bars from './icons/bars.svg'; +import brain from './icons/brain.svg'; +import caretDown from './icons/caret-down.svg'; +import caretUp from './icons/caret-up.svg'; +import check from './icons/check.svg'; +import checkCircle from './icons/check-circle.svg'; +import checkCircleO from './icons/check-circle-o.svg'; +import chevronDown from './icons/chevron-down.svg'; +import circle from './icons/circle.svg'; +import circleNotch from './icons/circle-notch.svg'; +import circleO from './icons/circle-o.svg'; +import cog from './icons/cog.svg'; +import createComment from './icons/create-comment.svg'; +import createScreenCapture from './icons/create-screen-capture.svg'; +import crosshairs from './icons/crosshairs.svg'; +import cube from './icons/cube.svg'; +import d3Rotate from './icons/3d-rotate.svg'; +import database from './icons/database.svg'; +import dotCircle from './icons/dot-circle.svg'; +import edit from './icons/edit.svg'; +import ellipseCircle from './icons/ellipse-circle.svg'; +import ellipseH from './icons/ellipse-h.svg'; +import ellipseV from './icons/ellipse-v.svg'; +import exclamationCircle from './icons/exclamation-circle.svg'; +import exclamationTriangle from './icons/exclamation-triangle.svg'; +import fastBackward from './icons/fast-backward.svg'; +import fastForward from './icons/fast-forward.svg'; +import info from './icons/info.svg'; +import inlineEdit from './icons/inline-edit.svg'; +import level from './icons/level.svg'; +import link from './icons/link.svg'; +import linkCircles from './icons/link-circles.svg'; +import list from './icons/list.svg'; +import liver from './icons/liver.svg'; +import lock from './icons/lock.svg'; +import lockAlt from './icons/lock-alt.svg'; +import lung from './icons/lung.svg'; +import measureNonTarget from './icons/measure-non-target.svg'; +import measureTarget from './icons/measure-target.svg'; +import measureTargetCr from './icons/measure-target-cr.svg'; +import measureTargetNe from './icons/measure-target-ne.svg'; +import measureTargetUn from './icons/measure-target-un.svg'; +import measureTemp from './icons/measure-temp.svg'; +import objectGroup from './icons/object-group.svg'; +import ohifLogo from './icons/ohif-logo.svg'; +import oval from './icons/oval.svg'; +import palette from './icons/palette.svg'; +import play from './icons/play.svg'; +import plus from './icons/plus.svg'; +import powerOff from './icons/power-off.svg'; +import reset from './icons/reset.svg'; +import rotate from './icons/rotate.svg'; +import rotateRight from './icons/rotate-right.svg'; +import search from './icons/search.svg'; +import searchPlus from './icons/search-plus.svg'; +import softTissue from './icons/soft-tissue.svg'; +import sort from './icons/sort.svg'; +import sortDown from './icons/sort-down.svg'; +import sortUp from './icons/sort-up.svg'; +import squareO from './icons/square-o.svg'; +import star from './icons/star.svg'; +import stepBackward from './icons/step-backward.svg'; +import stepForward from './icons/step-forward.svg'; +import stop from './icons/stop.svg'; +import sun from './icons/sun.svg'; +import th from './icons/th.svg'; +import thLarge from './icons/th-large.svg'; +import thList from './icons/th-list.svg'; +import times from './icons/times.svg'; +import trash from './icons/trash.svg'; +import user from './icons/user.svg'; +import youtube from './icons/youtube.svg'; + +const ICONS = { + user, + sort, + th, + star, + 'sort-up': sortUp, + 'sort-down': sortDown, + info, + cube, + crosshairs, + 'dot-circle': dotCircle, + 'angle-left': angleLeft, + '3d-rotate': d3Rotate, + plus, + 'chevron-down': chevronDown, + 'angle-double-down': angleDoubleDown, + 'angle-double-up': angleDoubleUp, + 'arrows-alt-h': arrowsAltH, + 'arrows-alt-v': arrowsAltV, + bars, + 'caret-down': caretDown, + 'caret-up': caretUp, + 'check-circle-o': checkCircleO, + check, + circle, + 'circle-o': circleO, + times, + 'create-comment': createComment, + 'create-screen-capture': createScreenCapture, + edit, + 'fast-backward': fastBackward, + 'fast-forward': fastForward, + 'object-group': objectGroup, + search, + 'power-off': powerOff, + 'inline-edit': inlineEdit, + list, + 'ohif-logo': ohifLogo, + lock, + play, + database, + cog, + 'circle-notch': circleNotch, + 'square-o': squareO, + 'check-circle': checkCircle, + 'lock-alt': lockAlt, + 'step-backward': stepBackward, + 'step-forward': stepForward, + stop, + 'th-large': thLarge, + 'th-list': thList, + sun, + palette, + youtube, + oval, + 'ellipse-h': ellipseH, + 'ellipse-v': ellipseV, + adjust, + level, + 'link-circles': linkCircles, + 'search-plus': searchPlus, + 'measure-non-target': measureNonTarget, + 'measure-target': measureTarget, + 'measure-target-cr': measureTargetCr, + 'measure-target-un': measureTargetUn, + 'measure-target-ne': measureTargetNe, + 'measure-temp': measureTemp, + 'ellipse-circle': ellipseCircle, + arrows, + reset, + rotate, + 'rotate-right': rotateRight, + trash, + 'exclamation-circle': exclamationCircle, + link, + 'exclamation-triangle': exclamationTriangle, + brain, + 'soft-tissue': softTissue, + lung, + liver, +}; + +/** + * Return the matching SVG Icon as a React Component. + * Results in an inlined SVG Element. If there's no match, + * return `null` + */ +export default function getIcon(key, props) { + if (!key || !ICONS[key]) { + return React.createElement('div', null, 'Missing Icon'); + } + + return React.createElement(ICONS[key], props); +} + +export { ICONS }; diff --git a/packages/_components/src/elements/Icon/icons/3d-rotate.svg b/packages/_components/src/elements/Icon/icons/3d-rotate.svg new file mode 100644 index 000000000..f9d2daadb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/3d-rotate.svg @@ -0,0 +1,13 @@ + + 3D Rotate + + + + diff --git a/packages/_components/src/elements/Icon/icons/adjust.svg b/packages/_components/src/elements/Icon/icons/adjust.svg new file mode 100644 index 000000000..ae2747644 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/adjust.svg @@ -0,0 +1,11 @@ + + Adjust + + diff --git a/packages/_components/src/elements/Icon/icons/angle-double-down.svg b/packages/_components/src/elements/Icon/icons/angle-double-down.svg new file mode 100644 index 000000000..04d9d9fcf --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-double-down.svg @@ -0,0 +1,11 @@ + + Angle Double Down + + diff --git a/packages/_components/src/elements/Icon/icons/angle-double-up.svg b/packages/_components/src/elements/Icon/icons/angle-double-up.svg new file mode 100644 index 000000000..07419bb65 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-double-up.svg @@ -0,0 +1,11 @@ + + Angle Double Up + + diff --git a/packages/_components/src/elements/Icon/icons/angle-left.svg b/packages/_components/src/elements/Icon/icons/angle-left.svg new file mode 100644 index 000000000..15fb36b07 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/angle-left.svg @@ -0,0 +1,11 @@ + + Angle Left + + diff --git a/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg b/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg new file mode 100644 index 000000000..c96cb738c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows-alt-h.svg @@ -0,0 +1,11 @@ + + Arrows Alt H + + diff --git a/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg b/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg new file mode 100644 index 000000000..f1e777ddd --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows-alt-v.svg @@ -0,0 +1,11 @@ + + Arrows Alt V + + diff --git a/packages/_components/src/elements/Icon/icons/arrows.svg b/packages/_components/src/elements/Icon/icons/arrows.svg new file mode 100644 index 000000000..4aca49cff --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/arrows.svg @@ -0,0 +1,22 @@ + + Arrows + + + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/bars.svg b/packages/_components/src/elements/Icon/icons/bars.svg new file mode 100644 index 000000000..3b735f90e --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/bars.svg @@ -0,0 +1,11 @@ + + Bars + + diff --git a/packages/_components/src/elements/Icon/icons/brain.svg b/packages/_components/src/elements/Icon/icons/brain.svg new file mode 100644 index 000000000..f03f67ace --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/brain.svg @@ -0,0 +1,24 @@ + + Brain + + + + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/caret-down.svg b/packages/_components/src/elements/Icon/icons/caret-down.svg new file mode 100644 index 000000000..cdebbd8f5 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/caret-down.svg @@ -0,0 +1,11 @@ + + Caret Down + + diff --git a/packages/_components/src/elements/Icon/icons/caret-up.svg b/packages/_components/src/elements/Icon/icons/caret-up.svg new file mode 100644 index 000000000..611ca870e --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/caret-up.svg @@ -0,0 +1,11 @@ + + Caret Up + + diff --git a/packages/_components/src/elements/Icon/icons/check-circle-o.svg b/packages/_components/src/elements/Icon/icons/check-circle-o.svg new file mode 100644 index 000000000..98b247319 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check-circle-o.svg @@ -0,0 +1,11 @@ + + Check Circle + + diff --git a/packages/_components/src/elements/Icon/icons/check-circle.svg b/packages/_components/src/elements/Icon/icons/check-circle.svg new file mode 100644 index 000000000..7f54493fe --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check-circle.svg @@ -0,0 +1,11 @@ + + Check Circle + + diff --git a/packages/_components/src/elements/Icon/icons/check.svg b/packages/_components/src/elements/Icon/icons/check.svg new file mode 100644 index 000000000..22d93a1fb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/check.svg @@ -0,0 +1,11 @@ + + Check + + diff --git a/packages/_components/src/elements/Icon/icons/chevron-down.svg b/packages/_components/src/elements/Icon/icons/chevron-down.svg new file mode 100644 index 000000000..1c28b0512 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/chevron-down.svg @@ -0,0 +1,14 @@ + + Chevron Down + + diff --git a/packages/_components/src/elements/Icon/icons/circle-notch.svg b/packages/_components/src/elements/Icon/icons/circle-notch.svg new file mode 100644 index 000000000..1a9429bda --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle-notch.svg @@ -0,0 +1,11 @@ + + Circle Notch + + diff --git a/packages/_components/src/elements/Icon/icons/circle-o.svg b/packages/_components/src/elements/Icon/icons/circle-o.svg new file mode 100644 index 000000000..c921f1534 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle-o.svg @@ -0,0 +1,11 @@ + + Circle Outline + + diff --git a/packages/_components/src/elements/Icon/icons/circle.svg b/packages/_components/src/elements/Icon/icons/circle.svg new file mode 100644 index 000000000..99b9995b3 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/circle.svg @@ -0,0 +1,11 @@ + + Circle + + diff --git a/packages/_components/src/elements/Icon/icons/cog.svg b/packages/_components/src/elements/Icon/icons/cog.svg new file mode 100644 index 000000000..017fc0e6d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/cog.svg @@ -0,0 +1,11 @@ + + Cog + + diff --git a/packages/_components/src/elements/Icon/icons/create-comment.svg b/packages/_components/src/elements/Icon/icons/create-comment.svg new file mode 100644 index 000000000..aec47a3ae --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/create-comment.svg @@ -0,0 +1,12 @@ + + Create Comment + + + diff --git a/packages/_components/src/elements/Icon/icons/create-screen-capture.svg b/packages/_components/src/elements/Icon/icons/create-screen-capture.svg new file mode 100644 index 000000000..cf7d8433d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/create-screen-capture.svg @@ -0,0 +1,15 @@ + + Create Screen Capture + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/crosshairs.svg b/packages/_components/src/elements/Icon/icons/crosshairs.svg new file mode 100644 index 000000000..4d53a1042 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/crosshairs.svg @@ -0,0 +1,15 @@ + + Crosshairs + + + diff --git a/packages/_components/src/elements/Icon/icons/cube.svg b/packages/_components/src/elements/Icon/icons/cube.svg new file mode 100644 index 000000000..6026cdf63 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/cube.svg @@ -0,0 +1,12 @@ + + + Cube + + diff --git a/packages/_components/src/elements/Icon/icons/database.svg b/packages/_components/src/elements/Icon/icons/database.svg new file mode 100644 index 000000000..323d566e2 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/database.svg @@ -0,0 +1,11 @@ + + Database + + diff --git a/packages/_components/src/elements/Icon/icons/dot-circle.svg b/packages/_components/src/elements/Icon/icons/dot-circle.svg new file mode 100644 index 000000000..3533dde76 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/dot-circle.svg @@ -0,0 +1,11 @@ + + Dot Circle + + diff --git a/packages/_components/src/elements/Icon/icons/edit.svg b/packages/_components/src/elements/Icon/icons/edit.svg new file mode 100644 index 000000000..c7554488d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/edit.svg @@ -0,0 +1,11 @@ + + Edit + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-circle.svg b/packages/_components/src/elements/Icon/icons/ellipse-circle.svg new file mode 100644 index 000000000..a0c591c2b --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-circle.svg @@ -0,0 +1,11 @@ + + Ellipse Circle + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-h.svg b/packages/_components/src/elements/Icon/icons/ellipse-h.svg new file mode 100644 index 000000000..1dbb78331 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-h.svg @@ -0,0 +1,11 @@ + + Ellipse Horizontal + + diff --git a/packages/_components/src/elements/Icon/icons/ellipse-v.svg b/packages/_components/src/elements/Icon/icons/ellipse-v.svg new file mode 100644 index 000000000..01a0d719a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ellipse-v.svg @@ -0,0 +1,11 @@ + + Ellipse Vertical + + diff --git a/packages/_components/src/elements/Icon/icons/exclamation-circle.svg b/packages/_components/src/elements/Icon/icons/exclamation-circle.svg new file mode 100644 index 000000000..f3a1d4146 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/exclamation-circle.svg @@ -0,0 +1,13 @@ + + Exclamation Circle + + diff --git a/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg b/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg new file mode 100644 index 000000000..65da6f8e8 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/exclamation-triangle.svg @@ -0,0 +1,11 @@ + + Exclamation Triangle + + diff --git a/packages/_components/src/elements/Icon/icons/fast-backward.svg b/packages/_components/src/elements/Icon/icons/fast-backward.svg new file mode 100644 index 000000000..c1696b98f --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/fast-backward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/fast-forward.svg b/packages/_components/src/elements/Icon/icons/fast-forward.svg new file mode 100644 index 000000000..cd6bb9b7c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/fast-forward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/info.svg b/packages/_components/src/elements/Icon/icons/info.svg new file mode 100644 index 000000000..9e802a69f --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/info.svg @@ -0,0 +1,11 @@ + + Info + + diff --git a/packages/_components/src/elements/Icon/icons/inline-edit.svg b/packages/_components/src/elements/Icon/icons/inline-edit.svg new file mode 100644 index 000000000..12c111b2c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/inline-edit.svg @@ -0,0 +1,11 @@ + + Inline Edit + + diff --git a/packages/_components/src/elements/Icon/icons/level.svg b/packages/_components/src/elements/Icon/icons/level.svg new file mode 100644 index 000000000..c79ffc2f6 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/level.svg @@ -0,0 +1,13 @@ + + Level + + + diff --git a/packages/_components/src/elements/Icon/icons/link-circles.svg b/packages/_components/src/elements/Icon/icons/link-circles.svg new file mode 100644 index 000000000..fd980320a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/link-circles.svg @@ -0,0 +1,15 @@ + + Link Circles + + + + + diff --git a/packages/_components/src/elements/Icon/icons/link.svg b/packages/_components/src/elements/Icon/icons/link.svg new file mode 100644 index 000000000..28faa4853 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/link.svg @@ -0,0 +1,12 @@ + + Link + + + diff --git a/packages/_components/src/elements/Icon/icons/list.svg b/packages/_components/src/elements/Icon/icons/list.svg new file mode 100644 index 000000000..15e7a6c95 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/list.svg @@ -0,0 +1,11 @@ + + List + + diff --git a/packages/_components/src/elements/Icon/icons/liver.svg b/packages/_components/src/elements/Icon/icons/liver.svg new file mode 100644 index 000000000..a099709f4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/liver.svg @@ -0,0 +1,16 @@ + + Liver + + + + diff --git a/packages/_components/src/elements/Icon/icons/lock-alt.svg b/packages/_components/src/elements/Icon/icons/lock-alt.svg new file mode 100644 index 000000000..570a16de6 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lock-alt.svg @@ -0,0 +1,11 @@ + + Lock + + diff --git a/packages/_components/src/elements/Icon/icons/lock.svg b/packages/_components/src/elements/Icon/icons/lock.svg new file mode 100644 index 000000000..6a43ae317 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lock.svg @@ -0,0 +1,11 @@ + + Lock + + diff --git a/packages/_components/src/elements/Icon/icons/lung.svg b/packages/_components/src/elements/Icon/icons/lung.svg new file mode 100644 index 000000000..2de91333c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/lung.svg @@ -0,0 +1,25 @@ + + Lung + + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-non-target.svg b/packages/_components/src/elements/Icon/icons/measure-non-target.svg new file mode 100644 index 000000000..6c6c55f61 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-non-target.svg @@ -0,0 +1,29 @@ + + Measure Non-Target + + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-cr.svg b/packages/_components/src/elements/Icon/icons/measure-target-cr.svg new file mode 100644 index 000000000..474136ac7 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-cr.svg @@ -0,0 +1,23 @@ + + Measure Target CR + + CR + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-ne.svg b/packages/_components/src/elements/Icon/icons/measure-target-ne.svg new file mode 100644 index 000000000..492a880e7 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-ne.svg @@ -0,0 +1,16 @@ + + Measure Target NE + NE + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target-un.svg b/packages/_components/src/elements/Icon/icons/measure-target-un.svg new file mode 100644 index 000000000..5ba5eb223 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target-un.svg @@ -0,0 +1,23 @@ + + Measure Target UN + + UN + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-target.svg b/packages/_components/src/elements/Icon/icons/measure-target.svg new file mode 100644 index 000000000..19596906a --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-target.svg @@ -0,0 +1,13 @@ + + Measure Target + + + diff --git a/packages/_components/src/elements/Icon/icons/measure-temp.svg b/packages/_components/src/elements/Icon/icons/measure-temp.svg new file mode 100644 index 000000000..fb2e2b98b --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/measure-temp.svg @@ -0,0 +1,16 @@ + + Measure Temp + + + + + + diff --git a/packages/_components/src/elements/Icon/icons/object-group.svg b/packages/_components/src/elements/Icon/icons/object-group.svg new file mode 100644 index 000000000..edd4bc189 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/object-group.svg @@ -0,0 +1,20 @@ + + Object Group + + + + + diff --git a/packages/_components/src/elements/Icon/icons/ohif-logo.svg b/packages/_components/src/elements/Icon/icons/ohif-logo.svg new file mode 100644 index 000000000..f6525ab90 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/ohif-logo.svg @@ -0,0 +1,17 @@ + + OHIF Logo + + + + + diff --git a/packages/_components/src/elements/Icon/icons/oval.svg b/packages/_components/src/elements/Icon/icons/oval.svg new file mode 100644 index 000000000..0a69e6b11 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/oval.svg @@ -0,0 +1,11 @@ + + Oval + + diff --git a/packages/_components/src/elements/Icon/icons/palette.svg b/packages/_components/src/elements/Icon/icons/palette.svg new file mode 100644 index 000000000..04f596ba1 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/palette.svg @@ -0,0 +1,11 @@ + + Pallete + + diff --git a/packages/_components/src/elements/Icon/icons/play.svg b/packages/_components/src/elements/Icon/icons/play.svg new file mode 100644 index 000000000..64ca7a5e3 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/play.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/plus.svg b/packages/_components/src/elements/Icon/icons/plus.svg new file mode 100644 index 000000000..6512e1116 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/plus.svg @@ -0,0 +1,11 @@ + + Add + + diff --git a/packages/_components/src/elements/Icon/icons/power-off.svg b/packages/_components/src/elements/Icon/icons/power-off.svg new file mode 100644 index 000000000..3dd5dcf20 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/power-off.svg @@ -0,0 +1,11 @@ + + Power Off + + diff --git a/packages/_components/src/elements/Icon/icons/reset.svg b/packages/_components/src/elements/Icon/icons/reset.svg new file mode 100644 index 000000000..8b2c9aa53 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/reset.svg @@ -0,0 +1,11 @@ + + Reset + + diff --git a/packages/_components/src/elements/Icon/icons/rotate-right.svg b/packages/_components/src/elements/Icon/icons/rotate-right.svg new file mode 100644 index 000000000..106284fa0 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/rotate-right.svg @@ -0,0 +1,11 @@ + + Rotate Right + + diff --git a/packages/_components/src/elements/Icon/icons/rotate.svg b/packages/_components/src/elements/Icon/icons/rotate.svg new file mode 100644 index 000000000..2dded38c4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/rotate.svg @@ -0,0 +1,11 @@ + + Rotate + + diff --git a/packages/_components/src/elements/Icon/icons/search-plus.svg b/packages/_components/src/elements/Icon/icons/search-plus.svg new file mode 100644 index 000000000..f130654c5 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/search-plus.svg @@ -0,0 +1,14 @@ + + Search Plus + + + + + diff --git a/packages/_components/src/elements/Icon/icons/search.svg b/packages/_components/src/elements/Icon/icons/search.svg new file mode 100644 index 000000000..a313de17d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/search.svg @@ -0,0 +1,11 @@ + + Search + + diff --git a/packages/_components/src/elements/Icon/icons/soft-tissue.svg b/packages/_components/src/elements/Icon/icons/soft-tissue.svg new file mode 100644 index 000000000..730c69450 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/soft-tissue.svg @@ -0,0 +1,16 @@ + + Soft Tissue + + diff --git a/packages/_components/src/elements/Icon/icons/sort-down.svg b/packages/_components/src/elements/Icon/icons/sort-down.svg new file mode 100644 index 000000000..6306b4884 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort-down.svg @@ -0,0 +1,11 @@ + + Sort Down + + diff --git a/packages/_components/src/elements/Icon/icons/sort-up.svg b/packages/_components/src/elements/Icon/icons/sort-up.svg new file mode 100644 index 000000000..ab5e0a836 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort-up.svg @@ -0,0 +1,11 @@ + + Sort Up + + diff --git a/packages/_components/src/elements/Icon/icons/sort.svg b/packages/_components/src/elements/Icon/icons/sort.svg new file mode 100644 index 000000000..1ade70182 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sort.svg @@ -0,0 +1,11 @@ + + Sort + + diff --git a/packages/_components/src/elements/Icon/icons/square-o.svg b/packages/_components/src/elements/Icon/icons/square-o.svg new file mode 100644 index 000000000..16d36444c --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/square-o.svg @@ -0,0 +1,11 @@ + + Square Outline + + diff --git a/packages/_components/src/elements/Icon/icons/star.svg b/packages/_components/src/elements/Icon/icons/star.svg new file mode 100644 index 000000000..265a0e6bb --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/star.svg @@ -0,0 +1,11 @@ + + Star + + diff --git a/packages/_components/src/elements/Icon/icons/step-backward.svg b/packages/_components/src/elements/Icon/icons/step-backward.svg new file mode 100644 index 000000000..78a15fa7d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/step-backward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/step-forward.svg b/packages/_components/src/elements/Icon/icons/step-forward.svg new file mode 100644 index 000000000..c42eabf65 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/step-forward.svg @@ -0,0 +1,10 @@ + + + diff --git a/packages/_components/src/elements/Icon/icons/stop.svg b/packages/_components/src/elements/Icon/icons/stop.svg new file mode 100644 index 000000000..9d124d325 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/stop.svg @@ -0,0 +1,11 @@ + + Stop + + diff --git a/packages/_components/src/elements/Icon/icons/sun.svg b/packages/_components/src/elements/Icon/icons/sun.svg new file mode 100644 index 000000000..08158098d --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/sun.svg @@ -0,0 +1,11 @@ + + Sun + + diff --git a/packages/_components/src/elements/Icon/icons/th-large.svg b/packages/_components/src/elements/Icon/icons/th-large.svg new file mode 100644 index 000000000..876289bc4 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th-large.svg @@ -0,0 +1,12 @@ + + TH Large + + diff --git a/packages/_components/src/elements/Icon/icons/th-list.svg b/packages/_components/src/elements/Icon/icons/th-list.svg new file mode 100644 index 000000000..e69919bf8 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th-list.svg @@ -0,0 +1,11 @@ + + TH List + + diff --git a/packages/_components/src/elements/Icon/icons/th.svg b/packages/_components/src/elements/Icon/icons/th.svg new file mode 100644 index 000000000..b1df15afa --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/th.svg @@ -0,0 +1,11 @@ + + TH + + diff --git a/packages/_components/src/elements/Icon/icons/times.svg b/packages/_components/src/elements/Icon/icons/times.svg new file mode 100644 index 000000000..27f875dce --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/times.svg @@ -0,0 +1,12 @@ + + Times + + diff --git a/packages/_components/src/elements/Icon/icons/trash.svg b/packages/_components/src/elements/Icon/icons/trash.svg new file mode 100644 index 000000000..20bca7e22 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/trash.svg @@ -0,0 +1,11 @@ + + Trash + + diff --git a/packages/_components/src/elements/Icon/icons/user.svg b/packages/_components/src/elements/Icon/icons/user.svg new file mode 100644 index 000000000..c55700132 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/user.svg @@ -0,0 +1,11 @@ + + User + + diff --git a/packages/_components/src/elements/Icon/icons/youtube.svg b/packages/_components/src/elements/Icon/icons/youtube.svg new file mode 100644 index 000000000..00db81c23 --- /dev/null +++ b/packages/_components/src/elements/Icon/icons/youtube.svg @@ -0,0 +1,11 @@ + + YouTube Logo + + diff --git a/packages/_components/src/elements/Icon/index.js b/packages/_components/src/elements/Icon/index.js new file mode 100644 index 000000000..3d83dbf3e --- /dev/null +++ b/packages/_components/src/elements/Icon/index.js @@ -0,0 +1,4 @@ +import { ICONS } from './getIcon.js'; +import Icon from './Icon.js'; + +export { Icon, ICONS }; diff --git a/packages/_components/src/elements/Svg/Svg.js b/packages/_components/src/elements/Svg/Svg.js new file mode 100644 index 000000000..41ea577f5 --- /dev/null +++ b/packages/_components/src/elements/Svg/Svg.js @@ -0,0 +1,14 @@ +import './Svg.styl'; + +import PropTypes from 'prop-types'; +import getSvg from './getSvg.js'; + +const Svg = props => { + return getSvg(props.name, props); +}; + +Svg.propTypes = { + name: PropTypes.string.isRequired, +}; + +export { Svg }; diff --git a/packages/_components/src/elements/Svg/Svg.styl b/packages/_components/src/elements/Svg/Svg.styl new file mode 100644 index 000000000..e69de29bb diff --git a/packages/_components/src/elements/Svg/__docs__/svg.mdx b/packages/_components/src/elements/Svg/__docs__/svg.mdx new file mode 100644 index 000000000..6fe66a7ee --- /dev/null +++ b/packages/_components/src/elements/Svg/__docs__/svg.mdx @@ -0,0 +1,57 @@ +--- +name: Svg +menu: Elements +route: /elements/svg +--- + +import { Playground, Props } from 'docz'; +import { State } from 'react-powerplug'; +import { Svg, SVGS } from './../index.js'; + +# Svg + +## Basic usage + + +
    + {Object.keys(SVGS) + .sort() + .map((name, i) => { + return ( + +
    + +
    + {name} +
    +
    +
    + ); + })} +
    +
    + +## API + + diff --git a/packages/_components/src/elements/Svg/getSvg.js b/packages/_components/src/elements/Svg/getSvg.js new file mode 100644 index 000000000..4933fc121 --- /dev/null +++ b/packages/_components/src/elements/Svg/getSvg.js @@ -0,0 +1,24 @@ +import React from 'react'; +// Svgs +import ohifLogoText from './svgs/ohif-logo-text.svg'; +import ohifLogoWrappedText from './svgs/ohif-logo-wrapped-text.svg'; + +const SVGS = { + 'ohif-logo-wrapped-text': ohifLogoWrappedText, + 'ohif-logo-text': ohifLogoText, +}; + +/** + * Return the matching SVG as a React Component. + * Results in an inlined SVG Element. If there's no match, + * return `null` + */ +export default function getSvg(key, props) { + if (!key || !SVGS[key]) { + return React.createElement('div', null, 'Missing SVG'); + } + + return React.createElement(SVGS[key], props); +} + +export { SVGS }; diff --git a/packages/_components/src/elements/Svg/index.js b/packages/_components/src/elements/Svg/index.js new file mode 100644 index 000000000..54a598b17 --- /dev/null +++ b/packages/_components/src/elements/Svg/index.js @@ -0,0 +1,2 @@ +export { Svg } from './Svg.js'; +export { SVGS } from './getSvg.js'; diff --git a/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg b/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg new file mode 100644 index 000000000..ffc43c748 --- /dev/null +++ b/packages/_components/src/elements/Svg/svgs/ohif-logo-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg b/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg new file mode 100644 index 000000000..d9c2cb6b6 --- /dev/null +++ b/packages/_components/src/elements/Svg/svgs/ohif-logo-wrapped-text.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/_components/src/elements/form/DropdownMenu.css b/packages/_components/src/elements/form/DropdownMenu.css new file mode 100644 index 000000000..a20017fa4 --- /dev/null +++ b/packages/_components/src/elements/form/DropdownMenu.css @@ -0,0 +1,86 @@ +.dd-menu { + float: left; + margin-right: 10px; + cursor: pointer; + position: relative; + z-index: 999; +} + +.dd-title { + font-size: 13px; +} + +.dd-menu-toggle { + display: inline-block; +} + +.dd-caret-down { + display: inline-block; + width: 0; + height: 0; + margin-top: 0.5rem; + margin-left: 0.5rem; + border-top: 5px solid; + border-right: 5px solid transparent; + border-left: 5px solid transparent; +} + +.dd-menu-list { + position: absolute; + top: 100%; + margin-top: 10px; + background: white; + border-radius: 5px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + -ms-border-radius: 5px; + -o-border-radius: 5px; + overflow: hidden; + transition: all 300ms ease; + -webkit-transition: all 300ms ease; + -moz-transition: all 300ms ease; + -ms-transition: all 300ms ease; + -o-transition: all 300ms ease; +} + +.dd-menu-list.open { + display: inline-block; +} + +.dd-menu-list.left { + left: 0; +} + +.dd-menu-list.right { + right: 0; +} + +.dd-menu-list.center { + left: 50%; + transform: translateX(-50%); + -webkit-transform: translateX(-50%); + -moz-transform: translateX(-50%); + -ms-transform: translateX(-50%); + -o-transform: translateX(-50%); +} + +.dd-item { + display: flex; + color: var(--text-color-active); + padding: 10px 15px; + border-bottom: 1px solid #ccc; +} + +.dd-item:hover { + text-decoration: none; + background: #eee; +} + +.dd-item:last-child { + border-bottom: none; +} + +.dd-item-icon { + margin-right: 10px; + margin-top: 2px; +} diff --git a/packages/_components/src/elements/form/DropdownMenu.js b/packages/_components/src/elements/form/DropdownMenu.js new file mode 100644 index 000000000..6a40f4088 --- /dev/null +++ b/packages/_components/src/elements/form/DropdownMenu.js @@ -0,0 +1,133 @@ +import './DropdownMenu.css'; + +import React, { Component } from 'react'; + +import { Icon } from '../Icon'; +import PropTypes from 'prop-types'; + +class DropdownMenu extends Component { + state = { + open: false, + }; + + static propTypes = { + titleElement: PropTypes.node, + title: PropTypes.string, + align: PropTypes.oneOf(['left', 'center', 'right']), + /** Items to render in the select's drop down */ + list: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string.isRequired, + icon: PropTypes.object, + onClick: PropTypes.func, + link: PropTypes.string, + }) + ), + }; + + getListItems = () => { + const { list, align } = this.props; + + return list.map(({ icon, title, link, onClick }, key) => { + if (link) { + return ( + this.handleOnClick(onClick)} + > + {icon && } + {title} + + ); + } else { + return ( + + ); + } + }); + }; + + renderList = () => { + const { align } = this.props; + + if (!this.state.open) { + return null; + } + + return ( +
    + {this.getListItems()} +
    + ); + }; + + handleOnClick = onClick => { + this.toggleList(); + + if (onClick) { + onClick(); + } + }; + + handleMouseClick = e => { + if (this.node.contains(e.target)) { + return; + } + + this.toggleList(); + }; + + renderTitleElement = () => { + const { titleElement, title } = this.props; + + if (titleElement) { + return titleElement; + } + + return ( + + {title} + + + ); + }; + + toggleList = () => { + const { open } = this.state; + let state = true; + + document.addEventListener('mousedown', this.handleMouseClick, false); + + if (open) { + document.removeEventListener('mousedown', this.handleMouseClick, false); + state = false; + } + + this.setState({ + open: state, + }); + }; + + render() { + return ( +
    (this.node = node)}> +
    + {this.renderTitleElement()} +
    + + {this.renderList()} +
    + ); + } +} + +export { DropdownMenu }; diff --git a/packages/_components/src/elements/form/Label.css b/packages/_components/src/elements/form/Label.css new file mode 100644 index 000000000..6d0a170b1 --- /dev/null +++ b/packages/_components/src/elements/form/Label.css @@ -0,0 +1,8 @@ +.label-ohif { + font-size: 1em; + color: #ffffff; +} + +.label-example { + background-color: var(--ui-gray-darker); +} diff --git a/packages/_components/src/elements/form/Label.js b/packages/_components/src/elements/form/Label.js new file mode 100644 index 000000000..06086fbd6 --- /dev/null +++ b/packages/_components/src/elements/form/Label.js @@ -0,0 +1,27 @@ +import './Label.css'; + +import React from 'react'; + +import PropTypes from 'prop-types'; + +class Label extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.props.text }; + } + + render() { + return ( + + ); + } +} + +Label.propTypes = { + text: PropTypes.string.isRequired, + for: PropTypes.string, +}; + +export { Label }; diff --git a/packages/_components/src/elements/form/Range.css b/packages/_components/src/elements/form/Range.css new file mode 100644 index 000000000..434a8486f --- /dev/null +++ b/packages/_components/src/elements/form/Range.css @@ -0,0 +1,64 @@ +.range { + margin: 0; + width: 100%; + -webkit-appearance: none; +} + +.range:focus { + outline: none; +} + +.range::-webkit-slider-runnable-track { + width: 100%; + height: 2px; + cursor: pointer; + box-shadow: none; + background-color: var(--ui-border-color-dark); + border-radius: 0px; + border: 0px solid var(--ui-border-color-dark); +} + +.range::-moz-range-track { + width: 100%; + height: 2px; + cursor: pointer; + box-shadow: none; + background-color: var(--ui-border-color-dark); + border-radius: 0px; + border: 0px solid var(--ui-border-color-dark); +} + +.range::-webkit-slider-thumb { + box-shadow: none; + border: 0px solid var(--active-color); + box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25); + height: 21px; + width: 11px; + border-radius: 11px; + background: var(--active-color); + cursor: pointer; + -webkit-appearance: none; + margin-top: -10px; +} + +.range::-moz-range-thumb { + box-shadow: none; + border: 0px solid var(--active-color); + box-shadow: 0px 10px 10px rgba(0, 0, 0, 0.25); + height: 21px; + width: 11px; + border-radius: 11px; + background: var(--active-color); + cursor: pointer; + -webkit-appearance: none; + margin-top: -10px; +} + +.range::-moz-focus-outer { + border: 0; +} + +.range-example { + background: black; + height: 2em; +} diff --git a/packages/_components/src/elements/form/Range.js b/packages/_components/src/elements/form/Range.js new file mode 100644 index 000000000..a8647a8fa --- /dev/null +++ b/packages/_components/src/elements/form/Range.js @@ -0,0 +1,40 @@ +import './Range.css'; + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +class Range extends React.Component { + constructor(props) { + super(props); + this.state = { value: props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( + + ); + } +} + +Range.propTypes = { + value: PropTypes.number, + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + id: PropTypes.string, + onChange: PropTypes.func, +}; + +export { Range }; diff --git a/packages/_components/src/elements/form/Select.css b/packages/_components/src/elements/form/Select.css new file mode 100644 index 000000000..0ea43c5a9 --- /dev/null +++ b/packages/_components/src/elements/form/Select.css @@ -0,0 +1,46 @@ +.select-ohif { + display: block; + font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; + font-size: 0.8em; + font-weight: 600; + color: #444; + line-height: 1.3; + padding: 0.6em 1.4em 0.5em 0.8em; + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 0; + border: 1px solid #aaa; + box-shadow: 0 1px 0 1px rgba(0, 0, 0, 0.04); + border-radius: 0.5em; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + background-color: #fff; + background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'), + linear-gradient(to bottom, #ffffff 0%, #e5e5e5 100%); + background-repeat: no-repeat, repeat; + background-position: right 0.7em top 50%, 0 0; + background-size: 0.65em auto, 100%; +} + +.select-ohif::-ms-expand { + display: none; +} + +.select-ohif:hover { + border-color: #888; +} + +.select-ohif:focus { + border-color: #aaa; + box-shadow: 0 0 1px 3px rgba(59, 153, 252, 0.7); + box-shadow: 0 0 0 3px -moz-mac-focusring; + color: #222; + outline: none; +} + +.select-ohif option { + font-weight: normal; +} diff --git a/packages/_components/src/elements/form/Select.js b/packages/_components/src/elements/form/Select.js new file mode 100644 index 000000000..5652f9a8c --- /dev/null +++ b/packages/_components/src/elements/form/Select.js @@ -0,0 +1,48 @@ +import './Select.css'; + +import React, { Component } from 'react'; + +import PropTypes from 'prop-types'; + +class Select extends Component { + constructor(props) { + super(props); + this.state = { value: this.props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( + + ); + } +} + +Select.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + key: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ), + value: PropTypes.string, + onChange: PropTypes.func, +}; + +export { Select }; diff --git a/packages/_components/src/elements/form/TextArea.css b/packages/_components/src/elements/form/TextArea.css new file mode 100644 index 000000000..72e8b2860 --- /dev/null +++ b/packages/_components/src/elements/form/TextArea.css @@ -0,0 +1,7 @@ +.textarea-ohif { + background-color: #b6b6b6; + border-color: #b6b6b6; + font-family: Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', + sans-serif; + font-size: 1em; +} diff --git a/packages/_components/src/elements/form/TextArea.js b/packages/_components/src/elements/form/TextArea.js new file mode 100644 index 000000000..7f132f267 --- /dev/null +++ b/packages/_components/src/elements/form/TextArea.js @@ -0,0 +1,39 @@ +import './TextArea.css'; + +import React from 'react'; +import PropTypes from 'prop-types'; + +class TextArea extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.props.value }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + if (this.props.onChange) this.props.onChange(); + }; + + render() { + return ( +