more shifting

This commit is contained in:
dannyrb 2019-07-04 16:19:28 -04:00
parent b73ea99971
commit e58ba39a60
432 changed files with 51231 additions and 0 deletions

View File

@ -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"

View File

@ -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
}

View File

@ -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"
]
}

View File

@ -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

11
packages/_components/.env Normal file
View File

@ -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

View File

@ -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"
}
}
}

1
packages/_components/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text eol=lf

View File

@ -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**

View File

@ -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!
### 👉 &nbsp; [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.

View File

@ -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 ^_^

32
packages/_components/.gitignore vendored Normal file
View File

@ -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/

View File

@ -0,0 +1 @@
10.15.3

View File

@ -0,0 +1,8 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"sysoev.language-stylus",
"silvenon.mdx",
"dbaeumer.vscode-eslint"
]
}

View File

@ -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"
}

View File

@ -0,0 +1,165 @@
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<div align="center">
<h1>react-viewerbase</h1>
<p><strong>React Viewerbase is a collection of components and utilities</strong> that power OHIF's <a href="https://github.com/OHIF/Viewers">zero-footprint DICOM viewer</a>.</p>
</div>
<div align="center">
<a href="https://react.ohif.org/"><strong>Read The Docs</strong></a> |
<a href="https://react.ohif.org/contributing">Edit the docs</a>
</div>
<hr />
[![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]
<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->
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 (
<LayoutButton
selectedCell={this.state.selectedCell}
onChange={cell => 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)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore -->
<table><tr><td align="center"><a href="https://github.com/swederik"><img src="https://avatars3.githubusercontent.com/u/607793?v=4" width="100px;" alt="Erik Ziegler"/><br /><sub><b>Erik Ziegler</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=swederik" title="Code">💻</a> <a href="#maintenance-swederik" title="Maintenance">🚧</a></td><td align="center"><a href="http://dannyrb.com/"><img src="https://avatars1.githubusercontent.com/u/5797588?v=4" width="100px;" alt="Danny Brown"/><br /><sub><b>Danny Brown</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=dannyrb" title="Code">💻</a> <a href="#maintenance-dannyrb" title="Maintenance">🚧</a></td><td align="center"><a href="https://github.com/galelis"><img src="https://avatars3.githubusercontent.com/u/2378326?v=4" width="100px;" alt="Gustavo André Lelis"/><br /><sub><b>Gustavo André Lelis</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=galelis" title="Code">💻</a> <a href="#maintenance-galelis" title="Maintenance">🚧</a></td><td align="center"><a href="https://github.com/maltempi"><img src="https://avatars3.githubusercontent.com/u/7017182?v=4" width="100px;" alt="Thiago Maltempi"/><br /><sub><b>Thiago Maltempi</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=maltempi" title="Code">💻</a></td><td align="center"><a href="https://www.linkedin.com/in/siliconvalleynextgeneration/"><img src="https://avatars0.githubusercontent.com/u/1230575?v=4" width="100px;" alt="Esref Durna"/><br /><sub><b>Esref Durna</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=EsrefDurna" title="Code">💻</a></td><td align="center"><a href="http://www.isomics.com"><img src="https://avatars0.githubusercontent.com/u/126077?v=4" width="100px;" alt="Steve Pieper"/><br /><sub><b>Steve Pieper</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=pieper" title="Code">💻</a></td><td align="center"><a href="http://www.biharck.com.br"><img src="https://avatars0.githubusercontent.com/u/1713255?v=4" width="100px;" alt="Biharck Araujo"/><br /><sub><b>Biharck Araujo</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=biharck" title="Code">💻</a></td></tr><tr><td align="center"><a href="https://rodrigoea.com/"><img src="https://avatars3.githubusercontent.com/u/1905961?v=4" width="100px;" alt="Rodrigo Antinarelli"/><br /><sub><b>Rodrigo Antinarelli</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=rodrigolabs" title="Code">💻</a></td><td align="center"><a href="https://github.com/jfmedeiros1820"><img src="https://avatars1.githubusercontent.com/u/2211708?v=4" width="100px;" alt="João Felipe de Medeiros Moreira"/><br /><sub><b>João Felipe de Medeiros Moreira</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=jfmedeiros1820" title="Code">💻</a></td><td align="center"><a href="https://github.com/jamesg1"><img src="https://avatars3.githubusercontent.com/u/3621147?v=4" width="100px;" alt="James Gosbell"/><br /><sub><b>James Gosbell</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=jamesg1" title="Code">💻</a></td><td align="center"><a href="https://github.com/evren217"><img src="https://avatars1.githubusercontent.com/u/4920551?v=4" width="100px;" alt="Evren Ozkan"/><br /><sub><b>Evren Ozkan</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=evren217" title="Code">💻</a></td><td align="center"><a href="https://github.com/zsaltzman"><img src="https://avatars1.githubusercontent.com/u/19156530?v=4" width="100px;" alt="Zach S."/><br /><sub><b>Zach S.</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=zsaltzman" title="Code">💻</a></td><td align="center"><a href="https://github.com/muakdogan"><img src="https://avatars0.githubusercontent.com/u/19971240?v=4" width="100px;" alt="Mete Ugur Akdogan"/><br /><sub><b>Mete Ugur Akdogan</b></sub></a><br /><a href="https://github.com/OHIF/react-viewerbase/commits?author=muakdogan" title="Code">💻</a></td></tr></table>
<!-- ALL-CONTRIBUTORS-LIST:END -->
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)
<!--
Links:
-->
<!-- prettier-ignore-start -->
[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
<!-- prettier-ignore-end -->

View File

@ -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 })
}
*/

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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;
}

