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:
1 parent
cac911f08e
commit
08ff214ea6
919 files changed
+45652
-52097
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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]
|
||||
[](#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 -->
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../../babel.config.js");
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = require("../../postcss.config.js");
|
||||
@@ -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)
|
||||
@@ -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 }`
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
@@ -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
Reference in new issue
Block a user