Chore/kill yalc with fire (#629)

* init

* more shifting

* Move @babel dependencies up to workspace root

* Begin updating dependent libraries to use sync'd webpack builds w/ watches

* Commit changes before a long weekend

* Split packages into platform and extensions

* Consolidate how/where we specify file/module type entrypoints

* Make it possible to pass in extensions as App props

* Resolve viewer's modules

* Target for UMD bundle

* Add Webpack Stylus loader

* Provide WebPack build options for microscopy, vtk, and ui

* Better entrypoint for extensions

* Push changes up to switch PCs

* Fix ESM symlink build for Viewers

* Shift docs to root

* Specify additional lerna config props

* Begin providing basic readme info

* Rename example extension folder

* Shift links to bottom of doc

* First attempt at a netlify deploy preview

* Attempt to fix deploy preview build

* push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands

* Try to use global gitbook-cli

* Export node_modules path

* More agressive with modifying PATH

* Follow deploy log output advice

* try again to set bin path

* Try swapping bash with a new shell

* Try bash -l instead of exec bash

* Annd let's try again

* and again

* We may have figured it out johnny, boy

* We need to pull cornerstone-wado-image-loader files from hoisted node_modules

* Attempt to fix ticks/escapes

* Change copy syntax; try running tests on viewers from root for circleci

* Lower version to match circleci image

* Try alternative jest-canvas-mock location and version file syntax

* Run version command instead of calling node directly

* Try alternative jest transform

* Set path and enable workspaces

* Add codecov flags

* Use cpx so our copying finishes?

* Get all platform unit tests to run

* Run and report individually and in parallel

* Try once relying on codecov yaml to split w/ flags

* Add projects to split by flags

* Generate separate reports

* Trigger codecov after everything has finished running; these may not support flags

* Use recommended flags from issue comments for codecov

* Fix typo

* Remove clear flag

* Shift back to calling codecov from root

* Generate example for codecov issue

* Try fixing paths

* Simplify

* Try using codecov orb

* Bump circleci version

* Upload core and viewer

* Use full string paths

* Remove individual codecov calls

* Fix dir

* Combine files before upload

* Escape anchors

* tryfix syntax

* Escape string literal

* Combine lines to reduce path

* long paths

* Use home alias

* Fix filename

* Try to see the contents of our cat'd file

* Also upload `core`

* Try running with aliased folder

* Fix path; split PR and Merge unit tests into separate jobs

* Set default threshold

* Fix typo

* Changing to scoped package names

* Adding note

* Catch more updates

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.0
 - @ohif/extension-dicom-html@0.0.4-alpha.0
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0
 - @ohif/extension-dicom-pdf@0.0.8-alpha.0
 - @ohif/extension-vtk@0.1.4-alpha.0
 - @ohif/core@0.11.1-alpha.0
 - @ohif/i18n@0.2.3-alpha.0
 - @ohif/ui@0.2.18-alpha.0
 - @ohif/viewer@0.0.22-alpha.0

* Clean up primary readme

* Support for dev and dev:* commands

* Shift build command; satisfy default PWA build

* Remove old scripts

* Clean up UI to set webpack scripts

* Tidy up project links

* Clean duplicate code in UI project

* Update UI project's docs

* Clean up ui and i18n config

* Reduce duplicate code in `@ohif/core`

* Fix @ohif/ui versions

* bump @ohif/core versions

* Redux testkit dep

* Remove unused/duplicate config files for projects/packages

* ohif-core --> @ohif/core

* Safer publish command

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.1
 - @ohif/extension-dicom-html@0.0.4-alpha.1
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1
 - @ohif/extension-dicom-pdf@0.0.8-alpha.1
 - @ohif/extension-vtk@0.1.4-alpha.1
 - @ohif/core@0.11.1-alpha.1
 - @ohif/i18n@0.2.3-alpha.1
 - @ohif/ui@0.2.18-alpha.1
 - @ohif/viewer@0.0.22-alpha.1

* test version bump

* Add global prettier config

* chore(release): publish %s [skip ci]

 - @ohif/viewer@0.0.22-alpha.2

* test lerna publish

* Update package.jsons

* Fix build scripts

* catch changes

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.2
 - @ohif/extension-dicom-html@0.0.4-alpha.2
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2
 - @ohif/extension-dicom-pdf@0.0.8-alpha.2
 - @ohif/extension-vtk@0.1.4-alpha.2
 - @ohif/core@0.11.1-alpha.2
 - @ohif/i18n@0.2.3-alpha.2
 - @ohif/ui@0.2.18-alpha.2
 - @ohif/viewer@0.0.22-alpha.3

* fix eslint issues caused by eslint 6 and our react-app config

* Publish with canary tag

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.3
 - @ohif/extension-dicom-html@0.0.4-alpha.3
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3
 - @ohif/extension-dicom-pdf@0.0.8-alpha.3
 - @ohif/extension-vtk@0.1.4-alpha.3
 - @ohif/core@0.11.1-alpha.3
 - @ohif/i18n@0.2.3-alpha.3
 - @ohif/ui@0.2.18-alpha.3
 - @ohif/viewer@0.0.22-alpha.4

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.4
 - @ohif/extension-dicom-html@0.0.4-alpha.4
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4
 - @ohif/extension-dicom-pdf@0.0.8-alpha.4
 - @ohif/extension-vtk@0.1.4-alpha.4
 - @ohif/core@0.11.1-alpha.4
 - @ohif/i18n@0.2.3-alpha.4
 - @ohif/ui@0.2.18-alpha.4
 - @ohif/viewer@0.0.22-alpha.5

* Simplify build

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.5
 - @ohif/extension-dicom-html@0.0.4-alpha.5
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5
 - @ohif/extension-dicom-pdf@0.0.8-alpha.5
 - @ohif/extension-vtk@0.1.4-alpha.5
 - @ohif/core@0.11.1-alpha.5
 - @ohif/i18n@0.2.3-alpha.5
 - @ohif/ui@0.2.18-alpha.5
 - @ohif/viewer@0.0.22-alpha.6

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.6
 - @ohif/extension-dicom-html@0.0.4-alpha.6
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6
 - @ohif/extension-dicom-pdf@0.0.8-alpha.6
 - @ohif/extension-vtk@0.1.4-alpha.6
 - @ohif/core@0.11.1-alpha.6
 - @ohif/i18n@0.2.3-alpha.6
 - @ohif/ui@0.2.18-alpha.6
 - @ohif/viewer@0.0.22-alpha.7

* Split version and publish

* chore(release): publish %s [skip ci]

 - @ohif/extension-cornerstone@0.0.39-alpha.7
 - @ohif/extension-dicom-html@0.0.4-alpha.7
 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7
 - @ohif/extension-dicom-pdf@0.0.8-alpha.7
 - @ohif/extension-vtk@0.1.4-alpha.7
 - @ohif/core@0.11.1-alpha.7
 - @ohif/i18n@0.2.3-alpha.7
 - @ohif/ui@0.2.18-alpha.7
 - @ohif/viewer@0.0.22-alpha.8

* Try simplify netlify build

* react-viewerbase --> @ohif/ui

* add test for stylus loader

* Try new way to specify publish path

* Use an output folder that won't be ignored

* Try move command

* fiddle with paths

* Dig for some information

* Make sure we're in root for our commands

* Remove discovery

* Setup script-tag example

* Fix typo

* Move config folder

* Update cornerstone extension to use shared/root webpack configs

* capture changes

* Update extension webpack configs

* Don't check in docz output

* Fix babel runtime resolution issue

* Simplify ui build steps

* Push in progress

* Shift directories

* Get viewer dev path to work

* Fix viewer commonjs build

* Fix build pwa config for viewers

* Make sure our package specific command can be called from root

* Update notes

* Fix imports and misc. merge notes

* Bump wado-image-loader version

* Switch up commands to remove webworkers copy

* Fix CI config

* Fix more broken package references

* Add react-dropzone

* Persist so e2e test can use

* Fix lockfile

* Use cypress orb to simplify CI

* Fix config

* Try again

* Install yarn latest for orb

* Install yarn a different way

* Wait on correct port

* Run tests in viewer project

* Fix cypress config

* Make Cypress happier - use default location

* Simplify e2e testing

* Capture UI changes since fork

* Catch core up

* Grab latest deps after merge

* Fix broken import

* Make sure cornerstone-wado-image-loader resolves to 3+

* Note regarding orbs

* Fix refresh on 404 routes

* Fix odd error for cornerstone-wado-image-loader

* Different target for UMD

* Simplify dev script

* Try to get fancy with netlify output

* Let's do it again

* Try to fix measurementTools issue

* See if we can get our lerna magic to work on merge to monorepo branch
This commit is contained in:
Danny Brown authored and GitHub committed 2019-08-13 15:36:46 -04:00
1 parent cac911f08e
commit 08ff214ea6
919 files changed
+45652 -52097

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
{
"projectName": "@ohif/ui",
"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
}
+11
View File
@@ -0,0 +1,11 @@
####
##
## We need to pin WebPack's version for docz to work. See this issue:
## https://github.com/pedronauck/docz/issues/704#issuecomment-480295032
##
## react-scripts checks that dependency versions match before running
## Our top level webpack version causes the preflight check to fail
## This prevents that check
##
####
SKIP_PREFLIGHT_CHECK=true
+39
View File
@@ -0,0 +1,39 @@
const merge = require('webpack-merge');
const path = require('path');
const webpackCommon = require('./../../../.webpack/webpack.common.js');
const pkg = require('./../package.json');
const ROOT_DIR = path.join(__dirname, './..');
const SRC_DIR = path.join(__dirname, '../src');
const DIST_DIR = path.join(__dirname, '../dist');
module.exports = (env, argv) => {
const commonConfig = webpackCommon(env, argv, { SRC_DIR, DIST_DIR });
return merge(commonConfig, {
// https://webpack.js.org/configuration/mode/#mode-production
mode: 'production',
devtool: 'source-map',
stats: {
colors: true,
hash: true,
timings: true,
assets: true,
chunks: false,
chunkModules: false,
modules: false,
children: false,
warnings: true,
},
optimization: {
minimize: true,
sideEffects: true,
},
output: {
path: ROOT_DIR,
library: 'ohifUi',
libraryTarget: 'umd',
filename: pkg.main,
},
});
};
+61
View File
@@ -0,0 +1,61 @@
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.7](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.6...@ohif/ui@0.2.18-alpha.7) (2019-08-08)
**Note:** Version bump only for package @ohif/ui
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.6](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.5...@ohif/ui@0.2.18-alpha.6) (2019-08-08)
**Note:** Version bump only for package @ohif/ui
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.5](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.4...@ohif/ui@0.2.18-alpha.5) (2019-08-08)
**Note:** Version bump only for package @ohif/ui
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.4](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.3...@ohif/ui@0.2.18-alpha.4) (2019-08-08)
**Note:** Version bump only for package @ohif/ui
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.3](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.2...@ohif/ui@0.2.18-alpha.3) (2019-08-08)
**Note:** Version bump only for package @ohif/ui
# Change Log
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.18-alpha.2](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.1...@ohif/ui@0.2.18-alpha.2) (2019-08-07)
**Note:** Version bump only for package @ohif/ui
## [0.2.18-alpha.1](https://github.com/OHIF/Viewers/compare/@ohif/ui@0.2.18-alpha.0...@ohif/ui@0.2.18-alpha.1) (2019-08-07)
**Note:** Version bump only for package @ohif/ui
## 0.2.18-alpha.0 (2019-08-05)
**Note:** Version bump only for package @ohif/ui
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Open Health Imaging Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+126
View File
@@ -0,0 +1,126 @@
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<div align="center">
<h1>@ohif/ui</h1>
<p><strong>@ohif/ui</strong> is a collection of components and utilities 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 />
[![NPM version][npm-version-image]][npm-url]
[![NPM downloads][npm-downloads-image]][npm-url]
[![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors)
[![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 @ohif/ui --save-exact
// with yarn
yarn add @ohif/ui --exact
```
## Usage
```jsx
import React, { Component } from "react";
import { LayoutButton } from "@ohif/ui";
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 })}
/>
);
}
}
```
## Developing Locally
_Restore dependencies after cloning:_
```bash
# Restore workspace dependencies (from repository root)
yarn install
# From project root
yarn run dev:ui
# OR from this project's directory
yarn run dev
```
## 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!
## 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
[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
<!-- prettier-ignore-end -->
+12
View File
@@ -0,0 +1,12 @@
.ExampleDropTarget{display:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;min-height:100px;border:2px dashed #000;padding:20px;-webkit-transition:all .2s ease;transition:all .2s ease}.ExampleDropTarger p,.ExampleDropTarget h4{text-align:center;word-break:break-word}.ExampleDropTarget.can-drop{border:2px dashed #20a5d6;background-color:#fafafa;color:#000}.ExampleDropTarget.hovered{border:2px dashed #20a5d6;background-color:#000;color:#91b9cd;cursor:copy}
.animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}@-webkit-keyframes shake{0%,to{-webkit-transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px)}}@keyframes shake{0%,to{-webkit-transform:translateX(0);transform:translateX(0)}10%,30%,50%,70%,90%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}20%,40%,60%,80%{-webkit-transform:translateX(10px);transform:translateX(10px)}}.shake{-webkit-animation-name:shake;animation-name:shake}.modal{display:block}.modal-content>.close{position:absolute;top:10px;right:10px}@media (min-width:768px){.modal-sm>.modal-dialog{width:300px}}@media (min-width:992px){.modal-lg>.modal-dialog{width:900px}}
.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:.5;cursor:not-allowed}.RoundedButtonGroup .roundedButtonWrapper .roundedButton{-webkit-box-align:center;align-items:center;background-color:var(--ui-gray-dark);border:2px solid var(--ui-border-color-dark);color:var(--text-secondary-color);display:-webkit-box;display:flex;font-size:15px;font-weight:500;-webkit-box-pack:center;justify-content:center;height:var(--height);line-height:var(--height);padding:0 22px;position:relative;text-transform:uppercase;-webkit-transition:var(--sidebar-transition);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}
.PresetDateRangePicker_panel{padding:0 22px 11px}.PresetDateRangePicker_button{position:relative;height:100%;text-align:center;background:0 0;border:2px solid #00a699;color:#00a699;padding:4px 12px;margin-right:8px;font:inherit;font-weight:700;line-height:normal;overflow:visible;box-sizing:border-box;cursor:pointer}.PresetDateRangePicker_button:active{outline:0}.PresetDateRangePicker_button__selected{color:#fff;background:#00a699}.SingleDatePickerInput{display:inline-block;background-color:#fff}.SingleDatePickerInput__withBorder{border-radius:2px;border:1px solid #dbdbdb}.SingleDatePickerInput__rtl{direction:rtl}.SingleDatePickerInput__disabled{background-color:#f2f2f2}.SingleDatePickerInput__block{display:block}.SingleDatePickerInput__showClearDate{padding-right:30px}.SingleDatePickerInput_clearDate{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;cursor:pointer;padding:10px;margin:0 10px 0 5px;position:absolute;right:0;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.SingleDatePickerInput_clearDate__default:focus,.SingleDatePickerInput_clearDate__default:hover{background:#dbdbdb;border-radius:50%}.SingleDatePickerInput_clearDate__small{padding:6px}.SingleDatePickerInput_clearDate__hide{visibility:hidden}.SingleDatePickerInput_clearDate_svg{fill:#82888a;height:12px;width:15px;vertical-align:middle}.SingleDatePickerInput_clearDate_svg__small{height:9px}.SingleDatePickerInput_calendarIcon{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;cursor:pointer;display:inline-block;vertical-align:middle;padding:10px;margin:0 5px 0 10px}.SingleDatePickerInput_calendarIcon_svg{fill:#82888a;height:15px;width:14px;vertical-align:middle}.SingleDatePicker{position:relative;display:inline-block}.SingleDatePicker__block{display:block}.SingleDatePicker_picker{z-index:1;background-color:#fff;position:absolute}.SingleDatePicker_picker__rtl{direction:rtl}.SingleDatePicker_picker__directionLeft{left:0}.SingleDatePicker_picker__directionRight{right:0}.SingleDatePicker_picker__portal{background-color:rgba(0,0,0,.3);position:fixed;top:0;left:0;height:100%;width:100%}.SingleDatePicker_picker__fullScreenPortal{background-color:#fff}.SingleDatePicker_closeButton{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;cursor:pointer;position:absolute;top:0;right:0;padding:15px;z-index:2}.SingleDatePicker_closeButton:focus,.SingleDatePicker_closeButton:hover{color:darken(#cacccd,10%);text-decoration:none}.SingleDatePicker_closeButton_svg{height:15px;width:15px;fill:#cacccd}.DayPickerKeyboardShortcuts_buttonReset{background:0 0;border:0;border-radius:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;cursor:pointer;font-size:14px}.DayPickerKeyboardShortcuts_buttonReset:active{outline:0}.DayPickerKeyboardShortcuts_show{width:33px;height:26px;position:absolute;z-index:2}.DayPickerKeyboardShortcuts_show:before{content:"";display:block;position:absolute}.DayPickerKeyboardShortcuts_show__bottomRight{bottom:0;right:0}.DayPickerKeyboardShortcuts_show__bottomRight:before{border-top:26px solid transparent;border-right:33px solid #00a699;bottom:0;right:0}.DayPickerKeyboardShortcuts_show__bottomRight:hover:before{border-right:33px solid #008489}.DayPickerKeyboardShortcuts_show__topRight{top:0;right:0}.DayPickerKeyboardShortcuts_show__topRight:before{border-bottom:26px solid transparent;border-right:33px solid #00a699;top:0;right:0}.DayPickerKeyboardShortcuts_show__topRight:hover:before{border-right:33px solid #008489}.DayPickerKeyboardShortcuts_show__topLeft{top:0;left:0}.DayPickerKeyboardShortcuts_show__topLeft:before{border-bottom:26px solid transparent;border-left:33px solid #00a699;top:0;left:0}.DayPickerKeyboardShortcuts_show__topLeft:hover:before{border-left:33px solid #008489}.DayPickerKeyboardShortcuts_showSpan{color:#fff;position:absolute}.DayPickerKeyboardShortcuts_showSpan__bottomRight{bottom:0;right:5px}.DayPickerKeyboardShortcuts_showSpan__topRight{top:1px;right:5px}.DayPickerKeyboardShortcuts_showSpan__topLeft{top:1px;left:5px}.DayPickerKeyboardShortcuts_panel{overflow:auto;background:#fff;border:1px solid #dbdbdb;border-radius:2px;position:absolute;top:0;bottom:0;right:0;left:0;z-index:2;padding:22px;margin:33px;text-align:left}.DayPickerKeyboardShortcuts_title{font-size:16px;font-weight:700;margin:0}.DayPickerKeyboardShortcuts_list{list-style:none;padding:0;font-size:14px}.DayPickerKeyboardShortcuts_close{position:absolute;right:22px;top:22px;z-index:2}.DayPickerKeyboardShortcuts_close:active{outline:0}.DayPickerKeyboardShortcuts_closeSvg{height:15px;width:15px;fill:#cacccd}.DayPickerKeyboardShortcuts_closeSvg:focus,.DayPickerKeyboardShortcuts_closeSvg:hover{fill:#82888a}.CalendarDay{box-sizing:border-box;cursor:pointer;font-size:14px;text-align:center}.CalendarDay:active{outline:0}.CalendarDay__defaultCursor{cursor:default}.CalendarDay__default{border:1px solid #e4e7e7;color:#484848Line truncated
.select-ohif{display:block;font-family:Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-size:.8em;font-weight:600;color:#444;line-height:1.3;padding:.6em 1.4em .5em .8em;width:100%;max-width:100%;box-sizing:border-box;margin:0;border:1px solid #aaa;box-shadow:0 1px 0 1px rgba(0,0,0,.04);border-radius:.5em;-moz-appearance:none;-webkit-appearance:none;appearance:none;background-color:#fff;background-image:url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"),-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e5e5e5));background-image:url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"),linear-gradient(180deg,#fff 0,#e5e5e5);background-repeat:no-repeat,repeat;background-position:right .7em top 50%,0 0;background-size:.65em auto,100%}.select-ohif::-ms-expand{display:none}.select-ohif:hover{border-color:#888}.select-ohif:focus{border-color:#aaa;box-shadow:0 0 1px 3px rgba(59,153,252,.7);box-shadow:0 0 0 3px -moz-mac-focusring;color:#222;outline:none}.select-ohif option{font-weight:400}
.label-ohif{font-size:1em;color:#fff}.label-example{background-color:var(--ui-gray-darker)}
.range{margin:0;width:100%;-webkit-appearance:none}.range:focus{outline:none}.range::-webkit-slider-runnable-track{width:100%;height:2px;cursor:pointer;box-shadow:none;background-color:var(--ui-border-color-dark);border-radius:0;border:0 solid var(--ui-border-color-dark)}.range::-moz-range-track{width:100%;height:2px;cursor:pointer;box-shadow:none;background-color:var(--ui-border-color-dark);border-radius:0;border:0 solid var(--ui-border-color-dark)}.range::-webkit-slider-thumb{box-shadow:none;border:0 solid var(--active-color);box-shadow:0 10px 10px rgba(0,0,0,.25);height:21px;width:11px;border-radius:11px;background:var(--active-color);cursor:pointer;-webkit-appearance:none;margin-top:-10px}.range::-moz-range-thumb{box-shadow:none;border:0 solid var(--active-color);box-shadow:0 10px 10px rgba(0,0,0,.25);height:21px;width:11px;border-radius:11px;background:var(--active-color);cursor:pointer;-webkit-appearance:none;margin-top:-10px}.range::-moz-focus-outer{border:0}.range-example{background:#000;height:2em}
.textarea-ohif{background-color:#b6b6b6;border-color:#b6b6b6;font-family:Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-size:1em}
.input-ohif{background-color:#b6b6b6;border-color:#b6b6b6;font-family:Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-size:1em}
.dd-menu{float:left;margin-right:10px;cursor:pointer;position:relative;z-index:999}.dd-title{font-size:13px}.dd-caret-down,.dd-menu-toggle{display:inline-block}.dd-caret-down{width:0;height:0;margin-top:.5rem;margin-left:.5rem;border-top:5px solid;border-right:5px solid transparent;border-left:5px solid transparent}.dd-menu-list{position:absolute;top:100%;margin-top:10px;background:#fff;border-radius:5px;-webkit-border-radius:5px;-moz-border-radius:5px;-ms-border-radius:5px;-o-border-radius:5px;overflow:hidden;transition:all .3s ease;-webkit-transition:all .3s ease;-moz-transition:all .3s ease;-ms-transition:all .3s ease;-o-transition:all .3s ease}.dd-menu-list.open{display:inline-block}.dd-menu-list.left{left:0}.dd-menu-list.right{right:0}.dd-menu-list.center{left:50%;transform:translateX(-50%);-webkit-transform:translateX(-50%);-moz-transform:translateX(-50%);-ms-transform:translateX(-50%);-o-transform:translateX(-50%)}.dd-item{display:-webkit-box;display:flex;color:var(--text-color-active);padding:10px 15px;border-bottom:1px solid #ccc}.dd-item:hover{text-decoration:none;background:#eee}.dd-item:last-child{border-bottom:none}.dd-item-icon{margin-right:10px;margin-top:2px}
.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:-webkit-box;display:flex;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;height:100%;color:var(--text-secondary-color)}.LayoutPanel{width:100%;height:100%}
.LayoutPanelDropTarget{width:100%;height:100%;opacity:1;-webkit-transition:all .3s ease;transition:all .3s ease}.LayoutPanelDropTarget.hovered{opacity:.5;cursor:copy}
+1
View File
@@ -0,0 +1 @@
module.exports = require("../../babel.config.js");
+75
View File
@@ -0,0 +1,75 @@
/**
* 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,
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");
},
modifyBabelRc: babelrc => {
const newBabelRc = {
...babelrc,
plugins: [
...babelrc.plugins,
require.resolve("babel-plugin-inline-react-svg")
]
};
return newBabelRc;
}
};
+10
View File
@@ -0,0 +1,10 @@
const base = require("../../jest.config.base.js");
const pkg = require("./package");
const esModules = ["react-dnd"];
module.exports = {
...base,
name: pkg.name,
displayName: pkg.name
};
+65
View File
@@ -0,0 +1,65 @@
{
"name": "@ohif/ui",
"version": "0.2.18-alpha.7",
"description": "A set of React components for Medical Imaging Viewers",
"author": "OHIF Contributors",
"license": "MIT",
"repository": "OHIF/Viewers",
"main": "dist/index.umd.js",
"module": "src/index.js",
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=8",
"npm": ">=5"
},
"files": [
"dist",
"README.md"
],
"scripts": {
"dev": "docz dev",
"dev:ui": "yarn run dev",
"docs": "docz build",
"build": "webpack --config .webpack/webpack.prod.js",
"build:package": "yarn run build",
"prepublishOnly": "yarn run build",
"test:unit": "jest --watchAll",
"test:unit:ci": "jest --ci --runInBand --collectCoverage --passWithNoTests"
},
"peerDependencies": {
"@ohif/i18n": "^0.2.3",
"i18next": "^17.0.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-i18next": "^10.11.0"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@ohif/i18n": "^0.2.3",
"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": {
"@mdx-js/tag": "^0.20.3",
"autoprefixer": "^9.6.0",
"docz": "1.2.0",
"docz-core": "1.2.0",
"docz-plugin-css": "0.11.0",
"docz-theme-default": "1.2.0",
"lodash.clonedeep": "^4.5.0",
"react-powerplug": "1.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
module.exports = require("../../postcss.config.js");
+44
View File
@@ -0,0 +1,44 @@
/* Tip & Success "Call outs" */
p.tip {
border-left-color: #f66;
}
p.success {
border-left-color: #42b983;
}
p.tip::before {
content: '!';
background-color: #f66;
}
p.success::before {
content: '\f00c';
font-family: FontAwesome;
background-color: #42b983;
}
p.tip,
p.success {
padding: 12px 24px 12px 30px;
margin: 2em 0;
line-height: 1.6em;
border-left-width: 4px;
border-left-style: solid;
background-color: #f8f8f8;
position: relative;
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
}
p.tip::before,
p.success::before {
position: absolute;
top: 14px;
left: -12px;
color: #fff;
width: 20px;
height: 20px;
border-radius: 100%;
text-align: center;
line-height: 20px;
font-weight: bold;
font-family: 'Dosis', 'Source Sans Pro', 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
}
@@ -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%;
}
@@ -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
);
@@ -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,
});
}
};
}
@@ -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
View File
@@ -0,0 +1,17 @@
import React, { Fragment } from 'react';
import { Link } from 'docz';
import PropTypes from 'prop-types';
const NameSpace = ({ name }) => (
<Fragment>
The namespace used on this component was: <pre>{name}</pre> Check the{' '}
<Link to="/translating#how-to-translate-components">translation docs</Link>{' '}
to see how to override it.
</Fragment>
);
NameSpace.propTypes = {
name: PropTypes.string.isRequired,
};
export default NameSpace;
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -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.
Whitespace-only changes.
@@ -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 />
@@ -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
View File
@@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="UTF-8">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }}</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="/public/docz.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700|Sanchez&display=swap">
<style>
/* "Tide" theme */
/* TODO:
- Convert the other themes to CSS Variables
- Allow user to set default CSS theme in JS config
- Export the themes as identical JS objects
- For theme switcher, update root-level CSS properties via JS (https://medium.com/@_bengarrison/accessing-and-modifying-css-variables-with-javascript-2ccb735bbff0)
*/
:root {
/* Interface UI Colors */
--default-color: #9CCEF9;
--hover-color: #ffffff;
--active-color: #20A5D6;
--ui-border-color: #44626F;
--ui-border-color-dark: #3C5D80;
--ui-border-color-active: #00a4d9;
--primary-background-color: #000000;
--box-background-color: #3E5975;
--text-primary-color: #ffffff;
--text-secondary-color: #91B9CD;
--input-background-color: #2c363f;
--input-placeholder-color: #d3d3d3;
--table-hover-color: #2c363f;
--table-text-primary-color: #ffffff;
--table-text-secondary-color: #91b9cd;
--large-numbers-color:#6FBDE2;
--state-error: #FFCCCC;
--state-error-border: #FFCCCC;
--state-error-text: #FFCCCC;
/* Common palette */
--ui-yellow: #E29E4A;
--ui-sky-blue: #6FBDE2;
/* State palette */
--ui-state-error: #FFCCCC;
--ui-state-error-border: #993333;
--ui-state-error-text: #661111;
--ui-gray-lighter: #436270;
--ui-gray-light: #516873;
--ui-gray: #263340;
--ui-gray-dark: #16202B;
--ui-gray-darker: #151A1F;
--ui-gray-darkest: #14202A;
--calendar-day-color: #d3d3d3;
--calendar-day-border-color: #d3d3d3;
--calendar-day-active-hover-background-color: #516873;
--calendar-main-color: #263340;
--viewport-border-thickness: 1px;
}
body {
font-family: 'Roboto', sans-serif !important;
}
</style>
{{ head }}
</head>
<body>
<div id="root" />
{{ footer }}
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
---
name: Introduction
route: /
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { TableList, TableListItem } from './../index.js'
# Introduction
**React Viewerbase is a collection of components and utilities** that power
OHIF's [zero-footprint DICOM viewer](https://github.com/OHIF/Viewers)
([demo][demo-url]). We maintain them as a separate component library to:
- Decouple presentation from business logic
- Test and develop components in isolation
- Provide well documented, reusable components
- Aid rapid application development for context specific viewers
## Quick Example
> This component library is pre- v1.0. All realeases until a v1.0 have the
> possibility of introducing breaking changes. Please depend on an "exact"
> version in your projects to prevent issues caused by loose versioning.
```js
// Add 'react-viewerbase' as a dependency
yarn add react-viewerbase
// Import and use components
import { TableList } from 'react-viewerbase'
```
<Playground>
<State
initial={{
selectedIndex: null,
listItems: [
{ label: 'Brain Left' },
{ label: 'Brain Right' },
{ label: 'Heart Left' },
{ label: 'Heart Right' },
],
}}
>
{({ state, setState }) => (
<TableList headerTitle="Example Table List Header">
{state.listItems.map((item, index) => {
return (
<TableListItem
key={`item_${index}`}
itemClass={state.selectedIndex === index ? 'selected' : ''}
itemIndex={index}
onItemClick={() => alert('item clicked')}
>
<label>{item.label}</label>
</TableListItem>
)
})}
</TableList>
)}
</State>
</Playground>
## FAQ
...
<!--
Links
-->
<!-- prettier-ignore-start -->
[demo-url]: https://docs.ohif.org/demo/
<!-- prettier-ignore-end -->
@@ -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
View File
@@ -0,0 +1,87 @@
---
name: Translating
route: /translating
---
# Translating
**React Viewerbase** supports internationalization using
[i18next](https://www.i18next.com/) through the npm package
[@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n), where is the main
instance of i18n containing several languages and tools.
## How to translate components
All you need is to use the internal custom superset of
[withTranslation HOC](https://react.i18next.com/latest/withtranslation-hoc),
available at `src/utils/LanguageProvider.js`
e.g.:
```js
import React from 'react';
import { withTranslation } from '../utils/LanguageProvider.js';
function MyComponent({ t }) {
return <p>{t('my translated text')}</p>;
}
export default withTranslation('MyNameSpace')(MyComponent);
```
---
## Namespaces
For further information about namespaces and how they work, please visit
[OHIF Namespaces docs](https://docs.ohif.org/essentials/translating.html#namespaces).
### How to get right the Name Space of a react-viewerbase component?
Check the component's page in this site, the NameSpace information should be on
the bottom line.
Also, a nameSpace is defined when exporting a component, this information can be
found on exports trough the code, in case of missing in docs.
E.g.
```
export default withTranslation('TheNamespaceYouAreLookingFor')(TheComponentYouWantToUse);
```
### How to use another NameSpace inside the current NameSpace?
i18next provides a parsing feature able to get translations strings from any
NameSpace, like this following example getting data from `InnerSpace` NameSpace:
```
$t(InnerSpace:Reset)
export default withTranslation('OuterSpace')(TheComponent);
```
---
## I18next Provider
**React Viewerbase** pass down the `t` function trough the custom
[withTranslation HOC](https://github.com/OHIF/react-viewerbase/blob/master/src/utils/LanguageProvider.js).
---
## Extending and Overriding languages
[How to Extend languages with @ohif/i18n](https://docs.ohif.org/essentials/translating.html#extending-languages-in-ohifi18n)
## Changing the language
The language choosing method is the same implemented in
[OHIF Viewers](https://docs.ohif.org/essentials/translating.html#changing-the-language)
> You can also change the language of the components displayed in this
> documentation by choosing an option in the
> [Docs Settings](https://react.ohif.org/docs-settings) section.
## Debugging translations
[How to Debug @ohif/i18n](https://docs.ohif.org/essentials/translating.html#debugging-translations)
+22
View File
@@ -0,0 +1,22 @@
import React from 'react';
import PropTypes from 'prop-types';
import LanguageSwitcher from '../components/languageSwitcher';
import './wrapper.styl';
const Wrapper = ({ children }) => (
<React.Fragment>
<div className="sidebarLanguageSwitcher">
<strong>Display components in:</strong>
<LanguageSwitcher />
</div>
{children}
</React.Fragment>
);
Wrapper.propTypes = {
children: PropTypes.node.isRequired,
};
export default Wrapper;
+15
View File
@@ -0,0 +1,15 @@
[class*="Sidebar__Menus"]
@media (min-width: 1120px)
margin-top: 80px
.sidebarLanguageSwitcher
position: fixed
top: 155px
left 0
z-index: 1049
padding: 0 24px 20px
border-bottom: 1px dotted #CED4DE
background: #F5F6F7
transition: all 200ms ease
@media (max-width: 1119px)
left -100%
@@ -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" />
@@ -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;
}
@@ -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>
);
}
}
@@ -0,0 +1 @@
export { Checkbox } from './checkbox.js';
@@ -0,0 +1,202 @@
import './CineDialog.styl';
import React, { PureComponent } from 'react';
import { withTranslation } from '../../utils/LanguageProvider';
import { Icon } from './../../elements/Icon';
import PropTypes from 'prop-types';
class CineDialog extends PureComponent {
constructor(props) {
super(props);
this.state = {
cineFrameRate: props.cineFrameRate,
isPlaying: props.isPlaying,
};
}
static propTypes = {
/** Minimum value for range slider */
cineMinFrameRate: PropTypes.number.isRequired,
/** Maximum value for range slider */
cineMaxFrameRate: PropTypes.number.isRequired,
/** Increment range slider can "step" in either direction. */
cineStepFrameRate: PropTypes.number.isRequired,
cineFrameRate: PropTypes.number.isRequired,
/** 'True' if playing, 'False' if paused. */
isPlaying: PropTypes.bool.isRequired,
onPlayPauseChanged: PropTypes.func,
onFrameRateChanged: PropTypes.func,
onClickNextButton: PropTypes.func,
onClickBackButton: PropTypes.func,
onClickSkipToStart: PropTypes.func,
onClickSkipToEnd: PropTypes.func,
/** i18next translation function */
t: PropTypes.func.isRequired,
};
static defaultProps = {
cineMinFrameRate: 1,
cineMaxFrameRate: 90,
cineStepFrameRate: 1,
cineFrameRate: 24,
isPlaying: false,
};
componentDidUpdate(prevProps) {
// TODO: Not sure if we should just switch this to a stateless
// fully-controlled component instead
if (
this.props.isPlaying !== prevProps.isPlaying ||
this.props.isPlaying !== this.state.isPlaying
) {
this.setState({
isPlaying: this.props.isPlaying,
});
}
if (
this.props.cineFrameRate !== prevProps.cineFrameRate ||
this.props.cineFrameRate !== this.state.cineFrameRate
) {
this.setState({
cineFrameRate: this.props.cineFrameRate,
});
}
}
handleInputChange = event => {
const target = event.target;
let value = target.value;
if (target.type === 'range') {
value = parseFloat(target.value);
}
const name = target.name;
this.setState({
[name]: value,
});
if (name === 'cineFrameRate' && this.props.onFrameRateChanged) {
this.props.onFrameRateChanged(parseFloat(value));
}
};
onClickPlayPause = () => {
const value = !this.state.isPlaying;
this.setState({
isPlaying: value,
});
if (this.props.onPlayPauseChanged) {
this.props.onPlayPauseChanged(value);
}
};
onClickNextButton = event => {
if (this.props.onClickNextButton) {
this.props.onClickNextButton(event);
}
};
onClickBackButton = event => {
if (this.props.onClickBackButton) {
this.props.onClickBackButton(event);
}
};
onClickSkipToStart = event => {
if (this.props.onClickSkipToStart) {
this.props.onClickSkipToStart(event);
}
};
onClickSkipToEnd = event => {
if (this.props.onClickSkipToEnd) {
this.props.onClickSkipToEnd(event);
}
};
// TODO:
// - Add next / previous display set buttons which just call
// onClickNextDisplaySet and onClickPreviousDisplaySet which are passed in as props.
// See https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.html#L38
// - Add 'isEnabled' prop: https://github.com/OHIF/Viewers/blob/master/Packages/ohif-viewerbase/client/components/viewer/cineDialog/cineDialog.js#L301
render() {
const { t } = this.props;
return (
<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;
@@ -0,0 +1,172 @@
@import './../../design/styles/common/global.styl'
@import './../../design/styles/common/form.styl'
.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" />
@@ -0,0 +1,2 @@
import CineDialog from './CineDialog';
export { CineDialog };
+48
View File
@@ -0,0 +1,48 @@
import {
ExampleDropTarget,
StudyBrowser,
ThumbnailEntry,
} from './studyBrowser';
import { LayoutButton, LayoutChooser } from './layoutButton';
import { MeasurementTable, MeasurementTableItem } from './measurementTable';
import { Overlay, OverlayTrigger } from './overlayTrigger';
import { TableList, TableListItem } from './tableList';
import {
AboutModal,
UserPreferences,
UserPreferencesModal,
} from './userPreferencesModal';
import { CineDialog } from './cineDialog';
import { QuickSwitch } from './quickSwitch';
import { RoundedButtonGroup } from './roundedButtonGroup';
import { SelectTree } from './selectTree';
import { SimpleDialog } from './simpleDialog';
import { StudyList } from './studyList';
import { ToolbarSection } from './toolbarSection';
import { Tooltip } from './tooltip';
export {
CineDialog,
ExampleDropTarget,
LayoutButton,
LayoutChooser,
MeasurementTable,
MeasurementTableItem,
Overlay,
OverlayTrigger,
QuickSwitch,
RoundedButtonGroup,
SelectTree,
SimpleDialog,
StudyBrowser,
StudyList,
TableList,
TableListItem,
ThumbnailEntry,
ToolbarSection,
Tooltip,
AboutModal,
UserPreferences,
UserPreferencesModal,
};
@@ -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,129 @@
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 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,47 @@
@import './../../design/styles/common/button.styl'
@import './../../design/styles/common/global.styl'
$borderColor = rgba(77, 99, 110, 0.81)
.layoutChooser-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">{this.props.t(timepoint.key)}</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', 'Common'])(
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,165 @@
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, {
keySeparator: '>',
nsSeparator: '|',
})}
</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';
@@ -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;
@@ -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';
@@ -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,
});
};
}
@@ -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)}
/>
);
});
};
}
Whitespace-only changes.
@@ -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 {};
};
}
@@ -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)}
/>
);
});
};
}
Whitespace-only changes.
@@ -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,
},
],
},
];
@@ -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,
label: 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.label && <span>{option.label}</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';
@@ -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);
};
}
@@ -0,0 +1,192 @@
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;
};
filterItems() {
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);
}
});
} else {
const label = item.label.toLowerCase();
const searchTerm = this.state.searchTerm.toLowerCase();
if (label.indexOf(searchTerm) !== -1) {
filteredItems.push(item);
}
}
});
return filteredItems;
}
getTreeItems() {
const storageKey = 'SelectTree';
let treeItems;
if (this.state.searchTerm) {
treeItems = this.filterItems();
} 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,
});
} else {
this.setState({
currentNode: item,
});
}
return this.props.onSelected(event, item);
};
onBreadcrumbSelected = () => {
this.setState({
currentNode: null,
});
};
}
@@ -0,0 +1,108 @@
@import './../../design/styles/common/global.styl'
.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>
);
}
}
@@ -0,0 +1,42 @@
---
name: Select Tree
menu: Components
route: /components/select-tree
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { SelectTree } from './../index.js'
import selectTreeItems from './selectTreeItems.js'
# SelectTree
## Basic usage
<Playground>
<State initial={{
items: selectTreeItems,
}}>
{({ state, setState }) => (
<React.Fragment>
<pre style={{ maxHeight: '135px', overflowX: 'auto' }}>
{JSON.stringify(state.items, null, 2)}
</pre>
<SelectTree
items={state.items}
onSelected={(event, item) => setState({ itemLabelSelected: item.label }) }
selectTreeFirstTitle="Select Tree Example Header"
autoFocus={false}
/>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={SelectTree} />
@@ -0,0 +1,30 @@
export default [
{
label: 'A',
value: 'A',
items: [
{
label: 'Abdominal Wall',
value: 'AbdominalWall',
},
{
label: 'Adrenal Left',
value: 'AdrenalLeft',
},
],
},
{
label: 'B',
value: 'B',
items: [
{
label: 'Brain',
value: 'Brain',
},
{
label: 'Breast',
value: 'Breast',
},
],
},
];
@@ -0,0 +1 @@
export { SelectTree } from './SelectTree';
@@ -0,0 +1,69 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './SimpleDialog.styl';
class SimpleDialog extends Component {
static propTypes = {
children: PropTypes.node,
componentRef: PropTypes.any,
componentStyle: PropTypes.object,
rootClass: PropTypes.string,
isOpen: PropTypes.bool,
headerTitle: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onConfirm: PropTypes.func.isRequired,
};
static defaultProps = {
isOpen: true,
componentStyle: {},
rootClass: '',
};
render() {
return (
<React.Fragment>
{this.props.isOpen && (
<div
className={`simpleDialog ${this.props.rootClass} `}
ref={this.props.componentRef}
style={this.props.componentStyle}
>
<form>
<div className="header">
<span className="closeBtn" onClick={this.onClose}>
<span className="closeIcon">x</span>
</span>
<h4 className="title">{this.props.headerTitle}</h4>
</div>
<div className="content">{this.props.children}</div>
<div className="footer">
<button className="btn btn-default" onClick={this.onClose}>
Cancel
</button>
<button className="btn btn-primary" onClick={this.onConfirm}>
Confirm
</button>
</div>
</form>
</div>
)}
</React.Fragment>
);
}
onClose = event => {
event.preventDefault();
event.stopPropagation();
this.props.onClose();
};
onConfirm = event => {
event.preventDefault();
event.stopPropagation();
this.props.onConfirm();
};
}
export { SimpleDialog };
@@ -0,0 +1,108 @@
@import './../../design/styles/common/button.styl'
@import './../../design/styles/common/global.styl'
.simpleDialog
position: fixed;
top: 0px;
left: 0px;
z-index: 1000;
border: 0;
border-radius: 6px;
background-color: var(--ui-gray-darker);
.header
border-bottom: 3px solid var(--primary-background-color);
padding: 19px 22px 17px;
position: relative;
.title
font-size: 20px;
font-weight: 600;
line-height: 24px;
padding-right: 40px;
color: var(--text-secondary-color);
margin: 0;
.closeBtn
-webkit-transform: translateY(-50%);
color: var(--text-secondary-color);
height: 20px;
opacity: 1;
overflow: hidden;
padding: 2px;
position: absolute;
right: 21px;
text-align: center;
text-shadow: none;
top: 50%;
transform: translateY(-50%);
transition: color 0.3s ease;
width: 20px;
.closeIcon
color: transparent;
display: block;
font-size: 0;
height: 100%;
line-height: 0;
overflow: hidden;
position: relative;
width: 100%;
&:after, &:before
content: ' ';
display: block;
height: 2px;
transition: background-color 0.3s ease;
width: 19px;
background-color: var(--text-secondary-color);
&:before
left: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
-webkit-transform-origin: 1px 50%;
transform-origin: 1px 50%;
&:after
right: 1px;
position: absolute;
top: 1px;
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
-webkit-transform-origin: calc(100% - 1px) 50%;
transform-origin: calc(100% - 1px) 50%;
.content
padding: 16px 22px 25px;
position: relative;
color: var(--text-primary-color);
.simpleDialogSelect, .simpleDialogInput
background-color: var(--input-background-color);
color: var(--text-primary-color);
border: 0;
border-radius: 2px;
font-size: 14px;
height: 30px;
width: 100%;
line-height: 16px;
padding: 8px 9px 6px;
margin-top: 10px;
display: block;
.simpleDialogLabelFor
font-size: 14px;
font-weight: 700;
line-height: 16px;
margin: 10px 0 0 0;
color: var(--text-primary-color);
.footer
padding: 15px;
text-align: right;
.btn
margin-left: 5px;
@@ -0,0 +1,83 @@
---
name: Simple Dialog
menu: Components
route: /components/simple-dialog
---
import { Playground, Props } from 'docz'
import { State } from 'react-powerplug'
import { SimpleDialog } from './../index.js'
# Simple Dialog
Its similar to a modal but we can pass its style as props and manipulate it as
we want. Initially used to display tool data changes in our Viewport.
## Basic usage
<Playground>
<State initial={{
isDialogOpen: false,
inputValue: '',
selectValue: '',
dialogStyle: {
top: '50px',
left: '100px',
position: 'absolute'
}
}}>
{({ state, setState }) => (
<React.Fragment>
<div><pre>{JSON.stringify(state, null, 2) }</pre></div>
{/* Toggle Dialog */}
<button onClick={() => setState({ isDialogOpen: !state.isDialogOpen})}>
{state.isDialogOpen ? 'Close Dialog' : 'Open Dialog'}
</button>
<SimpleDialog
isOpen={state.isDialogOpen}
headerTitle='Example Dialog Header'
onClose={() => setState({ isDialogOpen: false })}
onConfirm={() => setState({
isDialogOpen: false,
// If you don't want to update external state until complete,
// This is where you could grab current field values and update parent
// With setState
//
// inputValue: this.input.value,
// selectValue: this.select.value
})}
componentStyle={state.dialogStyle}
onValueChanged={(value) => setState({value}) }
>
<label htmlFor='input' className='simpleDialogLabelFor'>Input Example</label>
<input
id='input'
type='text'
className='simpleDialogInput'
autoComplete='off'
onChange={(e) => setState({inputValue: e.target.value})}
/>
<label htmlFor='select' className='simpleDialogLabelFor'>SelectExample</label>
<select
name='select'
id='select'
className='simpleDialogSelect'
onChange={(e) => setState({selectValue: e.target.value})}
>
<option value='Option1'>Option1</option>
<option value='Option2'>Option2</option>
<option value='Option3'>Option3</option>
<option value='Option4'>Option4</option>
</select>
</SimpleDialog>
</React.Fragment>
)}
</State>
</Playground>
## API
<Props of={SimpleDialog} />
@@ -0,0 +1 @@
export { SimpleDialog } from './SimpleDialog.js';
@@ -0,0 +1,85 @@
import React, { PureComponent, memo } from 'react';
import { DragLayer } from 'react-dnd';
import PropTypes from 'prop-types';
import { ThumbnailEntry } from './ThumbnailEntry';
import './DragPreview.styl';
let subscribedToOffsetChange = false;
let dragPreviewRef = null;
const collector = monitor => {
if (!subscribedToOffsetChange) {
monitor.subscribeToOffsetChange(onOffsetChange(monitor));
subscribedToOffsetChange = true;
}
if (dragPreviewRef) {
const offset =
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
if (offset) {
const transform = `translate(${offset.x}px, ${offset.y}px)`;
dragPreviewRef.style['transform'] = transform;
dragPreviewRef.style['-webkit-transform'] = transform;
}
}
const item = monitor.getItem();
let newItem = {};
if (item) {
newItem = {
active: item.active,
altImageText: item.altImageText,
id: item.id,
imageSrc: item.imageSrc,
imageId: item.imageId,
instanceNumber: item.instanceNumber,
error: item.error,
numImageFrames: item.numImageFrames,
seriesDescription: item.seriesDescription,
seriesNumber: item.seriesNumber,
stackPercentComplete: item.stackPercentComplete,
};
}
return {
...newItem,
isDragging: monitor.isDragging(),
};
};
const onOffsetChange = monitor => () => {
if (!dragPreviewRef) return;
const offset =
monitor.getSourceClientOffset() || monitor.getInitialSourceClientOffset();
if (!offset) return;
const transform = `translate(${offset.x}px, ${offset.y}px)`;
dragPreviewRef.style['transform'] = transform;
dragPreviewRef.style['-webkit-transform'] = transform;
};
const updateRef = ref => {
dragPreviewRef = ref;
};
class DragPreview extends PureComponent {
render() {
const { isDragging } = this.props;
if (!isDragging) return null;
return (
<div className="DragPreview">
<div className="source-preview" ref={updateRef}>
<ThumbnailEntry {...this.props} />
</div>
</div>
);
}
}
DragPreview.propTypes = {
isDragging: PropTypes.bool,
};
export default DragLayer(collector)(memo(DragPreview));
@@ -0,0 +1,14 @@
.DragPreview
.source-preview {
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
opacity: 0.5;
z-index: 9999;
left: 0;
top: 0;
transition: none;
pointer-events: none;
-webkit-touch-callout: none;
}
@@ -0,0 +1,30 @@
.ExampleDropTarget {
display: flex;
justify-content: center;
flex-direction: column;
min-height: 100px;
border: 2px dashed black;
padding: 20px;
transition: 0.2s all ease;
}
.ExampleDropTarget h4,
.ExampleDropTarger p {
text-align: center;
word-break: break-word;
}
.ExampleDropTarget.can-drop {
border: 2px dashed #20a5d6;
background-color: #fafafa;
color: black;
}
.ExampleDropTarget.hovered {
border: 2px dashed #20a5d6;
background-color: black;
color: #91b9cd;
cursor: copy;
}
Loaded 100 of 919 files, more files were not shown because too many files have changed in this diff. Show more