View File

@ -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',
],
},
}),
],
};

View File

@ -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%;
}

View File

@ -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 <div>{JSON.stringify(props)}</div>;
}
function EmptyViewport() {
return (
<div className="EmptyViewport">
<p>Please drag a stack here to view images.</p>
</div>
);
}
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 (
<PluginComponent
viewportData={data}
viewportIndex={viewportIndex}
children={[children]}
/>
);
}
return <EmptyViewport />;
}
getContent(childComponent, supportsDragAndDrop, viewportIndex) {
if (supportsDragAndDrop) {
return (
<LayoutPanelDropTarget
onDrop={this.onDrop}
viewportIndex={viewportIndex}
>
{childComponent}
</LayoutPanelDropTarget>
);
}
return <div className="LayoutPanel">{childComponent}</div>;
}
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 (
<div key={viewportIndex} className={className} style={{ ...layout }}>
{content}
</div>
);
});
return <div className={LayoutManager.className}>{viewportElements}</div>;
}
}
export default LayoutManager;

View File

@ -0,0 +1,12 @@
.LayoutPanelDropTarget {
width: 100%;
height: 100%;
opacity: 1;
transition: 0.3s all ease;
}
.LayoutPanelDropTarget.hovered {
opacity: 0.5;
cursor: copy;
}

View File

@ -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(
<div className={className}>{this.props.children}</div>
);
}
}
const collect = (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
canDrop: monitor.canDrop(),
isOver: monitor.isOver(),
});
export default DropTarget(Types.THUMBNAIL, divTarget, collect)(
LayoutPanelDropTarget
);

View File

@ -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 (
<div
className={`scrollArea ${this.props.class} ${
this.state.scrollAreaClasses
}`}
>
<div
className={scrollableClass}
ref={element => {
this.scrollableElement = element;
}}
onScroll={this.scrollHandlerThrottled}
onMouseEnter={this.scrollHandlerThrottled}
onTransitionEnd={this.scrollHandlerThrottled}
>
{this.props.children}
</div>
<div className="scrollNav scrollNavUp" onClick={this.scrollNavUp}>
{/* <svg className="scrollNavIcon"> */}
<Icon name="angle-double-up" />
</div>
<div className="scrollNav scrollNavDown" onClick={this.scrollNavDown}>
<Icon name="angle-double-down" />
</div>
</div>
);
}
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,
});
}
};
}

View File

@ -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)

View File

