more shifting
This commit is contained in:
parent
b73ea99971
commit
e58ba39a60
@ -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"
|
||||
|
||||
135
packages/_components/.all-contributorsrc
Normal file
135
packages/_components/.all-contributorsrc
Normal 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
|
||||
}
|
||||
18
packages/_components/.babelrc
Normal file
18
packages/_components/.babelrc
Normal 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"
|
||||
]
|
||||
}
|
||||
85
packages/_components/.circleci/config.yml
Normal file
85
packages/_components/.circleci/config.yml
Normal 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
11
packages/_components/.env
Normal 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
|
||||
17
packages/_components/.eslintrc
Normal file
17
packages/_components/.eslintrc
Normal 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
1
packages/_components/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text eol=lf
|
||||
40
packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md
vendored
Normal file
40
packages/_components/.github/ISSUE_TEMPLATE/---bug-report.md
vendored
Normal 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**
|
||||
18
packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md
vendored
Normal file
18
packages/_components/.github/ISSUE_TEMPLATE/---feature-request.md
vendored
Normal 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!
|
||||
|
||||
### 👉 [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.
|
||||
15
packages/_components/.github/ISSUE_TEMPLATE/---support-question.md
vendored
Normal file
15
packages/_components/.github/ISSUE_TEMPLATE/---support-question.md
vendored
Normal 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
32
packages/_components/.gitignore
vendored
Normal 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/
|
||||
1
packages/_components/.nvmrc
Normal file
1
packages/_components/.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
10.15.3
|
||||
8
packages/_components/.vscode/extensions.json
vendored
Normal file
8
packages/_components/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"sysoev.language-stylus",
|
||||
"silvenon.mdx",
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
29
packages/_components/.vscode/settings.json
vendored
Normal file
29
packages/_components/.vscode/settings.json
vendored
Normal 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"
|
||||
}
|
||||
165
packages/_components/README.md
Normal file
165
packages/_components/README.md
Normal 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]
|
||||
[](#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 -->
|
||||
113
packages/_components/doczrc.js
Normal file
113
packages/_components/doczrc.js
Normal 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 })
|
||||
}
|
||||
*/
|
||||
21
packages/_components/netlify.toml
Normal file
21
packages/_components/netlify.toml
Normal 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
|
||||
127
packages/_components/package.json
Normal file
127
packages/_components/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
packages/_components/public/docz.css
Normal file
44
packages/_components/public/docz.css
Normal 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;
|
||||
}
|
||||
61
packages/_components/rollup.config.js
Normal file
61
packages/_components/rollup.config.js
Normal 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',
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
22
packages/_components/src/LayoutChooser/LayoutManager.css
Normal file
22
packages/_components/src/LayoutChooser/LayoutManager.css
Normal 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%;
|
||||
}
|
||||
162
packages/_components/src/LayoutChooser/LayoutManager.js
Normal file
162
packages/_components/src/LayoutChooser/LayoutManager.js
Normal 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;
|
||||
@ -0,0 +1,12 @@
|
||||
.LayoutPanelDropTarget {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
opacity: 1;
|
||||
transition: 0.3s all ease;
|
||||
}
|
||||
|
||||
.LayoutPanelDropTarget.hovered {
|
||||
opacity: 0.5;
|
||||
cursor: copy;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
152
packages/_components/src/ScrollableArea/ScrollableArea.js
Normal file
152
packages/_components/src/ScrollableArea/ScrollableArea.js
Normal 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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
69
packages/_components/src/ScrollableArea/ScrollableArea.styl
Normal file
69
packages/_components/src/ScrollableArea/ScrollableArea.styl
Normal 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)
|
||||
17
packages/_components/src/__docs__/NameSpace.js
Normal file
17
packages/_components/src/__docs__/NameSpace.js
Normal 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;
|
||||
BIN
packages/_components/src/__docs__/assets/github.png
Normal file
BIN
packages/_components/src/__docs__/assets/github.png
Normal file
Binary file not shown.
BIN
packages/_components/src/__docs__/assets/npm.png
Normal file
BIN
packages/_components/src/__docs__/assets/npm.png
Normal file
Binary file not shown.
BIN
packages/_components/src/__docs__/assets/react.png
Normal file
BIN
packages/_components/src/__docs__/assets/react.png
Normal file
Binary file not shown.
BIN
packages/_components/src/__docs__/assets/typescript.png
Normal file
BIN
packages/_components/src/__docs__/assets/typescript.png
Normal file
Binary file not shown.
15
packages/_components/src/__docs__/compatibility.mdx
Normal file
15
packages/_components/src/__docs__/compatibility.mdx
Normal 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.
|
||||
0
packages/_components/src/__docs__/config/Wrapper.js
Normal file
0
packages/_components/src/__docs__/config/Wrapper.js
Normal file
13
packages/_components/src/__docs__/docs-settings.mdx
Normal file
13
packages/_components/src/__docs__/docs-settings.mdx
Normal 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 />
|
||||
76
packages/_components/src/__docs__/getting-started.mdx
Normal file
76
packages/_components/src/__docs__/getting-started.mdx
Normal 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 }`
|
||||
80
packages/_components/src/__docs__/index.html
Normal file
80
packages/_components/src/__docs__/index.html
Normal 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>
|
||||
76
packages/_components/src/__docs__/introduction.mdx
Normal file
76
packages/_components/src/__docs__/introduction.mdx
Normal 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 -->
|
||||
56
packages/_components/src/__docs__/styling-and-theming.mdx
Normal file
56
packages/_components/src/__docs__/styling-and-theming.mdx
Normal 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;
|
||||
}
|
||||
```
|
||||
87
packages/_components/src/__docs__/translating.mdx
Normal file
87
packages/_components/src/__docs__/translating.mdx
Normal 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)
|
||||
22
packages/_components/src/__docs__/wrapper.js
Normal file
22
packages/_components/src/__docs__/wrapper.js
Normal 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;
|
||||
15
packages/_components/src/__docs__/wrapper.styl
Normal file
15
packages/_components/src/__docs__/wrapper.styl
Normal 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%
|
||||
@ -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" />
|
||||
56
packages/_components/src/components/checkbox/checkbox.css
Normal file
56
packages/_components/src/components/checkbox/checkbox.css
Normal 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;
|
||||
}
|
||||
46
packages/_components/src/components/checkbox/checkbox.js
Normal file
46
packages/_components/src/components/checkbox/checkbox.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
1
packages/_components/src/components/checkbox/index.js
Normal file
1
packages/_components/src/components/checkbox/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { Checkbox } from './checkbox.js';
|
||||
202
packages/_components/src/components/cineDialog/CineDialog.js
Normal file
202
packages/_components/src/components/cineDialog/CineDialog.js
Normal 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;
|
||||
169
packages/_components/src/components/cineDialog/CineDialog.styl
Normal file
169
packages/_components/src/components/cineDialog/CineDialog.styl
Normal 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)
|
||||
@ -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" />
|
||||
2
packages/_components/src/components/cineDialog/index.js
Normal file
2
packages/_components/src/components/cineDialog/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
import CineDialog from './CineDialog';
|
||||
export { CineDialog };
|
||||
48
packages/_components/src/components/index.js
Normal file
48
packages/_components/src/components/index.js
Normal 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,
|
||||
};
|
||||
@ -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);
|
||||
@ -0,0 +1,4 @@
|
||||
.language-select
|
||||
color: var(--primary-background-color)
|
||||
display: block
|
||||
min-width: 150px
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './LanguageSwitcher';
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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
|
||||
@ -0,0 +1,10 @@
|
||||
.LayoutExample {
|
||||
background-color: var(--primary-background-color);
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.GridLayout td {
|
||||
padding: 5px;
|
||||
border: 1px solid black;
|
||||
}
|
||||
@ -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" />
|
||||
@ -0,0 +1,2 @@
|
||||
export { LayoutButton } from './LayoutButton.js';
|
||||
export { LayoutChooser } from './LayoutChooser.js';
|
||||
@ -0,0 +1,178 @@
|
||||
import './MeasurementTable.styl';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { withTranslation } from '../../utils/LanguageProvider';
|
||||
|
||||
import { Icon } from './../../elements/Icon';
|
||||
import { MeasurementTableItem } from './MeasurementTableItem.js';
|
||||
import { OverlayTrigger } from './../overlayTrigger';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ScrollableArea } from './../../ScrollableArea/ScrollableArea.js';
|
||||
import { TableList } from './../tableList';
|
||||
import { Tooltip } from './../tooltip';
|
||||
|
||||
class MeasurementTable extends Component {
|
||||
static propTypes = {
|
||||
measurementCollection: PropTypes.array.isRequired,
|
||||
timepoints: PropTypes.array.isRequired,
|
||||
overallWarnings: PropTypes.object.isRequired,
|
||||
readOnly: PropTypes.bool,
|
||||
onItemClick: PropTypes.func,
|
||||
onRelabelClick: PropTypes.func,
|
||||
onDeleteClick: PropTypes.func,
|
||||
onEditDescriptionClick: PropTypes.func,
|
||||
selectedMeasurementNumber: PropTypes.number,
|
||||
overwallWarnings: PropTypes.object,
|
||||
t: PropTypes.func,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
overallWarnings: {
|
||||
warningList: [],
|
||||
},
|
||||
readOnly: false,
|
||||
};
|
||||
|
||||
state = {
|
||||
selectedKey: null,
|
||||
};
|
||||
|
||||
render() {
|
||||
const hasOverallWarnings =
|
||||
this.props.overallWarnings.warningList.length > 0;
|
||||
return (
|
||||
<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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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" />
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@ -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',
|
||||
},
|
||||
];
|
||||
@ -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',
|
||||
],
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export { MeasurementTable } from './MeasurementTable.js';
|
||||
export { MeasurementTableItem } from './MeasurementTableItem.js';
|
||||
102
packages/_components/src/components/overlayTrigger/Fade.js
Normal file
102
packages/_components/src/components/overlayTrigger/Fade.js
Normal 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;
|
||||
102
packages/_components/src/components/overlayTrigger/Overlay.js
Normal file
102
packages/_components/src/components/overlayTrigger/Overlay.js
Normal 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;
|
||||
@ -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 };
|
||||
@ -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} />
|
||||
@ -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;
|
||||
@ -0,0 +1,2 @@
|
||||
export { Overlay } from './Overlay.js';
|
||||
export { OverlayTrigger } from './OverlayTrigger.js';
|
||||
153
packages/_components/src/components/quickSwitch/QuickSwitch.js
Normal file
153
packages/_components/src/components/quickSwitch/QuickSwitch.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
209
packages/_components/src/components/quickSwitch/QuickSwitch.styl
Normal file
209
packages/_components/src/components/quickSwitch/QuickSwitch.styl
Normal 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
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -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 {};
|
||||
};
|
||||
}
|
||||
133
packages/_components/src/components/quickSwitch/StudiesItem.styl
Normal file
133
packages/_components/src/components/quickSwitch/StudiesItem.styl
Normal 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
|
||||
@ -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)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -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} />
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
1
packages/_components/src/components/quickSwitch/index.js
Normal file
1
packages/_components/src/components/quickSwitch/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { QuickSwitch } from './QuickSwitch.js';
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
@ -0,0 +1 @@
|
||||
export { RadioButtonList } from './RadioButtonList.js';
|
||||
@ -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;
|
||||
}
|
||||
@ -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 };
|
||||
@ -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} />
|
||||
@ -0,0 +1 @@
|
||||
export { RoundedButtonGroup } from './RoundedButtonGroup.js';
|
||||
37
packages/_components/src/components/selectTree/InputRadio.js
Normal file
37
packages/_components/src/components/selectTree/InputRadio.js
Normal 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);
|
||||
};
|
||||
}
|
||||
182
packages/_components/src/components/selectTree/SelectTree.js
Normal file
182
packages/_components/src/components/selectTree/SelectTree.js
Normal 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,
|
||||
});
|
||||
};
|
||||
}
|
||||
106
packages/_components/src/components/selectTree/SelectTree.styl
Normal file
106
packages/_components/src/components/selectTree/SelectTree.styl
Normal 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;
|
||||
@ -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
Loading…
Reference in New Issue
Block a user