@ -0,0 +1,17 @@
import React, { Fragment } from 'react';
import { Link } from 'docz';
import PropTypes from 'prop-types';
const NameSpace = ({ name }) => (
<Fragment>
The namespace used on this component was: <pre>{name}</pre> Check the{' '}
<Link to="/translating#how-to-translate-components">translation docs</Link>{' '}
to see how to override it.
</Fragment>
);
NameSpace.propTypes = {
name: PropTypes.string.isRequired,
};
export default NameSpace;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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.

View File

@ -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:
<LanguageSwitcher />

View File

@ -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 (
<LayoutButton
selectedCell={this.state.selectedCell}
onChange={cell => 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 }`

View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/public/docz.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap">
<style>
/* "Tide" theme */
/* TODO:
- Convert the other themes to CSS Variables
- Allow user to set default CSS theme in JS config
- Export the themes as identical JS objects
- For theme switcher, update root-level CSS properties via JS (https://medium.com/@_bengarrison/accessing-and-modifying-css-variables-with-javascript-2ccb735bbff0)
*/
: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;
}
body {
font-family: 'Roboto', sans-serif !important;
}
</style>
{{ head }}
</head>
<body>
<div id="root" />
{{ footer }}
</body>
</html>

View File

@ -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'
```
<Playground>
<State
initial={{
selectedIndex: null,
listItems: [
{ label: 'Brain Left' },
{ label: 'Brain Right' },
{ label: 'Heart Left' },
{ label: 'Heart Right' },
],
}}
>
{({ state, setState }) => (
<TableList headerTitle="Example Table List Header">
{state.listItems.map((item, index) => {
return (
<TableListItem
key={`item_${index}`}
itemClass={state.selectedIndex === index ? 'selected' : ''}
itemIndex={index}
onItemClick={() => alert('item clicked')}
>
<label>{item.label}</label>
</TableListItem>
)
})}
</TableList>
)}
</State>
</Playground>
## FAQ
...
<!--
Links
-->
<!-- prettier-ignore-start -->
[demo-url]: https://docs.ohif.org/demo/
<!-- prettier-ignore-end -->

View File

@ -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;
}
```

View File

@ -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 <p>{t('my translated text')}</p>;
}
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)

View File

@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import LanguageSwitcher from '../components/languageSwitcher';
import './wrapper.styl';
const Wrapper = ({ children }) => (
<React.Fragment>
<div className="sidebarLanguageSwitcher">
<strong>Display components in:</strong>
<LanguageSwitcher />
</div>
{children}
</React.Fragment>
);
Wrapper.propTypes = {
children: PropTypes.node.isRequired,
};
export default Wrapper;

View File

@ -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%

View File

@ -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
<Playground>
<State
initial={{
checked : false,
label : 'Sample check'
}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
<Checkbox {...state} />
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={Checkbox} />
## Translation Namespace
<NameSpace name="Checkbox" />

View File

@ -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;
}

View File

@ -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 = <span className="ohif-checkbox ohif-checked" />;
} else {
checkbox = <span className="ohif-checkbox" />;
}
return (
<div className="ohif-check-container">
<form>
<label className="ohif-check-label">
<input
type="checkbox"
checked={this.state.checked}
onChange={e => {
this.handleChange(e);
}}
/>
{checkbox}
{this.state.label}
</label>
</form>
</div>
);
}
}

View File

@ -0,0 +1 @@
export { Checkbox } from './checkbox.js';

View File

@ -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 (
<div className="CineDialog">
<div className="noselect double-row-style">
<div className="cine-controls">
<div className="btn-group">
<button
title={t('Skip to first image')}
className="btn"
data-toggle="tooltip"
onClick={this.onClickSkipToStart}
>
<Icon name="fast-backward" />
</button>
<button
title={t('Previous image')}
className="btn"
data-toggle="tooltip"
onClick={this.onClickBackButton}
>
<Icon name="step-backward" />
</button>
<button
title={t('Play / Stop')}
className="btn"
data-toggle="tooltip"
onClick={this.onClickPlayPause}
>
<Icon name={this.state.isPlaying ? 'stop' : 'play'} />
</button>
<button
title={t('Next image')}
className="btn"
data-toggle="tooltip"
onClick={this.onClickNextButton}
>
<Icon name="step-forward" />
</button>
<button
title={t('Skip to last image')}
className="btn"
data-toggle="tooltip"
onClick={this.onClickSkipToEnd}
>
<Icon name="fast-forward" />
</button>
</div>
</div>
<div className="cine-options">
<div className="fps-section">
<input
type="range"
name="cineFrameRate"
min={this.props.cineMinFrameRate}
max={this.props.cineMaxFrameRate}
step={this.props.cineStepFrameRate}
value={this.state.cineFrameRate}
onChange={this.handleInputChange}
/>
</div>
<span className="fps">
{this.state.cineFrameRate.toFixed(1)} {t('fps')}
</span>
</div>
</div>
</div>
);
}
}
const connectedComponent = withTranslation('CineDialog')(CineDialog);
export { connectedComponent as CineDialog };
export default connectedComponent;

View File

@ -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)

View File

@ -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
<Playground>
<State
initial={{
isPlaying: false,
cineFrameRate: 24,
isLoopEnabled: true,
lastChange: '',
}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
<CineDialog
{...state}
onClickSkipToStart={() =>
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 })}
/>
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={CineDialog} />
## Translation Namespace
<NameSpace name="CineDialog" />

View File

@ -0,0 +1,2 @@
import CineDialog from './CineDialog';
export { CineDialog };

View File

@ -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,
};

View File

@ -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 (
<select
name="language-select"
id="language-select"
className="language-select"
value={currentLanguage}
onChange={onChange}
>
{languages.map(language => (
<option key={language.value} value={language.value}>
{language.label}
</option>
))}
</select>
);
};
export default withTranslation('UserPreferencesModal')(LanguageSwitcher);

View File

@ -0,0 +1,4 @@
.language-select
color: var(--primary-background-color)
display: block
min-width: 150px

View File

@ -0,0 +1 @@
export { default } from './LanguageSwitcher';

View File

@ -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 (
<div className="btn-group">
<ToolbarButton
isActive={this.state.dropdownVisible}
label={'Layout'}
icon="th"
onClick={this.onClick}
/>
<LayoutChooser
visible={this.state.dropdownVisible}
onChange={this.onChange}
onClick={this.onClick}
selectedCell={this.props.selectedCell}
/>
</div>
);
}
}
export default LayoutButton;

View File

@ -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 (
<div className="layoutChooser dropdown-menu" role="menu" style={style}>
<table>
<tbody>
{this.state.table.map((row, i) => {
return (
<tr key={i}>
{row.map((cell, j) => {
return (
<td
className={cell.className}
style={{
width: this.props.boxSize,
height: this.props.boxSize,
border: 'solid 1px black',
}}
key={j}
onMouseEnter={() => this.highlightCells(cell)}
onMouseLeave={() => this.highlightCells(this.emptyCell)}
onClick={() => this.onClick(cell)}
/>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
);
}
}
export { LayoutChooser };

View File

@ -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

View File

@ -0,0 +1,10 @@
.LayoutExample {
background-color: var(--primary-background-color);
width: 160px;
height: 160px;
}
.GridLayout td {
padding: 5px;
border: 1px solid black;
}

View File

@ -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
<Playground>
<State initial={{
selectedCell: {
className: 'hover',
col: 1,
row: 1
},
}}>
{({ state, setState }) => (
<React.Fragment>
{/* STATE */}
<div><pre>{JSON.stringify(state, null, 2) }</pre></div>
{/* COMPONENT */}
<div className="LayoutExample">
<LayoutButton
{...state}
onChange={cell => setState({ selectedCell: cell })}
/>
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={LayoutButton} />
## Translation Namespace
<NameSpace name="Buttons" />

View File

@ -0,0 +1,2 @@
export { LayoutButton } from './LayoutButton.js';
export { LayoutChooser } from './LayoutChooser.js';

View File

@ -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 (
<div className="measurementTable">
<div className="measurementTableHeader">
{hasOverallWarnings && (
<OverlayTrigger
key={'overwall-warning'}
placement="left"
overlay={
<Tooltip
placement="left"
className="in tooltip-warning"
id="tooltip-left"
style={{}}
>
<div className="warningTitle">
{this.props.t('Criteria nonconformities')}
</div>
<div className="warningContent">
{this.getWarningContent()}
</div>
</Tooltip>
}
>
<span className="warning-status">
<span className="warning-border">
<Icon name="exclamation-triangle" />
</span>
</span>
</OverlayTrigger>
)}
{this.getTimepointsHeader()}
</div>
<ScrollableArea>
<div>{this.getMeasurementsGroups()}</div>
</ScrollableArea>
</div>
);
}
getMeasurementsGroups = () => {
return this.props.measurementCollection.map((measureGroup, index) => {
return (
<TableList
key={index}
customHeader={this.getCustomHeader(measureGroup)}
>
{this.getMeasurements(measureGroup)}
</TableList>
);
});
};
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 (
<MeasurementTableItem
key={key}
itemIndex={itemIndex}
itemClass={itemClass}
measurementData={measurement}
onItemClick={this.onItemClick}
onRelabel={this.props.onRelabelClick}
onDelete={this.props.onDeleteClick}
onEditDescription={this.props.onEditDescriptionClick}
/>
);
});
};
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 (
<React.Fragment>
<div className="tableListHeaderTitle">
{this.props.t(measureGroup.groupName)}
</div>
{measureGroup.maxMeasurements && (
<div className="maxMeasurements">
{this.props.t('MAX')} {measureGroup.maxMeasurements}
</div>
)}
<div className="numberOfItems">{measureGroup.measurements.length}</div>
</React.Fragment>
);
};
getTimepointsHeader = () => {
return this.props.timepoints.map((timepoint, index) => {
return (
<div key={index} className="measurementTableHeaderItem">
<div className="timepointLabel">{timepoint.label}</div>
<div className="timepointDate">{timepoint.date}</div>
</div>
);
});
};
getWarningContent = () => {
const { warningList = '' } = this.props.overwallWarnings;
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
return <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{warningList}</React.Fragment>;
}
};
}
const connectedComponent = withTranslation('MeasurementTable')(
MeasurementTable
);
export { connectedComponent as MeasurementTable };
export default connectedComponent;

View File

@ -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;

View File

@ -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 (
<React.Fragment>
{hasWarnings ? (
<OverlayTrigger
key={this.props.itemIndex}
placement="left"
overlay={
<Tooltip
placement="left"
className="in tooltip-warning"
id="tooltip-left"
>
<div className="warningTitle">{this.props.t(warningTitle)}</div>
<div className="warningContent">{this.getWarningContent()}</div>
</Tooltip>
}
>
<div>{this.getTableListItem()}</div>
</OverlayTrigger>
) : (
<React.Fragment>{this.getTableListItem()}</React.Fragment>
)}
</React.Fragment>
);
}
getActionButton = (btnLabel, onClickCallback) => {
return (
<button key={btnLabel} className="btnAction" onClick={onClickCallback}>
<span style={{ marginRight: '4px' }}>
<Icon name="edit" width="14px" height="14px" />
</span>
{this.props.t(btnLabel)}
</button>
);
};
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 (
<TableListItem
itemKey={this.props.measurementData.measurementNumber}
itemClass={`measurementItem ${this.props.itemClass} ${hasWarningClass}`}
itemIndex={this.props.itemIndex}
onItemClick={this.onItemClick}
>
<div>
<div className="measurementLocation">
{this.props.t(this.props.measurementData.label)}
</div>
<div>{this.getDataDisplayText()}</div>
<div className="rowActions">{actionButtons}</div>
</div>
</TableListItem>
);
};
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 (
<div key={`displayText_${index}`} className="measurementDisplayText">
{data.displayText ? data.displayText : '...'}
</div>
);
});
};
getWarningContent = () => {
const { warningList = '' } = this.props.measurementData;
if (Array.isArray(warningList)) {
const listedWarnings = warningList.map((warn, index) => {
return <li key={index}>{warn}</li>;
});
return <ol>{listedWarnings}</ol>;
} else {
return <React.Fragment>{warningList}</React.Fragment>;
}
};
}
const connectedComponent = withTranslation('MeasurementTable')(
MeasurementTableItem
);
export { connectedComponent as MeasurementTableItem };
export default connectedComponent;

View File

@ -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;

View File

@ -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
<Playground>
<State
initial={{
// Spelling error: overwallWarnings
overwallWarnings: warnings,
timepoints,
measurementCollection: measurements,
}}
>
{({ state, setState }) => (
<div style={{ height: '340px' }}>
<MeasurementTable
{...state}
onItemClick={() => alert('onItemClick')}
onRelabelClick={() => alert('onRelabelClick')}
onDeleteClick={() => alert('onDeleteClick')}
onEditDescriptionClick={() => alert('onEditDescriptionClick')}
/>
</div>
)}
</State>
</Playground>
## API
<Props of={MeasurementTable} />
## Translation Namespace
<NameSpace name="MeasurementTable" />

View File

@ -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',
},
],
},
],
},
];

View File

@ -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',
},
];

View File

@ -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',
],
};

View File

@ -0,0 +1,2 @@
export { MeasurementTable } from './MeasurementTable.js';
export { MeasurementTableItem } from './MeasurementTableItem.js';

View File

@ -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 (
<Transition {...props}>
{(status, innerProps) =>
React.cloneElement(children, {
...innerProps,
className: classNames(
'fade',
className,
children.props.className,
fadeStyles[status]
),
})
}
</Transition>
);
}
}
Fade.propTypes = propTypes;
Fade.defaultProps = defaultProps;
export default Fade;

View File

@ -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 (
<BaseOverlay {...props} transition={transition}>
{child}
</BaseOverlay>
);
}
}
Overlay.propTypes = propTypes;
Overlay.defaultProps = defaultProps;
const connectedComponent = withTranslation()(Overlay);
export { connectedComponent as Overlay };
export default connectedComponent;

View File

@ -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 `<Overlay>`.
/**
* @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
{...props}
show={this.state.show}
onHide={this.handleHide}
target={this}
>
{overlay}
</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 };

View File

@ -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
<Playground>
<div style={{
display: 'flex',
justifyContent: 'space-between'
}}>
{
['right', 'left', 'bottom', 'top'].map((placement) => {
return (
<React.Fragment key={`example-${placement}`}>
<OverlayTrigger
key={`example-${placement}`}
placement={placement}
overlay={
<Tooltip
placement={placement}
className='in'
id={`tooltip-${placement}`}
>
Here I am!
</Tooltip>
}
>
<button> Tooltip on {placement}</button>
</OverlayTrigger>
</React.Fragment>
)
})
}
</div>
</Playground>
### On Click
<Playground>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
{['right', 'left', 'bottom', 'top'].map(placement => {
return (
<OverlayTrigger
key={`example-${placement}`}
placement={placement}
trigger="click"
overlay={
<Tooltip
placement={placement}
className="in"
id={`tooltip-${placement}`}
>
Here I am!
</Tooltip>
}
>
<button> Tooltip on {placement}</button>
</OverlayTrigger>
)
})}
</div>
</Playground>
## API
<Props of={OverlayTrigger} />

View File

@ -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;

View File

@ -0,0 +1,2 @@
export { Overlay } from './Overlay.js';
export { OverlayTrigger } from './OverlayTrigger.js';

View File

@ -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 (
<div
className={`series-quick-switch clearfix noselect ${
this.state.sideClass
} ${quickSwitchClass}`}
onMouseLeave={this.hideSeriesSwitch}
>
<div className="series-switch" onMouseEnter={this.showSeriesSwitch}>
<div className="title-label">Series</div>
<div className="series-box">
{this.getSmallListItems()}
<ScrollableArea scrollStep={201} class="series-browser">
<SeriesList
seriesItems={this.getSeriesItems()}
onClick={this.onSeriesClick}
activeDisplaySetInstanceUid={
this.state.activeDisplaySetInstanceUid
}
/>
</ScrollableArea>
</div>
</div>
<div className="study-switch">
<div className="title-label">Study</div>
<div className="study-box">
<ScrollableArea scrollStep={91} class="study-browser">
<StudiesList
studyListData={this.props.studyListData}
onClick={this.onStudyClick}
activeStudyInstanceUid={this.state.activeStudyInstanceUid}
/>
</ScrollableArea>
</div>
</div>
</div>
);
}
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 (
<div key={index} className={`series-item ${active ? 'active' : ''}`} />
);
});
};
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,
});
};
}

View File

@ -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

View File

@ -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 (
<React.Fragment>
<div className="study-browser-series clearfix thumbnails-wrapper">
<div className="study-series-container">{this.getSeriesItems()}</div>
</div>
</React.Fragment>
);
}
getSeriesItems = () => {
return this.props.seriesItems.map((seriesData, index) => {
return (
<ThumbnailEntry
key={seriesData.displaySetInstanceUid}
id={`series_thumb_${index}`}
{...seriesData}
active={
seriesData.displaySetInstanceUid ===
this.props.activeDisplaySetInstanceUid
}
onClick={() => this.props.onClick(seriesData)}
/>
);
});
};
}

View File

@ -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 (
<div
className={`studyBrowseItem${activeClass}`}
onClick={this.props.onClick}
>
<div className="studyItemBox">
<div className="studyModality">
<div
className="studyModalityText"
style={this.getModalitiesStyle()}
>
{modalities}
</div>
</div>
<div className="studyText">
{hasDescriptionAndDate ? (
<React.Fragment>
<div className="studyDate">{studyDate}</div>
<div className="studyDescription">{studyDescription}</div>
</React.Fragment>
) : (
<div className="studyAvailability">
{studyAvailable ? (
<React.Fragment>N/A</React.Fragment>
) : (
<React.Fragment>Click to load</React.Fragment>
)}
</div>
)}
</div>
</div>
</div>
);
}
getModalitiesStyle = () => {
return {};
};
}

View File

@ -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

View File

@ -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 (
<div className={`studiesList ${this.props.class}`}>
{this.getBrowserItems()}
</div>
);
}
getBrowserItems = () => {
return this.props.studyListData.map((studyData, index) => {
return (
<StudiesItem
key={index}
studyData={studyData}
active={
studyData.studyInstanceUid === this.props.activeStudyInstanceUid
}
onClick={event => this.props.onClick(studyData)}
/>
);
});
};
}

View File

@ -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
<Playground>
<State initial={{
studyListData: studies,
}}>
{({ state, setState }) => (
<QuickSwitch
studyListData={state.studyListData}
// If this is always activeStudy's thumbnails,
// Why do we need to specify this?
seriesListData={
state.studyListData.find((study) => {
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 });
}}
/>
)}
</State>
</Playground>
## API
<Props of={QuickSwitch} />

View File

@ -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,
},
],
},
];

View File

@ -0,0 +1 @@
export { QuickSwitch } from './QuickSwitch.js';

View File

@ -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;
}

View File

@ -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 = (
<input
type="radio"
checked={this.state.checked === button.id}
onChange={e => {
this.handleChange(e);
}}
value={button.id}
/>
);
//needed to style the custom radio check
let inputSpan;
if (this.state.checked === button.id) {
inputSpan = (
<span className="ohif-radio-button ohif-selected">{input}</span>
);
} else {
inputSpan = <span className="ohif-radio-button">{input}</span>;
}
return (
<span className="ohif-radio-button-container" key={button.id}>
<label className="ohif-radio-button-label">
{inputSpan}
{button.label}
</label>
</span>
);
});
return (
<div className="ohif-radio-button-group">
<form>{buttons}</form>
</div>
);
}
}

View File

@ -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
<Playground>
<State
initial={{
description : [
{
'label' : 'Button 1',
'id' : 'button1',
'checked' : false
},
{
'label' : 'Button 2',
'id' : 'button2',
'checked' : false,
},
{
'label' : 'Button 3',
'id' : 'button3',
'checked' : true,
}
]
}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>{JSON.stringify(state, null, 2)}</pre>
</div>
<div style={{ maxWidth: '300px', margin: '0 auto' }}>
<RadioButtonList description={[
{
'label' : 'Button 1',
'id' : 'button1',
'checked' : false
},
{
'label' : 'Button 2',
'id' : 'button2',
'checked' : false,
},
{
'label' : 'Button 3',
'id' : 'coolButton3',
'checked' : true,
}
]}/>
</div>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={RadioButtonList} />
## Translation Namespace
<NameSpace name="RadioButtonList" />

View File

@ -0,0 +1 @@
export { RadioButtonList } from './RadioButtonList.js';

View File

@ -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;
}

View File

@ -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 && <span>{option.text}</span>;
const iconProps =
typeof option.icon === 'string' ? { name: option.icon } : option.icon;
const bottomLabel = option.bottomLabel && (
<div className="bottomLabel">{option.bottomLabel}</div>
);
return (
<div
key={index}
className={className}
onClick={() => this.onClickOption(option.value)}
>
<div className="roundedButton">
{optionText}
{iconProps && <Icon {...iconProps} />}
</div>
{bottomLabel}
</div>
);
});
return <div className={className}>{buttons}</div>;
}
}
export { RoundedButtonGroup };

View File

@ -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
<Playground>
<State
initial={{
value: null,
options: [
{
value: 'studies',
icon: 'th',
bottomLabel: 'Series',
},
{
value: 'measurements',
icon: 'th-list',
bottomLabel: 'Measurements',
},
],
}}
>
{({ state, setState }) => (
<React.Fragment>
<div>
<pre>value: {JSON.stringify(state.value, null, 2)}</pre>
</div>
<RoundedButtonGroup
{...state}
onValueChanged={value => setState({ value })}
/>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={RoundedButtonGroup} />

View File

@ -0,0 +1 @@
export { RoundedButtonGroup } from './RoundedButtonGroup.js';

View File

@ -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 (
<label
className={'wrapperLabel radioLabel ' + labelClass}
htmlFor={this.props.id}
>
<input
type="radio"
id={this.props.id}
className="radioInput"
value={this.props.value}
onChange={this.onSelected}
/>
<span className="wrapperText">{this.props.label}</span>
</label>
);
}
onSelected = evt => {
this.props.onSelected(evt, this.props.itemData);
};
}

View File

@ -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 (
<div className="selectTree selectTreeRoot">
<div className="treeContent">
{this.headerItem()}
<div className="treeOptions">
{this.state.currentNode && (
<SelectTreeBreadcrumb
onSelected={this.onBreadcrumbSelected}
label={this.state.currentNode.label}
value={this.state.currentNode.value}
/>
)}
<div className="treeInputsWrapper">
<div className="treeInputs">{treeItems}</div>
</div>
</div>
</div>
</div>
);
}
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 (
<InputRadio
key={itemKey}
id={`${storageKey}_${item.value}`}
name={index}
itemData={item}
value={item.value}
label={item.label}
labelClass={this.getLabelClass(item)}
onSelected={this.onSelected}
/>
);
});
}
headerItem = () => {
let title = this.props.selectTreeFirstTitle;
if (this.state.currentNode && this.props.selectTreeSecondTitle) {
title = this.props.selectTreeSecondTitle;
}
return (
<div className="wrapperLabel treeHeader">
<div className="wrapperText">{title}</div>
{this.props.searchEnabled && (
<div className="wrapperSearch">
<div className="searchIcon">
<Icon name="search" />
</div>
<input
type="text"
className="searchInput"
placeholder="Search labels"
autoFocus={this.props.autoFocus}
onChange={this.searchLocations}
value={this.state.searchTerm ? this.state.searchTerm : ''}
/>
</div>
)}
</div>
);
};
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,
});
};
}

View File

@ -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;

View File

@ -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 (
<div className="selectTreeBreadcrumb">
<label
className="wrapperLabel radioLabel"
htmlFor="selectTreeBreadcrumb"
>
<input
type="radio"
id="selectTreeBreadcrumb"
className="treeNode radioInput"
value={this.props.value}
onChange={this.props.onSelected}
/>
<span className="wrapperText">
<span className="backIcon">
<Icon name="fast-backward" />
</span>
{this.props.label}
</span>
</label>
</div>
);
}
}

Some files were not shown because too many files have changed in this diff Show More