reorganize packages, start converting overall app to react (very broken at the moment)
This commit is contained in:
1 parent
024fe1a448
commit
5f30f7bd35
479 files changed
+5364
-11611
No files matched your search
@@ -1,4 +1,4 @@
|
||||
@import "{ohif:design}/app"
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
#aboutModal
|
||||
.logo
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import './item.html';
|
||||
import './item.js';
|
||||
import './list.html';
|
||||
import './list.js';
|
||||
import './list.styl';
|
||||
import './quickSwitch.html';
|
||||
import './quickSwitch.js';
|
||||
import './series.html';
|
||||
import './series.js';
|
||||
import './sidebar.html';
|
||||
import './sidebar.js';
|
||||
@@ -0,0 +1,29 @@
|
||||
<template name="studyBrowserItem">
|
||||
<div class="study-browser-item {{this.studyItemClass}} {{#if isActive}}active{{/if}} {{#if isLoading}}loading{{/if}}" data-uid="{{this.studyInformation.studyInstanceUid}}">
|
||||
<div class="study-item">
|
||||
{{#if isLoading}}
|
||||
{{>loadingText}}
|
||||
{{/if}}
|
||||
{{#let studyData=instance.studyData.get}}
|
||||
<div class="study-item-box">
|
||||
<div class="study-modality">
|
||||
<div class="study-modality-text" style="{{modalityStyle studyData.modalities}}">{{studyData.modalities}}</div>
|
||||
</div>
|
||||
<div class="study-text">
|
||||
{{#if (or isLoaded hasDescriptionAndDate)}}
|
||||
<div class="study-date">{{formatDA studyData.studyDate 'D-MMM-YYYY'}}</div>
|
||||
<div class="study-description">{{studyData.studyDescription}}</div>
|
||||
{{else}}
|
||||
<div class="study-availability">{{#if eq studyAvailable false}}N/A{{else}}Click to load{{/if}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
</div>
|
||||
{{#if this.studyChildTemplate}}
|
||||
{{#if and (not isLoading) instance.studyData.get instance.loaded}}
|
||||
{{>Template.dynamic template=this.studyChildTemplate data=(clone this)}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import $ from 'jquery';
|
||||
import _ from 'underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.studyBrowserItem.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { studyInformation } = instance.data;
|
||||
const { studyInstanceUid } = studyInformation;
|
||||
|
||||
instance.loaded = new ReactiveVar(false);
|
||||
instance.loading = new ReactiveVar(false);
|
||||
|
||||
instance.studyData = new ReactiveVar(studyInformation);
|
||||
|
||||
// Try to load the study data from an external source if available
|
||||
if (OHIF.studies.getStudyBoxData) {
|
||||
OHIF.studies.getStudyBoxData(studyInformation).then(studyData => {
|
||||
if (!instance.loaded.get()) {
|
||||
instance.studyData.set(studyData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
instance.studyMetadata = null;
|
||||
instance.getStudyMetadata = () => {
|
||||
instance.loading.dep.depend();
|
||||
|
||||
if (!instance.studyMetadata) {
|
||||
let study = OHIF.viewer.Studies.findBy({ studyInstanceUid }) || null;
|
||||
if (study && !(study instanceof OHIF.viewerbase.metadata.StudyMetadata)) {
|
||||
study = new OHIF.metadata.StudyMetadata(study, study.studyInstanceUid);
|
||||
}
|
||||
|
||||
instance.studyMetadata = study;
|
||||
}
|
||||
|
||||
return instance.studyMetadata;
|
||||
};
|
||||
|
||||
instance.autorun(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
OHIF.studies.loadingDict.get(studyInstanceUid);
|
||||
const studyMetadata = instance.getStudyMetadata();
|
||||
if (studyMetadata) {
|
||||
const firstInstance = studyMetadata.getFirstInstance();
|
||||
instance.studyData.set({
|
||||
studyDate: firstInstance.getRawValue('x00080020') || '',
|
||||
studyDescription: firstInstance.getRawValue('x00081030') || '',
|
||||
modalities: firstInstance.getRawValue('x00080060') || '',
|
||||
});
|
||||
|
||||
instance.loaded.set(true);
|
||||
instance.loading.set(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.studyBrowserItem.events({
|
||||
'click .study-item'(event, instance) {
|
||||
if (instance.loading.get()) return;
|
||||
|
||||
const { studyInformation } = instance.data;
|
||||
|
||||
// Skip if study is not available (set to false explicitly)
|
||||
if (studyInformation.available === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = event.currentTarget.parentElement;
|
||||
const $element = $(element);
|
||||
const triggerClick = () => {
|
||||
const cloneEvent = _.clone(event);
|
||||
delete cloneEvent.type;
|
||||
cloneEvent.currentTarget = cloneEvent.target = element;
|
||||
const newEvent = $.Event('ohif.studies.study.click', cloneEvent);
|
||||
$element.trigger(newEvent, studyInformation);
|
||||
};
|
||||
|
||||
if (instance.loaded.get()) {
|
||||
triggerClick();
|
||||
} else {
|
||||
instance.loading.set(true);
|
||||
OHIF.studies.loadStudy(studyInformation.studyInstanceUid).then(() => {
|
||||
instance.loaded.set(true);
|
||||
instance.loading.set(false);
|
||||
$element.trigger('ohif.studies.study.load', studyInformation);
|
||||
Tracker.afterFlush(triggerClick);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyBrowserItem.helpers({
|
||||
isLoaded() {
|
||||
return Template.instance().loaded.get();
|
||||
},
|
||||
|
||||
hasDescriptionAndDate() {
|
||||
const studyData = Template.instance().studyData.get();
|
||||
return studyData.studyDescription && studyData.studyDate;
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
return Template.instance().loading.get();
|
||||
},
|
||||
|
||||
modalitiesText(modalities) {
|
||||
const text = modalities || 'UN';
|
||||
return _.uniq(text.split(/[^A-Za-z]+/g)).join(' ');
|
||||
},
|
||||
|
||||
modalityStyle(modalities) {
|
||||
// Responsively styles the Modality Acronyms for studies with more than one modality
|
||||
const numModalities = modalities ? modalities.split(/\s/g).length : 1;
|
||||
|
||||
if (numModalities === 1) {
|
||||
// If we have only one modality, it should take up the whole div
|
||||
return 'font-size:1em';
|
||||
} else if (numModalities === 2) {
|
||||
// If we have two modalities, let them sit side-by-side
|
||||
return 'font-size:0.7em';
|
||||
} else {
|
||||
// If we have more than two modalities, change the line height to display multiple rows
|
||||
return 'font-size:0.7em;line-height:1.4em;padding-top:0.5em';
|
||||
}
|
||||
},
|
||||
|
||||
studyAvailable() {
|
||||
const { studyInformation } = Template.instance().data;
|
||||
if (!studyInformation) {
|
||||
return;
|
||||
}
|
||||
|
||||
return studyInformation.available;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template name="studyBrowserList">
|
||||
<div class="study-browser-list {{this.class}}">
|
||||
{{#each studyInformation in getStudiesInformation}}
|
||||
{{>studyBrowserItem (clone this studyInformation=studyInformation)}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.studyBrowserList.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
if (instance.data.studiesData) {
|
||||
instance.studiesData = instance.data.studiesData;
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyBrowserList.helpers({
|
||||
getStudiesInformation() {
|
||||
const instance = Template.instance();
|
||||
|
||||
let studiesInformation;
|
||||
if (instance.studiesData) {
|
||||
studiesInformation = instance.studiesData.get() || [];
|
||||
} else {
|
||||
studiesInformation = instance.data.studiesInformation || [];
|
||||
}
|
||||
|
||||
const result = new Map();
|
||||
studiesInformation.forEach(item => result.set(item.studyInstanceUid, item));
|
||||
return Array.from(result.values());
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
$boxBorderColor = transparent
|
||||
$boxHoverBackgroundColor = #14191E
|
||||
$boxHoverBorderColor = #2d4660
|
||||
$boxActiveBorderColor = #131D29
|
||||
$boxWidth = 50px
|
||||
$borderThickness = 2px
|
||||
$boxBorderRadius = 8px
|
||||
$nestingMargin = 6px
|
||||
$spacerX = 7px
|
||||
$spacerY = 12px
|
||||
|
||||
.study-browser-item
|
||||
position: relative
|
||||
// required transformation to make inner fixed elements relative to this one
|
||||
transform(scale(1))
|
||||
|
||||
.loadingTextDiv
|
||||
theme('color', '$textSecondaryColor')
|
||||
background-color: $boxHoverBackgroundColor
|
||||
border-radius: 12px
|
||||
font-size: 32px
|
||||
opacity: 0
|
||||
margin: 0
|
||||
padding: 25px
|
||||
position: absolute
|
||||
pointer-events: none
|
||||
z-index: 100
|
||||
|
||||
&.loading
|
||||
.loadingTextDiv
|
||||
height: 100%
|
||||
left: 0
|
||||
opacity: 0.75
|
||||
top: 0
|
||||
width: 100%
|
||||
|
||||
.study-item-box
|
||||
opacity: 0.2
|
||||
|
||||
&.active
|
||||
.study-item-box
|
||||
.study-modality
|
||||
theme('color', '$primaryBackgroundColor')
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
theme('background-color', '$activeColor')
|
||||
border-color: $boxActiveBorderColor
|
||||
|
||||
&:not(.active) .study-browser-series
|
||||
max-height: 0 !important
|
||||
|
||||
.study-series-container
|
||||
opacity: 0
|
||||
transform(translateY(-100%))
|
||||
|
||||
.study-browser-series
|
||||
overflow: hidden
|
||||
transition(max-height 0.3s ease)
|
||||
|
||||
.study-series-container
|
||||
opacity: 1
|
||||
transition(opacity 0.3s ease\, transform 0.3s ease)
|
||||
transform(translateY(0))
|
||||
transform-origin(50% 0%)
|
||||
|
||||
.study-item-box
|
||||
border: 1px solid $boxBorderColor
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
padding: $spacerY $spacerX ($spacerY - 1)
|
||||
position: relative
|
||||
transition($sidebarTransition)
|
||||
z-index: 0
|
||||
|
||||
&:hover
|
||||
background-color: $boxHoverBackgroundColor
|
||||
border-color: $boxHoverBorderColor
|
||||
|
||||
&.additional
|
||||
theme('color', '$textSecondaryColor')
|
||||
|
||||
&.additional
|
||||
theme('color', '$textPrimaryColor')
|
||||
font-size: 16px
|
||||
font-weight: normal
|
||||
height: 91px
|
||||
line-height: 91px
|
||||
padding: 0
|
||||
text-align: center
|
||||
|
||||
.study-text
|
||||
font-size: 13px
|
||||
left: ($spacerX * 3) + $boxWidth + ($nestingMargin * 3)
|
||||
line-height: 14px
|
||||
position: absolute
|
||||
right: $spacerX
|
||||
top: $spacerY
|
||||
.study-date
|
||||
margin-top: 8px
|
||||
theme('color', '$textSecondaryColor')
|
||||
.study-description
|
||||
margin-top: 8px
|
||||
theme('color', '$textPrimaryColor')
|
||||
.study-availability
|
||||
margin-top: 24px
|
||||
theme('color', '$textPrimaryColor')
|
||||
|
||||
.study-modality
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 20px
|
||||
line-height: $boxWidth
|
||||
margin-left: $nestingMargin * 2
|
||||
margin-top: $nestingMargin * 2
|
||||
position: relative
|
||||
|
||||
.study-modality-text
|
||||
height: 100%
|
||||
overflow: hidden
|
||||
text-align: center
|
||||
text-transform: uppercase
|
||||
|
||||
&
|
||||
&:before
|
||||
&:after
|
||||
theme('border', '%s solid $primaryBackgroundColor' % $borderThickness)
|
||||
theme('background-color', '$boxBackgroundColor')
|
||||
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,3 @@
|
||||
<template name="studyBrowserQuickSwitch">
|
||||
{{>studyBrowserList studyBrowserData}}
|
||||
</template>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.studyBrowserQuickSwitch.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.studiesInformation = new ReactiveVar();
|
||||
const filter = { studyInstanceUid: OHIF.viewer.data.studyInstanceUids };
|
||||
OHIF.studies.searchStudies(filter).then(studies => instance.studiesInformation.set(studies));
|
||||
});
|
||||
|
||||
Template.studyBrowserQuickSwitch.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.autorun(() => {
|
||||
instance.studiesInformation.dep.depend();
|
||||
const currentStudy = instance.data.currentStudy.get();
|
||||
const studyInstanceUid = (currentStudy && currentStudy.studyInstanceUid) || '';
|
||||
Tracker.afterFlush(() => {
|
||||
const $studyBrowserItems = instance.$('.study-browser-item');
|
||||
$studyBrowserItems.removeClass('active');
|
||||
$studyBrowserItems.filter(`[data-uid="${studyInstanceUid}"]`).addClass('active');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.studyBrowserQuickSwitch.events({
|
||||
'ohif.studies.study.click'(event, instance, studyInformation) {
|
||||
const { studyInstanceUid } = studyInformation;
|
||||
const study = OHIF.viewer.Studies.findBy({ studyInstanceUid });
|
||||
instance.data.currentStudy.set(study);
|
||||
const $studySwitch = $(event.currentTarget).closest('.study-switch');
|
||||
$studySwitch.siblings('.series-switch').trigger('rescale');
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyBrowserQuickSwitch.helpers({
|
||||
studyBrowserData() {
|
||||
const instance = Template.instance();
|
||||
const studiesInformation = instance.studiesInformation.get();
|
||||
return { studiesInformation };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
<template name="studyBrowserSeries">
|
||||
<div class="study-browser-series">
|
||||
<div class="study-series-container">
|
||||
{{#each thumbnail in instance.thumbnails.get}}
|
||||
{{>thumbnailEntry (clone this thumbnail=thumbnail)}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.studyBrowserSeries.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { studyInformation, studyMetadata } = instance.data;
|
||||
const studyInstanceUid = (studyMetadata && studyMetadata.studyInstanceUid) || (studyInformation && studyInformation.studyInstanceUid);
|
||||
|
||||
instance.thumbnails = new ReactiveVar([]);
|
||||
|
||||
// Get the study metadata and update the study thumbnails
|
||||
instance.autorun(() => {
|
||||
let metadata = studyMetadata;
|
||||
|
||||
// Check for reactivity
|
||||
if (metadata instanceof ReactiveVar) {
|
||||
metadata = metadata.get();
|
||||
}
|
||||
|
||||
// Retrieve the study metadata
|
||||
if (!metadata) {
|
||||
metadata = OHIF.viewer.Studies.findBy({ studyInstanceUid });
|
||||
}
|
||||
|
||||
// Stop here if there's no study metadata
|
||||
if (!metadata) return;
|
||||
|
||||
// Get the study display sets
|
||||
let displaySets = metadata.displaySets;
|
||||
if (!displaySets.length) {
|
||||
displaySets = OHIF.viewerbase.sortingManager.getDisplaySets(metadata);
|
||||
metadata.displaySets = displaySets;
|
||||
metadata.setDisplaySets(displaySets);
|
||||
|
||||
metadata.forEachDisplaySet(displaySet => {
|
||||
OHIF.viewerbase.stackManager.makeAndAddStack(metadata, displaySet);
|
||||
});
|
||||
}
|
||||
|
||||
// Defines the resulting thumbnails list
|
||||
const thumbnails = [];
|
||||
displaySets.forEach((stack, thumbnailIndex) => {
|
||||
thumbnails.push({
|
||||
thumbnailIndex,
|
||||
stack
|
||||
});
|
||||
});
|
||||
|
||||
instance.thumbnails.set(thumbnails);
|
||||
});
|
||||
});
|
||||
|
||||
Template.studyBrowserSeries.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Run this computation every time the thumbnails are changed
|
||||
instance.autorun(() => {
|
||||
instance.thumbnails.get();
|
||||
Tracker.afterFlush(() => {
|
||||
instance.$('.study-browser-series').adjustMax('height');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<template name="studyBrowserSidebar">
|
||||
{{#scrollArea class='fit noselect' scrollableClass='p-a-1' scrollStep=191}}
|
||||
{{>studyBrowserList (clone this studyChildTemplate='studyBrowserSeries')}}
|
||||
{{/scrollArea}}
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.studyBrowserSidebar.events({
|
||||
'ohif.studies.study.click'(event, instance) {
|
||||
const $element = $(event.currentTarget);
|
||||
|
||||
// Remove active class from sibling studies
|
||||
$element.siblings().removeClass('active');
|
||||
|
||||
// Toggle the active class on clicked study
|
||||
$element.toggleClass('active');
|
||||
}
|
||||
});
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<template name="dialogStudyAssociation">
|
||||
{{#dialogForm (extend this
|
||||
schema=instance.schema
|
||||
dialogClass='modal-lg'
|
||||
title='Study Association'
|
||||
confirmLabel='Save'
|
||||
)}}
|
||||
{{>studyAssociationTable}}
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import moment from 'moment';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
|
||||
Template.dialogStudyAssociation.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.confirmCallback = formData => {
|
||||
OHIF.log.info('Saving associations');
|
||||
const Timepoints = OHIF.studylist.timepointApi.timepoints;
|
||||
|
||||
// Find the rows of the study association table
|
||||
const $tableRows = instance.$('#studyAssociationTable table tbody tr');
|
||||
|
||||
// Create an empty object to group studies into
|
||||
const studies = {};
|
||||
|
||||
// Loop through each row to parse the data
|
||||
$tableRows.each(function() {
|
||||
// Get a selector for this row
|
||||
const $row = $(this);
|
||||
|
||||
// Check the includeStudy checkbox to see if we should parse this row
|
||||
const includeStudy = $row.find('input.includeStudy[type="checkbox"]').eq(0).prop('checked');
|
||||
if (!includeStudy) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the selected timepoint option for this study
|
||||
const $timepointInput = $row.find('input.timepointOption[type="radio"]:checked');
|
||||
|
||||
// Find the related label and trim it down to actual label (TODO: do this another way)
|
||||
const timepointType = $timepointInput.val();
|
||||
|
||||
// Get the study metaData by checking the row with the template engine Blaze
|
||||
const data = Blaze.getData(this);
|
||||
|
||||
// Concatenate the study data to an array, depending on whether is was marked as baseline
|
||||
// or follow-up
|
||||
if (!studies.hasOwnProperty(timepointType)) {
|
||||
studies[timepointType] = [];
|
||||
}
|
||||
|
||||
studies[timepointType].push(data);
|
||||
});
|
||||
|
||||
const studiesKeys = Object.keys(studies);
|
||||
|
||||
// TODO: REMOVE - Temporary for RSNA
|
||||
const hasBaseline = _.contains(studiesKeys, 'baseline');
|
||||
if (hasBaseline) {
|
||||
const patientId = studies[studiesKeys[0]][0].patientId;
|
||||
Timepoints.remove({ patientId });
|
||||
}
|
||||
|
||||
studiesKeys.forEach(timepointType => {
|
||||
// Get the studies associated with this timepoint
|
||||
const relatedStudies = studies[timepointType];
|
||||
|
||||
// Create an array of all the studyInstanceUids for storage in the Timepoint
|
||||
const studyInstanceUids = relatedStudies.map(function(study) {
|
||||
return study.studyInstanceUid;
|
||||
});
|
||||
|
||||
// Create an array of all the studyDates for storage in the Timepoint
|
||||
let studyDates = relatedStudies.map(study => moment(study.studyDate).toDate());
|
||||
|
||||
// Sort the study dates, so we can get a range for these values
|
||||
studyDates = studyDates.sort();
|
||||
|
||||
// HipaaEventType to log changes in collections
|
||||
let hipaaEventType;
|
||||
let hipaaEvent;
|
||||
|
||||
// Check if these studies are already associated with an existing Timepoint
|
||||
let existingTimepoint;
|
||||
if (timepointType === 'baseline') {
|
||||
// If we're trying to associate them to the Baseline, we don't need to
|
||||
// check if the studyInstanceUids are already associated with anything else
|
||||
existingTimepoint = Timepoints.findOne({
|
||||
patientId: relatedStudies[0].patientId,
|
||||
timepointType: 'baseline'
|
||||
});
|
||||
} else {
|
||||
// If we're trying to associate them to a Follow-up, we should check if any
|
||||
// of them are already part of a Follow-up (e.g. Follow-up 1), so that
|
||||
// the rest will also be associated with Follow-up 1.
|
||||
existingTimepoint = Timepoints.findOne({
|
||||
patientId: relatedStudies[0].patientId,
|
||||
studyInstanceUids: {
|
||||
$in: studyInstanceUids
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let timepointId;
|
||||
if (existingTimepoint) {
|
||||
// If these studies are already associated with an existing Timepoint,
|
||||
// and the desired timepoint type is the same (e.g. Follow-up), update
|
||||
// this Timepoint instead of creating a new one
|
||||
Timepoints.update(existingTimepoint._id, {
|
||||
$set: {
|
||||
studyInstanceUids
|
||||
}
|
||||
});
|
||||
timepointId = existingTimepoint.timepointId;
|
||||
hipaaEventType = 'modify';
|
||||
} else {
|
||||
// Create a new timepoint to represent the (baseline or follow-up) studies
|
||||
let timepoint = {
|
||||
timepointType: timepointType,
|
||||
timepointId: OHIF.utils.guid(),
|
||||
studyInstanceUids: studyInstanceUids,
|
||||
patientId: relatedStudies[0].patientId,
|
||||
earliestDate: studyDates[0],
|
||||
latestDate: studyDates[studyDates.length - 1],
|
||||
isLocked: false
|
||||
};
|
||||
|
||||
// Insert this timepoint into the Timepoints Collection
|
||||
Timepoints.insert(timepoint);
|
||||
timepointId = timepoint.timepointId;
|
||||
hipaaEventType = 'create';
|
||||
}
|
||||
|
||||
// Log
|
||||
hipaaEvent = {
|
||||
eventType: hipaaEventType,
|
||||
userId: OHIF.user.getUserId(),
|
||||
userName: OHIF.user.getName(),
|
||||
collectionName: 'Timepoints',
|
||||
recordId: timepointId,
|
||||
patientId: relatedStudies[0].patientId,
|
||||
patientName: relatedStudies[0].patientName
|
||||
};
|
||||
});
|
||||
|
||||
OHIF.studylist.timepointApi.storeTimepoints();
|
||||
|
||||
return formData;
|
||||
};
|
||||
});
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
<template name="studyAssociationTable">
|
||||
<div id="studyAssociationTable">
|
||||
<div id="associationInstructions" class="row">
|
||||
<div class="col-md-12">
|
||||
<h5>Instructions</h5>
|
||||
<p>Select the studies you would like to associate to a time point. Only one follow-up time point can be associated at the same time.</p>
|
||||
<p>We have automatically retrieved all studies within 14 days ({{formatDA this.earliestDate}} to {{formatDA latestDate}}) of your selected studies, in case you forgot to select a study.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tableHeaderBackground">
|
||||
</div>
|
||||
<table class="table table-responsive">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center">Include Study?</th>
|
||||
<th class="center">Study Date</th>
|
||||
<th class="center">Patient Name</th>
|
||||
<th class="center">Study Description</th>
|
||||
<th class="center">Timepoint Type</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each relevantStudies}}
|
||||
<tr class="{{#if autoselected}}disabled{{/if}}">
|
||||
<td class="checkboxWrapper center">
|
||||
<label>
|
||||
<input type="checkbox" class="includeStudy" {{valueIf autoselected '' 'checked'}}/> <span></span>
|
||||
</label>
|
||||
</td>
|
||||
<td class="center studyDataCell">
|
||||
{{#if autoselected}}
|
||||
<p class="studyDate autoselected"
|
||||
data-toggle="tooltip"
|
||||
title="This study was autoselected.">
|
||||
{{formatDA studyDate}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="studyDate">{{formatDA studyDate}}</p>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="center studyDataCell">
|
||||
<p>{{formatPN patientName}}</p>
|
||||
</td>
|
||||
<td class="studyDataCell">
|
||||
<p>{{studyDescription}}</p>
|
||||
</td>
|
||||
<td class="timepointOptions center studyDataCell noselect">
|
||||
{{#each timepointOptions}}
|
||||
<label class="noselect">
|
||||
<input type="radio"
|
||||
name="{{../_id}}"
|
||||
value="{{value}}"
|
||||
class="timepointOption"
|
||||
{{valueIf checked 'checked' ''}}
|
||||
{{valueIf ../autoselected 'disabled' ''}}>
|
||||
<span></span>{{name}}
|
||||
</label>
|
||||
{{/each}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import moment from 'moment';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
/**
|
||||
* Finds related studies within defined time window of =/- 14 days of selected studies
|
||||
* @param selectedStudies
|
||||
* @param range Object
|
||||
*/
|
||||
function getDateRange(selectedStudies, range) {
|
||||
if (range === undefined) {
|
||||
range = {
|
||||
days: 14
|
||||
};
|
||||
}
|
||||
|
||||
if (!selectedStudies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const earliestStudy = selectedStudies[0];
|
||||
const latestStudy = selectedStudies[selectedStudies.length - 1];
|
||||
|
||||
const earliestDate = moment(earliestStudy.studyDate, 'YYYYMMDD');
|
||||
earliestDate.subtract(range);
|
||||
|
||||
const latestDate = moment(latestStudy.studyDate, 'YYYYMMDD');
|
||||
latestDate.add(range);
|
||||
|
||||
return {
|
||||
earliestDate: earliestDate,
|
||||
latestDate: latestDate
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all studies related to the currently input studies,
|
||||
* based on various criteria. Returns the entire array of related studies.
|
||||
*
|
||||
* (at the moment, this is only the date range +/- 14 days, with a matching patientId)
|
||||
*
|
||||
* @param selectedStudies A user-selected list of studies
|
||||
* @returns {*} The entire array of related studies
|
||||
*/
|
||||
function autoSelectStudies(selectedStudies) {
|
||||
if (!selectedStudies.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = getDateRange(selectedStudies);
|
||||
|
||||
// Fetch autoselected studies based on the date range
|
||||
// Note that we used MongoDB's fetch here so we have a mutable array,
|
||||
// rather than a Cursor
|
||||
const autoselected = OHIF.studylist.collections.Studies.find({
|
||||
studyDate: {
|
||||
$gte: range.earliestDate.format('YYYYMMDD'),
|
||||
$lte: range.latestDate.format('YYYYMMDD')
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
studyDate: 1
|
||||
}
|
||||
}).fetch();
|
||||
|
||||
// Make an array of studyInstanceUids in selectedStudies
|
||||
const studyInstanceUids = selectedStudies.map(selectedStudy => selectedStudy.studyInstanceUid);
|
||||
|
||||
autoselected.forEach(study => {
|
||||
const exists = studyInstanceUids.indexOf(study.studyInstanceUid);
|
||||
if (exists > -1) {
|
||||
study.autoselected = false;
|
||||
return;
|
||||
}
|
||||
|
||||
study.autoselected = true;
|
||||
});
|
||||
|
||||
return autoselected;
|
||||
}
|
||||
|
||||
Template.studyAssociationTable.helpers({
|
||||
/**
|
||||
* This helpers includes the user-selected and autoselected studies
|
||||
* to be associated.
|
||||
*
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
relevantStudies() {
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
|
||||
return autoSelectStudies(selectedStudies);
|
||||
},
|
||||
/**
|
||||
* This helper returns the list of Timepoint types the user can set for this study
|
||||
*
|
||||
* @returns {Array.<T>}
|
||||
*/
|
||||
timepointOptions() {
|
||||
return [{
|
||||
value: 'baseline',
|
||||
name: 'Baseline',
|
||||
checked: true
|
||||
}, {
|
||||
value: 'followup',
|
||||
name: 'Follow-up',
|
||||
checked: false
|
||||
}];
|
||||
},
|
||||
earliestDate() {
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
|
||||
const range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.earliestDate;
|
||||
}
|
||||
},
|
||||
latestDate() {
|
||||
const selectedStudies = OHIF.studylist.getSelectedStudies();
|
||||
|
||||
const range = getDateRange(selectedStudies);
|
||||
if (range) {
|
||||
return range.latestDate;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.studyAssociationTable.events({
|
||||
'change input.includeStudy'(event, instance) {
|
||||
const checkbox = event.currentTarget;
|
||||
const studyRow = $(checkbox).closest('tr');
|
||||
const studyDataCells = studyRow.find('td.studyDataCell');
|
||||
|
||||
if (checkbox.checked === true) {
|
||||
studyRow.removeClass('disabled');
|
||||
studyDataCells.find('input').attr('disabled', false);
|
||||
} else {
|
||||
studyRow.addClass('disabled');
|
||||
studyDataCells.find('input').attr('disabled', true);
|
||||
}
|
||||
}
|
||||
});
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
$tableHoverColor = #2c363f
|
||||
$tableTextPrimaryColor = white
|
||||
$tableHeaderHeight = 50px
|
||||
$bodyCellHeight = 30px
|
||||
|
||||
#studyAssociationTable
|
||||
color: #fff
|
||||
|
||||
.header
|
||||
text-align: center
|
||||
|
||||
.center
|
||||
text-align: center
|
||||
|
||||
#associationInstructions
|
||||
theme('background', '$uiGrayDarker')
|
||||
margin-bottom: 2px
|
||||
padding: 0 15px
|
||||
|
||||
.tableHeaderBackground
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
position: absolute
|
||||
height: $tableHeaderHeight + 1px
|
||||
left: 0
|
||||
width: 100%
|
||||
z-index: 1
|
||||
border-bottom: solid 1px
|
||||
theme('border-bottom-color', '$largeNumbersColor')
|
||||
|
||||
table
|
||||
z-index: 2
|
||||
position: relative
|
||||
|
||||
thead
|
||||
tr
|
||||
th
|
||||
padding-bottom: 5px
|
||||
border-bottom: solid 1px #6fbde2
|
||||
height: $tableHeaderHeight
|
||||
font-weight: normal
|
||||
|
||||
tbody
|
||||
tr
|
||||
padding: 5px
|
||||
background-color: black
|
||||
|
||||
&:nth-child(even)
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
|
||||
td
|
||||
position: relative
|
||||
height: $bodyCellHeight
|
||||
line-height: $bodyCellHeight
|
||||
color: $tableTextPrimaryColor
|
||||
border-top: 1px solid #436270
|
||||
border-bottom: 1px solid #436270
|
||||
vertical-align: middle
|
||||
transition(all 0.1s ease)
|
||||
|
||||
p
|
||||
margin: 0
|
||||
|
||||
&.timepointOptions
|
||||
label
|
||||
font-weight: 200
|
||||
cursor: pointer
|
||||
|
||||
input
|
||||
margin: 0 3px
|
||||
|
||||
&[type="radio"]
|
||||
border: 0
|
||||
clip: rect(0 0 0 0)
|
||||
height: 1px
|
||||
margin: -1px
|
||||
overflow: hidden
|
||||
padding: 0
|
||||
position: absolute
|
||||
width: 1px
|
||||
|
||||
& + span
|
||||
display: block
|
||||
position: absolute
|
||||
border: 2px solid #c8c8c8
|
||||
border-radius: 100%
|
||||
height: 15px
|
||||
width: 15px
|
||||
top: 2px
|
||||
left: 0
|
||||
padding: 3px
|
||||
z-index: 5
|
||||
transition: border .25s linear
|
||||
-webkit-transition: border .25s linear
|
||||
|
||||
& + span:before
|
||||
display: block
|
||||
content: ''
|
||||
border-radius: 100%
|
||||
height: 100%
|
||||
width: 100%
|
||||
transition: background 0.25s linear
|
||||
-webkit-transition: background 0.25s linear
|
||||
|
||||
&:checked + span
|
||||
theme('border-color', '$activeColor')
|
||||
|
||||
&:before
|
||||
theme('background-color', '$activeColor')
|
||||
|
||||
&:disabled
|
||||
& + span
|
||||
border-color: #535960 !important
|
||||
|
||||
&:checked + span
|
||||
border-color: #0D4256 !important
|
||||
|
||||
&:before
|
||||
background-color: #0D4256 !important
|
||||
|
||||
label
|
||||
padding: 0 5px
|
||||
padding-left: 20px
|
||||
display: inline-block
|
||||
position: relative
|
||||
line-height: initial
|
||||
|
||||
&.checkboxWrapper
|
||||
label
|
||||
display: block
|
||||
margin: 0
|
||||
|
||||
input
|
||||
margin: 0 3px
|
||||
|
||||
&[type="checkbox"]
|
||||
border: 0
|
||||
clip: rect(0 0 0 0)
|
||||
height: 1px
|
||||
margin: -1px
|
||||
overflow: hidden
|
||||
padding: 0
|
||||
position: absolute
|
||||
width: 1px
|
||||
|
||||
& + span
|
||||
display: block
|
||||
border: 1px solid #b6b6b6
|
||||
background-color: white
|
||||
border-radius: 2px
|
||||
height: 16px
|
||||
width: 16px
|
||||
margin: 0 auto
|
||||
z-index: 5
|
||||
transition: border .25s linear
|
||||
-webkit-transition: border .25s linear
|
||||
|
||||
& + span:before
|
||||
display: block
|
||||
position: relative
|
||||
content: ''
|
||||
font-size: 9px
|
||||
top: -7px
|
||||
color: white
|
||||
background-color: transparent
|
||||
transition: background 0.25s linear
|
||||
-webkit-transition: background 0.25s linear
|
||||
|
||||
&:checked + span
|
||||
theme('background-color', '$activeColor')
|
||||
|
||||
&:before
|
||||
content: '\2713'
|
||||
|
||||
&.disabled
|
||||
td
|
||||
color: #535960
|
||||
|
||||
&:hover, &:active, &.active
|
||||
background-color: $tableHoverColor
|
||||
color: white
|
||||
|
||||
td
|
||||
// This selector is necessary to override bootstrap's 'table' class
|
||||
border-top: 1px solid #436270
|
||||
border-bottom: 1px solid #436270
|
||||
background-color: $tableHoverColor
|
||||
@@ -0,0 +1,7 @@
|
||||
// Study-Timepoint Association imports
|
||||
import './associationModal/associationModal.html';
|
||||
import './associationModal/associationModal.js';
|
||||
|
||||
import './associationModal/studyAssociationTable/studyAssociationTable.html';
|
||||
import './associationModal/studyAssociationTable/studyAssociationTable.styl';
|
||||
import './associationModal/studyAssociationTable/studyAssociationTable.js';
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<template name="caseProgress">
|
||||
{{#form (extend class='caseProgress' api=instance.api)}}
|
||||
{{#if instance.isFollowUp.get}}
|
||||
{{>radialProgressBar isLocked=isLocked progressPercent=progressPercent progressText=progressText}}
|
||||
{{#if isLocked}}
|
||||
<div class="caseProgressStatus">
|
||||
<h5>Locked</h5>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/form}}
|
||||
</template>
|
||||
@@ -0,0 +1,143 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.caseProgress.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.progressPercent = new ReactiveVar();
|
||||
instance.progressText = new ReactiveVar();
|
||||
instance.isLocked = new ReactiveVar(false);
|
||||
instance.isFollowUp = new ReactiveVar(false);
|
||||
});
|
||||
|
||||
Template.caseProgress.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const { timepointApi, measurementApi, timepointId } = instance.data;
|
||||
|
||||
// Stop here if we have no current timepoint ID (and therefore no defined timepointAPI)
|
||||
if (!timepointApi) {
|
||||
instance.progressPercent.set(100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current and prior timepoints
|
||||
const current = timepointApi.timepoints.findOne({ timepointId });
|
||||
const priorFilter = {
|
||||
latestDate: { $lt: current.latestDate },
|
||||
patientId: current.patientId
|
||||
};
|
||||
const priorSorting = { sort: { latestDate: -1 } };
|
||||
const prior = timepointApi.timepoints.findOne(priorFilter, priorSorting);
|
||||
|
||||
// Stop here if timepoint is locked
|
||||
if (current && current.isLocked) {
|
||||
return instance.isLocked.set(true);
|
||||
} else {
|
||||
instance.isLocked.set(false);
|
||||
}
|
||||
|
||||
// Stop here if no current or prior timepoint was found
|
||||
if (!current || !prior || !current.timepointId) {
|
||||
return instance.progressPercent.set(100);
|
||||
}
|
||||
|
||||
// Retrieve the initial number of targets left to measure at this
|
||||
// follow-up. Note that this is done outside of the reactive function
|
||||
// below so that new lesions don't change the initial target count.
|
||||
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const toolGroups = config.measurementTools;
|
||||
|
||||
const toolIds = [];
|
||||
toolGroups.forEach(toolGroup => toolGroup.childTools.forEach(tool => {
|
||||
const option = 'options.caseProgress.include';
|
||||
if (OHIF.utils.ObjectPath.get(tool, option)) {
|
||||
toolIds.push(tool.id);
|
||||
}
|
||||
}));
|
||||
|
||||
const getTimepointFilter = timepointId => ({
|
||||
timepointId,
|
||||
toolId: { $in: toolIds }
|
||||
});
|
||||
|
||||
const getNumMeasurementsAtTimepoint = timepointId => {
|
||||
OHIF.log.info('getNumMeasurementsAtTimepoint');
|
||||
const filter = getTimepointFilter(timepointId);
|
||||
|
||||
let count = 0;
|
||||
toolGroups.forEach(toolGroup => {
|
||||
count += measurementApi.fetch(toolGroup.id, filter).length;
|
||||
});
|
||||
|
||||
return count;
|
||||
};
|
||||
|
||||
const getNumRemainingBetweenTimepoints = (currentTimepointId, priorTimepointId) => {
|
||||
const currentFilter = getTimepointFilter(currentTimepointId);
|
||||
const priorFilter = getTimepointFilter(priorTimepointId);
|
||||
|
||||
let totalRemaining = 0;
|
||||
toolGroups.forEach(toolGroup => {
|
||||
const toolGroupId = toolGroup.id;
|
||||
const numCurrent = measurementApi.fetch(toolGroupId, currentFilter).length;
|
||||
const numPrior = measurementApi.fetch(toolGroupId, priorFilter).length;
|
||||
const remaining = Math.max(numPrior - numCurrent, 0);
|
||||
totalRemaining += remaining;
|
||||
});
|
||||
|
||||
return totalRemaining;
|
||||
};
|
||||
|
||||
// If we're currently reviewing a Baseline timepoint, don't do any
|
||||
// progress measurement.
|
||||
if (current.timepointType === 'baseline') {
|
||||
instance.progressPercent.set(100);
|
||||
instance.isFollowUp.set(false);
|
||||
} else {
|
||||
instance.isFollowUp.set(true);
|
||||
// Setup a reactive function to update the progress whenever
|
||||
// a measurement is made
|
||||
instance.autorun(() => {
|
||||
measurementApi.changeObserver.depend();
|
||||
// Obtain the number of Measurements for which the current Timepoint has
|
||||
// no Measurement data
|
||||
const totalMeasurements = getNumMeasurementsAtTimepoint(prior.timepointId);
|
||||
const numRemainingMeasurements = getNumRemainingBetweenTimepoints(current.timepointId, prior.timepointId);
|
||||
const numMeasurementsMade = totalMeasurements - numRemainingMeasurements;
|
||||
|
||||
// Update the Case Progress text with the remaining measurement count
|
||||
instance.progressText.set(numRemainingMeasurements);
|
||||
|
||||
// Calculate the Case Progress as a percentage in order to update the
|
||||
// radial progress bar
|
||||
const progressPercent = Math.min(100, Math.round(100 * numMeasurementsMade / totalMeasurements));
|
||||
instance.progressPercent.set(progressPercent);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Template.caseProgress.helpers({
|
||||
progressPercent() {
|
||||
return Template.instance().progressPercent.get();
|
||||
},
|
||||
|
||||
progressText() {
|
||||
return Template.instance().progressText.get();
|
||||
},
|
||||
|
||||
isLocked() {
|
||||
return Template.instance().isLocked.get();
|
||||
},
|
||||
|
||||
progressComplete() {
|
||||
const instance = Template.instance();
|
||||
if (!instance.data.timepointApi) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const progressPercent = instance.progressPercent.get();
|
||||
return progressPercent === 100;
|
||||
}
|
||||
});
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
.caseProgress
|
||||
transition(all 0.3 ease)
|
||||
|
||||
.radialProgress
|
||||
display: inline-block
|
||||
float:left
|
||||
|
||||
.caseProgressStatus
|
||||
display: inline-block
|
||||
float: left
|
||||
margin-left: 22px
|
||||
|
||||
h5
|
||||
theme('color', '$textPrimaryColor')
|
||||
line-height: 40px
|
||||
margin: 0
|
||||
font-size: 15px
|
||||
font-weight: bold
|
||||
@@ -0,0 +1,8 @@
|
||||
// Case Progress imports
|
||||
import './caseProgress.html';
|
||||
import './caseProgress.styl';
|
||||
import './caseProgress.js';
|
||||
|
||||
import './radialProgressBar/radialProgressBar.html';
|
||||
import './radialProgressBar/radialProgressBar.styl';
|
||||
import './radialProgressBar/radialProgressBar.js';
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<template name="radialProgressBar">
|
||||
<div class="radialProgress">
|
||||
<svg id="svg"
|
||||
width="26"
|
||||
height="26"
|
||||
viewPort="0 0 26 26"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<!-- stroke-dasharray is 2 * PI * radius !-->
|
||||
<circle r="11"
|
||||
cx="13"
|
||||
cy="13"
|
||||
fill="transparent"
|
||||
stroke-dasharray="69.11503837897544"
|
||||
stroke-dashoffset="0">
|
||||
</circle>
|
||||
{{#unless isLocked}}
|
||||
<circle id="bar"
|
||||
r="11"
|
||||
cx="13"
|
||||
cy="13"
|
||||
fill="transparent"
|
||||
stroke-dasharray="69.11503837897544"
|
||||
style="stroke-dashoffset: {{progressRadius}}px;">
|
||||
</circle>
|
||||
{{/unless}}
|
||||
</svg>
|
||||
<div class="progressArea {{#if isLocked}}locked{{else}}{{#if progressComplete}}complete{{/if}}{{/if}}">
|
||||
{{#if isLocked}}
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-status-lock"}}></use>
|
||||
</svg>
|
||||
{{else}}
|
||||
{{#if progressComplete}}
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-status-complete"}}></use>
|
||||
</svg>
|
||||
{{else}}
|
||||
{{progressText}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.radialProgressBar.helpers({
|
||||
progressComplete() {
|
||||
const instance = Template.instance();
|
||||
return instance.data.progressPercent === 100;
|
||||
},
|
||||
|
||||
progressRadius() {
|
||||
const instance = Template.instance();
|
||||
const radius = 11 * 2 * Math.PI;
|
||||
const percentLeft = (100 - instance.data.progressPercent) / 100;
|
||||
return percentLeft * radius;
|
||||
}
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
$circleSize = 26px
|
||||
|
||||
.radialProgress
|
||||
border-radius: 100%
|
||||
height: $circleSize
|
||||
margin-top: 3px
|
||||
margin-right: 4px
|
||||
position: relative
|
||||
vendorize(box-shadow, 0 0 1em black)
|
||||
width: $circleSize
|
||||
|
||||
#svg circle
|
||||
stroke-dashoffset: 0
|
||||
theme('stroke', '$uiBorderColorDark')
|
||||
stroke-width: 3px
|
||||
vendorize(transition, stroke-dashoffset 1s linear)
|
||||
|
||||
#svg #bar
|
||||
theme('stroke', '$textSecondaryColor')
|
||||
vendorize(transform, rotate(270deg))
|
||||
vendorize(transform-origin, center center)
|
||||
|
||||
.progressArea
|
||||
theme('color', '$textSecondaryColor')
|
||||
display: table
|
||||
font-weight: 700
|
||||
font-size: 12px
|
||||
height: $circleSize
|
||||
line-height: $circleSize
|
||||
left: 50%
|
||||
position: absolute
|
||||
text-align: center
|
||||
top: 50%
|
||||
transform(translate(-50%, -50%))
|
||||
width: $circleSize
|
||||
|
||||
&.locked
|
||||
line-height: 0
|
||||
|
||||
&, & svg
|
||||
height: 12px
|
||||
width: 10px
|
||||
|
||||
&.complete
|
||||
line-height: 0
|
||||
|
||||
&, & svg
|
||||
height: 16px
|
||||
width: 16px
|
||||
|
||||
svg
|
||||
theme('fill', '$textSecondaryColor')
|
||||
theme('stroke', '$textSecondaryColor')
|
||||
@@ -0,0 +1,5 @@
|
||||
//import './association';
|
||||
import './caseProgress';
|
||||
import './measurementTable';
|
||||
import './measurementLightTable';
|
||||
import './measureFlow';
|
||||
@@ -0,0 +1,3 @@
|
||||
import './measureFlow.html';
|
||||
import './measureFlow.styl';
|
||||
import './measureFlow.js';
|
||||
@@ -0,0 +1,32 @@
|
||||
<template name="measureFlow">
|
||||
<div class="measure-flow {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}"
|
||||
style="top:{{choose position.y 0}}px; left:{{choose position.x 0}}px"
|
||||
tabindex="-1">
|
||||
<button class="btn-add" style="opacity: 0">
|
||||
{{#if this.measurement.location}}Edit{{else}}Add{{/if}} label
|
||||
</button>
|
||||
{{#if eq instance.state.get 'selected'}}
|
||||
<div class="tree-leaf">
|
||||
<i class="icon-check"></i>
|
||||
<span>{{instance.value.label}}</span>
|
||||
{{#if instance.descriptionEdit.get}}
|
||||
<div class="description clearfix">
|
||||
<textarea
|
||||
class="pull-left"
|
||||
rows="1"
|
||||
style="{{#unless instance.description.get}}height:0{{/unless}}"
|
||||
>{{instance.description.get}}</textarea>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if instance.description.get}}
|
||||
<div class="descriptionText">{{instance.description.get}}</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
<div class="actions clearfix">
|
||||
<button class="pull-left btn-rename">Relabel</button>
|
||||
<button class="pull-right btn-description">{{valueIf instance.description.get 'Edit' 'Add'}} description</button>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,326 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
|
||||
Template.measureFlow.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.value = instance.data.currentValue || '';
|
||||
|
||||
instance.state = new ReactiveVar('closed');
|
||||
instance.description = new ReactiveVar(instance.data.currentDescription || '');
|
||||
instance.descriptionEdit = new ReactiveVar(false);
|
||||
|
||||
const items = [
|
||||
'Abdomen/Chest Wall',
|
||||
'Adrenal',
|
||||
'Bladder',
|
||||
'Bone',
|
||||
'Brain',
|
||||
'Breast',
|
||||
'Colon',
|
||||
'Esophagus',
|
||||
'Extremities',
|
||||
'Gallbladder',
|
||||
'Kidney',
|
||||
'Liver',
|
||||
'Lung',
|
||||
'Lymph Node',
|
||||
'Mediastinum/Hilum',
|
||||
'Muscle',
|
||||
'Neck',
|
||||
'Other Soft Tissue',
|
||||
'Ovary',
|
||||
'Pancreas',
|
||||
'Pelvis',
|
||||
'Peritoneum/Omentum',
|
||||
'Prostate',
|
||||
'Retroperitoneum',
|
||||
'Small Bowel',
|
||||
'Spleen',
|
||||
'Stomach',
|
||||
'Subcutaneous'
|
||||
];
|
||||
|
||||
instance.items = [];
|
||||
_.each(items, item => {
|
||||
instance.items.push({
|
||||
label: item,
|
||||
value: item
|
||||
});
|
||||
});
|
||||
|
||||
const commonItems = [
|
||||
'Abdomen/Chest Wall',
|
||||
'Lung',
|
||||
'Lymph Node',
|
||||
'Liver',
|
||||
'Mediastinum/Hilum',
|
||||
'Pelvis',
|
||||
'Peritoneum/Omentum',
|
||||
'Retroperitoneum'
|
||||
];
|
||||
|
||||
instance.commonItems = [];
|
||||
_.each(commonItems, item => {
|
||||
instance.commonItems.push({
|
||||
label: item,
|
||||
value: item
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.measureFlow.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const $measureFlow = instance.$('.measure-flow');
|
||||
const $btnAdd = instance.$('.btn-add');
|
||||
|
||||
// Make the measure flow bounded by the window borders
|
||||
$measureFlow.bounded();
|
||||
|
||||
$btnAdd.focus();
|
||||
|
||||
if (instance.data.autoClick) {
|
||||
$btnAdd.trigger('click', {
|
||||
clientX: instance.data.position.x,
|
||||
clientY: instance.data.position.Y
|
||||
});
|
||||
} else {
|
||||
if (instance.data.direction) {
|
||||
const direction = instance.data.direction;
|
||||
let { left, top } = $measureFlow.offset();
|
||||
|
||||
left = direction.x === -1 ? left -= $btnAdd.outerWidth() : left;
|
||||
top = direction.y === -1 ? top -= $btnAdd.outerHeight() : top;
|
||||
|
||||
const distance = 5;
|
||||
left += direction.x * distance;
|
||||
top += direction.y * distance;
|
||||
|
||||
$measureFlow.css({
|
||||
left,
|
||||
top
|
||||
});
|
||||
}
|
||||
|
||||
// Display the button after reposition it
|
||||
$btnAdd.css('opacity', 1);
|
||||
}
|
||||
});
|
||||
|
||||
Template.measureFlow.events({
|
||||
'click, mousedown, mouseup'(event, instance) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
'click .measure-flow .btn-add, click .measure-flow .btn-rename'(event, instance) {
|
||||
const $measureFlow = instance.$('.measure-flow');
|
||||
|
||||
// Set the open state for the component
|
||||
instance.state.set('open');
|
||||
|
||||
// Wait template rerender before rendering the selectTree
|
||||
Tracker.afterFlush(() => {
|
||||
// Get the click or rendering position
|
||||
let position;
|
||||
if (_.isUndefined(event.clientX)) {
|
||||
position = {
|
||||
left: instance.data.position.x,
|
||||
top: instance.data.position.y,
|
||||
};
|
||||
} else {
|
||||
position = {
|
||||
left: event.clientX,
|
||||
top: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
// Define the data for selectTreeComponent
|
||||
const data = {
|
||||
key: 'label',
|
||||
items: instance.items,
|
||||
commonItems: instance.commonItems,
|
||||
hideCommon: instance.data.hideCommon,
|
||||
label: 'Assign label',
|
||||
searchPlaceholder: 'Search labels',
|
||||
// storageKey: 'measureLabelCommon',
|
||||
threeColumns: instance.data.threeColumns,
|
||||
position
|
||||
};
|
||||
|
||||
// Define in which element the selectTree will be rendered in
|
||||
const parentElement = $measureFlow[0];
|
||||
|
||||
// Render the selectTree element
|
||||
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
|
||||
|
||||
// Focus the measure flow to handle closing
|
||||
$measureFlow.focus();
|
||||
});
|
||||
},
|
||||
|
||||
'click .measure-flow .btn-description'(event, instance) {
|
||||
// Fade out the action buttons
|
||||
instance.$('.measure-flow .actions').addClass('fadeOut');
|
||||
|
||||
// Set the description edit mode
|
||||
instance.descriptionEdit.set(true);
|
||||
|
||||
// Wait for DOM re-rendering, resize and focus the description textarea
|
||||
Tracker.afterFlush(() => {
|
||||
const $textarea = instance.$('textarea');
|
||||
$textarea.trigger('input').focus().select();
|
||||
});
|
||||
},
|
||||
|
||||
'input textarea, change textarea'(event, instance) {
|
||||
const element = event.currentTarget;
|
||||
const $element = $(element);
|
||||
|
||||
// Resize the textarea based on its content length
|
||||
$element.css('max-height', 0);
|
||||
$element.height(element.scrollHeight);
|
||||
$element.css('max-height', '');
|
||||
|
||||
// Reposition the measure flow if needed
|
||||
const $measureFlow = instance.$('.measure-flow');
|
||||
$element.one('transitionend', () => $measureFlow.trigger('spatialChanged'));
|
||||
},
|
||||
|
||||
'keydown textarea'(event, instance) {
|
||||
// Unset the description edit mode if ENTER or ESC was pressed
|
||||
if (event.which === 13 || event.which === 27) {
|
||||
instance.$('.measure-flow .actions').removeClass('fadeOut');
|
||||
instance.descriptionEdit.set(false);
|
||||
instance.$('.measure-flow').focus();
|
||||
}
|
||||
|
||||
// Keep the current description if ENTER was pressed
|
||||
if (event.which === 13) {
|
||||
event.preventDefault();
|
||||
instance.description.set($(event.currentTarget).val());
|
||||
}
|
||||
},
|
||||
|
||||
'blur textarea'(event, instance) {
|
||||
instance.$('.measure-flow .actions').removeClass('fadeOut');
|
||||
instance.descriptionEdit.set(false);
|
||||
instance.description.set($(event.currentTarget).val());
|
||||
},
|
||||
|
||||
'click .select-tree-common label'(event, instance) {
|
||||
// Set the common section clicked flag
|
||||
instance.commonClicked = event.currentTarget;
|
||||
},
|
||||
|
||||
'change .select-tree-root'(event, instance) {
|
||||
// Stop here if it's an inner input event
|
||||
if (event.target !== event.currentTarget) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the selectTree component value before it's removed from DOM
|
||||
const $treeRoot = $(event.currentTarget);
|
||||
instance.value = $treeRoot.data('component').value();
|
||||
},
|
||||
|
||||
'click .tree-leaf input'(event, instance) {
|
||||
const $target = $(event.currentTarget);
|
||||
let $label = $target.closest('label');
|
||||
const $treeRoot = $label.closest('.select-tree-root');
|
||||
const $container = $treeRoot.find('.tree-options:first');
|
||||
let labelOffset;
|
||||
|
||||
// Check if the clicked target was a label inside common section
|
||||
if (instance.commonClicked) {
|
||||
$label = $(instance.commonClicked);
|
||||
labelOffset = $label.data('offset');
|
||||
} else {
|
||||
labelOffset = $label.offset();
|
||||
}
|
||||
|
||||
// Change the measure flow state to selected
|
||||
instance.state.set('selected');
|
||||
|
||||
// Wait for the DOM re-rendering
|
||||
Tracker.afterFlush(() => {
|
||||
// Get the measure flow div
|
||||
const $measureFlow = instance.$('.measure-flow');
|
||||
|
||||
// Adjust the label position
|
||||
if (instance.commonClicked) {
|
||||
labelOffset.top -= 10;
|
||||
labelOffset.left -= 12;
|
||||
} else {
|
||||
labelOffset.top -= 10;
|
||||
}
|
||||
|
||||
// Reposition the measure flow based on the clicked label position
|
||||
$measureFlow.css(labelOffset);
|
||||
|
||||
// Resize the copied label with same width of the clicked one
|
||||
// $measureFlow.children('.tree-leaf').width($label.outerWidth());
|
||||
$measureFlow.children('.tree-leaf').width(212);
|
||||
|
||||
// Reset the flag to avoid wrong positioning when clicking normal labels again
|
||||
instance.commonClicked = false;
|
||||
|
||||
instance.data.updateCallback(instance.value.value, instance.description.get());
|
||||
|
||||
Meteor.defer(() => !$measureFlow.is(':hover') && $measureFlow.trigger('close'));
|
||||
});
|
||||
|
||||
// Wait the fade-out transition and remove the selectTree component
|
||||
$container.one('transitionend', event => Blaze.remove(instance.selectTreeView));
|
||||
},
|
||||
|
||||
'blur .measure-flow'(event, instance) {
|
||||
const $measureFlow = $(event.currentTarget);
|
||||
const element = $measureFlow[0];
|
||||
Meteor.defer(() => {
|
||||
const focused = $(':focus')[0];
|
||||
if (element !== focused && !$.contains(element, focused)) {
|
||||
$measureFlow.trigger('close');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'mouseleave .measure-flow'(event, instance) {
|
||||
const $measureFlow = $(event.currentTarget);
|
||||
const canClose = instance.state.get() === 'selected' && !instance.descriptionEdit.get();
|
||||
if (canClose && !$.contains($measureFlow[0], event.toElement)) {
|
||||
$measureFlow.trigger('close');
|
||||
}
|
||||
},
|
||||
|
||||
'mouseenter .measure-flow'(event, instance) {
|
||||
// Prevent from closing if user go out and in too fast
|
||||
clearTimeout(instance.closingTimeout);
|
||||
$(event.currentTarget).off('animationend').removeClass('fadeOut');
|
||||
},
|
||||
|
||||
'close .measure-flow'(event, instance) {
|
||||
const $measureFlow = $(event.currentTarget);
|
||||
|
||||
// Clear the timeout to prevent executing the close process twice
|
||||
clearTimeout(instance.closingTimeout);
|
||||
|
||||
instance.closingTimeout = setTimeout(() => {
|
||||
const animationEndHandler = event => {
|
||||
// Prevent closing if the animation is coming from actions panel
|
||||
if (event.target !== $measureFlow[0]) {
|
||||
$measureFlow.one('animationend', animationEndHandler);
|
||||
return;
|
||||
}
|
||||
|
||||
instance.data.doneCallback();
|
||||
};
|
||||
|
||||
$measureFlow.one('animationend', animationEndHandler).addClass('fadeOut');
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measure-flow
|
||||
outline: none
|
||||
position: fixed
|
||||
z-index: 1
|
||||
|
||||
&.fadeOut
|
||||
animateFadeOut()
|
||||
|
||||
&.open,
|
||||
&.selected
|
||||
|
||||
.btn-add
|
||||
opacity: 0 !important
|
||||
outline: none !important
|
||||
pointer-events: none !important
|
||||
|
||||
.btn-add
|
||||
outline: none
|
||||
theme('color', '$textColorActive')
|
||||
theme('background-color', '$activeColor')
|
||||
theme('border', '2px solid $uiBorderColor')
|
||||
border-radius: 14px
|
||||
cursor: pointer
|
||||
font-weight: bold
|
||||
height: 24px
|
||||
left: 0
|
||||
line-height: 24px
|
||||
opacity: 1
|
||||
position: absolute
|
||||
padding: 0 14px
|
||||
top: 0
|
||||
transition(opacity 0.3s ease)
|
||||
white-space: nowrap
|
||||
|
||||
// / {selector()}
|
||||
// color: $themes['tide']['uiYellow'] !important
|
||||
|
||||
.select-tree-root>.tree-content
|
||||
width: 140px
|
||||
|
||||
&>.tree-options
|
||||
position: relative
|
||||
z-index: 2
|
||||
|
||||
.tree-inputs
|
||||
position: relative
|
||||
|
||||
&>.tree-leaf
|
||||
position: relative
|
||||
|
||||
.icon-check
|
||||
theme('background-color', '$activeColor')
|
||||
animateZoomIn()
|
||||
left: -46px
|
||||
position: absolute
|
||||
top: 3px
|
||||
|
||||
span
|
||||
background: white
|
||||
font-weight: normal
|
||||
height: 46px
|
||||
line-height: 46px
|
||||
padding: 0 12px
|
||||
|
||||
input, span
|
||||
display: none
|
||||
|
||||
.actions
|
||||
padding-top: 16px
|
||||
opacity: 0
|
||||
|
||||
&:not(.fadeOut)
|
||||
animateFadeIn()
|
||||
animation-delay: 0.3s
|
||||
|
||||
&.fadeOut
|
||||
animateFadeOut()
|
||||
|
||||
button
|
||||
theme('border', '1px solid $uiBorderColor')
|
||||
theme('color', '$textPrimaryColor')
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
border-radius: 16px
|
||||
font-weight: normal
|
||||
height: 31px
|
||||
line-height: 31px
|
||||
padding: 0 12px
|
||||
text-align: center
|
||||
|
||||
.description
|
||||
margin-top: -10px
|
||||
margin-bottom: 10px
|
||||
|
||||
textarea
|
||||
background-color: white
|
||||
box-shadow: 0 10px white
|
||||
border: 0
|
||||
line-height: 20px
|
||||
outline: none
|
||||
overflow: hidden
|
||||
padding: 0 12px 5px
|
||||
resize: none
|
||||
transition(height 0.3s ease)
|
||||
width: 100%
|
||||
|
||||
.descriptionText
|
||||
background-color: white
|
||||
box-shadow: 0 10px white
|
||||
line-height: 20px
|
||||
margin-bottom: 10px
|
||||
margin-top: -10px
|
||||
padding: 0 12px
|
||||
|
||||
&.selected>.tree-leaf
|
||||
|
||||
span
|
||||
display: block
|
||||
|
||||
&.tree-columns
|
||||
|
||||
.select-tree-root
|
||||
|
||||
&.navigated .select-tree-common .content
|
||||
animation-name: none
|
||||
|
||||
&.selected .select-tree-common .content
|
||||
animation-name: selectTreeCommonCloseLeft
|
||||
|
||||
.tree-search
|
||||
input, i
|
||||
display: none
|
||||
|
||||
.select-tree-common
|
||||
padding-left: 0
|
||||
padding-right: 6px
|
||||
right: auto
|
||||
left: 0
|
||||
width: 180px
|
||||
|
||||
&>.tree-content
|
||||
transform-origin(0% 50%)
|
||||
width: 280px
|
||||
|
||||
.select-tree-common .content
|
||||
right: -180px
|
||||
|
||||
&.started .select-tree-common .content
|
||||
right: 46px
|
||||
|
||||
&.navigated .select-tree-common .content
|
||||
animation-name: selectTreeCommonCloseRight
|
||||
|
||||
.tree-inputs>label
|
||||
float: left
|
||||
width: 50%
|
||||
|
||||
@keyframes selectTreeCommonCloseRight {
|
||||
from {
|
||||
height: 37px
|
||||
display: table
|
||||
right: 6px
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
}
|
||||
to {
|
||||
height: 100%
|
||||
right: -100%
|
||||
opacity: 0
|
||||
visibility: hidden
|
||||
width: calc(100% - 6px)
|
||||
}
|
||||
}
|
||||
|
||||
.icon-check
|
||||
background-color: white
|
||||
border-radius: 20px
|
||||
display: block
|
||||
height: 40px
|
||||
overflow: hidden
|
||||
position: relative
|
||||
width: 40px
|
||||
|
||||
&:before
|
||||
border: 2px solid black
|
||||
border-left-width: 0
|
||||
border-top-width: 0
|
||||
content: ''
|
||||
display: block
|
||||
height: 18px
|
||||
left: 15px
|
||||
position: absolute
|
||||
top: 9px
|
||||
transform(rotate(45deg))
|
||||
width: 9px
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Measurement Table Components imports
|
||||
|
||||
import './measurementEditDescription/measurementEditDescription.html';
|
||||
import './measurementEditDescription/measurementEditDescription.js';
|
||||
|
||||
import './measurementLightTable.html';
|
||||
import './measurementLightTable.styl';
|
||||
|
||||
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.html';
|
||||
import './measurementLightTableHeaderRow/measurementLightTableHeaderRow.styl';
|
||||
|
||||
import './measurementLightTableRow/measurementLightTableRow.html';
|
||||
import './measurementLightTableRow/measurementLightTableRow.styl';
|
||||
import './measurementLightTableRow/measurementLightTableRow.js';
|
||||
|
||||
import './measurementLightTableView/measurementLightTableView.html';
|
||||
import './measurementLightTableView/measurementLightTableView.styl';
|
||||
import './measurementLightTableView/measurementLightTableView.js';
|
||||
|
||||
import './measurementRelabel/measurementRelabel.html';
|
||||
import './measurementRelabel/measurementRelabel.styl';
|
||||
import './measurementRelabel/measurementRelabel.js';
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<template name="measurementEditDescription">
|
||||
{{#dialogForm (extend this
|
||||
dialogClass='modal-sm'
|
||||
api=instance.api
|
||||
)}}
|
||||
{{>inputText
|
||||
labelClass='form-group'
|
||||
key='description'
|
||||
hideSearch=true
|
||||
options=(clone placeholder='Add a description')
|
||||
}}
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
|
||||
Template.measurementEditDescription.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const form = instance.$('form').data('component');
|
||||
const measurementData = instance.data.measurementData;
|
||||
const collection = OHIF.viewer.measurementApi.tools[measurementData.toolType];
|
||||
const currentMeasurement = collection.findOne({ _id: measurementData._id });
|
||||
|
||||
if (currentMeasurement.description) {
|
||||
form.value({
|
||||
description: currentMeasurement.description
|
||||
});
|
||||
}
|
||||
|
||||
// Update the description after confirming the dialog data
|
||||
instance.data.promise.then(formData => {
|
||||
collection.update({
|
||||
_id: measurementData._id,
|
||||
}, {
|
||||
$set: { description: formData.description }
|
||||
});
|
||||
});
|
||||
});
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<template name="measurementLightTable">
|
||||
<div id="measurementLightTableContainer" class="flex-v">
|
||||
<div class="measurementLightTableHeaderContainer">
|
||||
<div class="measurementLightTableHeader">
|
||||
<div class="studyDateLabel">
|
||||
Study date:
|
||||
</div>
|
||||
<div class="studyDate">
|
||||
{{formatDA this.timepointApi.all.[0].latestDate "DD-MMM-YY"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{#scrollArea class='measurementLightTableView flex-grow fit' scrollStep=63}}
|
||||
{{>measurementLightTableView (clone this)}}
|
||||
{{/scrollArea}}
|
||||
</div>
|
||||
</template>
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
#measurementLightTableContainer
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
.measurementLightTableHeaderContainer
|
||||
display: flex
|
||||
padding: 0 2px 0 44px
|
||||
position: relative
|
||||
|
||||
.measurementLightTableHeader
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
padding-top: 2px
|
||||
position: relative
|
||||
margin: 0.4em 0
|
||||
|
||||
.studyDateLabel, .studyDate
|
||||
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
font-weight: 400
|
||||
padding-left: 12px
|
||||
text-align: left
|
||||
|
||||
.studyDateLabel
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 12px
|
||||
line-height: 12px
|
||||
|
||||
.studyDate
|
||||
theme('color', '$textPrimaryColor')
|
||||
font-size: 14px
|
||||
line-height: 20px
|
||||
padding-bottom: 6px
|
||||
|
||||
.measurementLightTableLayoutChanger
|
||||
margin: 0 auto
|
||||
padding: 20px
|
||||
text-align: center
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<template name="measurementLightTableHeaderRow">
|
||||
<div class="measurementLightTableHeaderRow">
|
||||
<div class="type">{{toolGroup.name}}</div>
|
||||
<div class="numberOfMeasurements">{{measurementRows.length}}</div>
|
||||
</div>
|
||||
</template>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
$headerRowHeight = 63px
|
||||
|
||||
.measurementLightTableHeaderRow
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
theme('color', '$textSecondaryColor')
|
||||
display: flex
|
||||
theme('fill', '$textSecondaryColor')
|
||||
height: $headerRowHeight
|
||||
line-height: $headerRowHeight
|
||||
margin-top: 2px
|
||||
width: 100%
|
||||
|
||||
div
|
||||
align-items: stretch
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
text-align: center
|
||||
|
||||
.type
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
line-height: $headerRowHeight
|
||||
padding: 0 10px
|
||||
text-align: left
|
||||
|
||||
.numberOfMeasurements
|
||||
theme('color', '$uiSkyBlue')
|
||||
float: right
|
||||
font-weight: 300
|
||||
font-size: 40px
|
||||
max-width: 54px
|
||||
height: $headerRowHeight
|
||||
line-height: 66px
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<template name="measurementLightTableRow">
|
||||
<div class="measurementLightTableRow" data-measurementid="{{_id}}">
|
||||
<div class='measurementRowSidebar'>
|
||||
<div class="measurementNumber">
|
||||
{{rowItem.measurementNumber}}
|
||||
</div>
|
||||
{{#if rowItem.responseStatus}}
|
||||
<div class="response-status-icon">
|
||||
{{rowItem.responseStatus}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="measurementRowContent">
|
||||
<div class="measurementDetails">
|
||||
<label class="location">
|
||||
{{#if rowItem.location}}
|
||||
{{rowItem.location}}
|
||||
{{else}}
|
||||
(No Location)
|
||||
{{/if}}
|
||||
{{#if rowItem.description}}
|
||||
({{rowItem.description}})
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="value">
|
||||
{{#if displayData}}
|
||||
{{{displayData}}}
|
||||
{{else}}
|
||||
...
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="rowOptions">
|
||||
<div class="rowAction js-edit-label pull-left m-r-2" tabindex="1">
|
||||
<svg class="edit-icon">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
|
||||
</svg>
|
||||
Relabel
|
||||
</div>
|
||||
<div class="rowAction js-edit-description pull-left m-r-2" tabindex="1">
|
||||
<svg class="edit-icon">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
|
||||
</svg>
|
||||
Description
|
||||
</div>
|
||||
<div class="rowAction js-delete pull-left" tabindex="1">
|
||||
<svg class="close-icon">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
|
||||
</svg>
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
const getPosition = event => {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
};
|
||||
};
|
||||
|
||||
Template.measurementLightTableRow.helpers({
|
||||
displayData() {
|
||||
const instance = Template.instance();
|
||||
const { rowItem } = instance.data;
|
||||
|
||||
const data = rowItem.entries[0];
|
||||
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const measurementTools = config.measurementTools;
|
||||
|
||||
const toolGroup = measurementTools.find( toolGroup => toolGroup.id === rowItem.measurementTypeId);
|
||||
const tool = toolGroup.childTools.find( childTool => childTool.id === data.toolType );
|
||||
if (!tool) {
|
||||
return 'No measurement value';
|
||||
}
|
||||
|
||||
const { displayFunction } = tool.options.measurementTable;
|
||||
return displayFunction(data);
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementLightTableRow.events({
|
||||
'click .measurementLightTableRow'(event, instance) {
|
||||
const $row = instance.$('.measurementLightTableRow');
|
||||
const rowItem = instance.data.rowItem;
|
||||
const timepoints = instance.data.timepointApi.all();
|
||||
|
||||
$row.closest('.measurementLightTableView').find('.measurementLightTableRow').not($row).removeClass('active');
|
||||
$row.toggleClass('active');
|
||||
|
||||
const childToolKey = $(event.target).attr('data-child');
|
||||
OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey);
|
||||
},
|
||||
|
||||
'click .js-edit-label'(event, instance) {
|
||||
event.stopPropagation();
|
||||
const rowItem = instance.data.rowItem;
|
||||
const entry = rowItem.entries[0];
|
||||
|
||||
// Show the measure flow for measurements
|
||||
OHIF.measurements.openLocationModal({
|
||||
measurement: entry,
|
||||
element: document.body,
|
||||
measurementApi: instance.data.measurementApi,
|
||||
position: getPosition(event),
|
||||
autoClick: true
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-edit-description'(event, instance) {
|
||||
const rowItem = instance.data.rowItem;
|
||||
const entry = rowItem.entries[0];
|
||||
OHIF.ui.showDialog('measurementEditDescription', {
|
||||
event,
|
||||
title: 'Edit Description',
|
||||
element: event.element,
|
||||
measurementData: entry
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-delete'(event, instance) {
|
||||
event.stopPropagation();
|
||||
const dialogSettings = {
|
||||
class: 'themed',
|
||||
title: 'Delete measurements',
|
||||
message: 'Are you sure you want to delete the measurement?',
|
||||
position: getPosition(event)
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => {
|
||||
const measurementTypeId = instance.data.rowItem.measurementTypeId;
|
||||
const measurement = instance.data.rowItem.entries[0];
|
||||
const measurementNumber = measurement.measurementNumber;
|
||||
const { timepointApi, measurementApi } = instance.data;
|
||||
|
||||
// Remove all the measurements with the given type and number
|
||||
measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber });
|
||||
|
||||
// Sync the new measurement data with cornerstone tools
|
||||
const baseline = timepointApi.baseline();
|
||||
measurementApi.sortMeasurements(baseline.timepointId);
|
||||
|
||||
// Repaint the images on all viewports without the removed measurements
|
||||
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
|
||||
|
||||
// Notify that viewer suffered changes
|
||||
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
|
||||
});
|
||||
}
|
||||
});
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measurementLightTableRow
|
||||
display: flex
|
||||
margin-left: -6px
|
||||
margin-top: 2px
|
||||
padding-left: 6px
|
||||
opacity: 0.7
|
||||
transform(scale(1))
|
||||
width: calc(100% + 6px)
|
||||
|
||||
&:hover
|
||||
opacity 1
|
||||
|
||||
&.active
|
||||
opacity 1
|
||||
.measurementRowSidebar
|
||||
theme('color', '$activeColor')
|
||||
|
||||
.rowOptions
|
||||
height: 35px
|
||||
visibility: visible
|
||||
|
||||
.rowOptions
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
height: 0
|
||||
overflow: hidden
|
||||
transition(all 0.3s ease)
|
||||
visibility: hidden
|
||||
padding-left: 14px
|
||||
|
||||
.rowAction
|
||||
theme('color', '$defaultColor')
|
||||
cursor: pointer
|
||||
line-height: 35px
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&:hover, &:active
|
||||
theme('color', '$textPrimaryColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$textPrimaryColor')
|
||||
theme('stroke', '$textPrimaryColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$defaultColor')
|
||||
theme('stroke', '$defaultColor')
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&.edit-icon
|
||||
width: 14px
|
||||
height: 13px
|
||||
|
||||
&.close-icon
|
||||
width: 11px
|
||||
height: 11px
|
||||
|
||||
.measurementRowSidebar
|
||||
theme('background', '$uiGray')
|
||||
theme('color', '$textSecondaryColor')
|
||||
cursor: pointer
|
||||
flex: 1
|
||||
max-width: 30px
|
||||
transition(all 0.3s ease)
|
||||
|
||||
.measurementNumber
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
margin-right: 5px
|
||||
padding-top: 10px
|
||||
text-align: center
|
||||
|
||||
.measurementRowContent
|
||||
flex: 1
|
||||
|
||||
.measurementDetails
|
||||
padding: 5px 2px 0 14px
|
||||
line-height: 30px
|
||||
font-size: 14px
|
||||
|
||||
.location
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-weight: 400
|
||||
margin-left: -2px
|
||||
|
||||
.value
|
||||
theme('color', '$textPrimaryColor')
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<template name="measurementLightTableView">
|
||||
{{#let config=measurementConfiguration groups=measurementGroups.get}}
|
||||
{{#each measurementGroup in groups}}
|
||||
{{>measurementLightTableHeaderRow (clone this
|
||||
toolGroup=measurementGroup.toolGroup
|
||||
measurementRows=measurementGroup.measurementRows)}}
|
||||
{{#each rowItem in measurementGroup.measurementRows}}
|
||||
{{>measurementLightTableRow (clone this rowItem=rowItem)}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{#if hasAnyMeasurement}}
|
||||
<div class="report-area">
|
||||
{{#if saveEnabled}}
|
||||
{{#if hasUnsavedChanges}}
|
||||
<div class="unsaved-changes-alert">
|
||||
You have unsaved changes
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#form (extend api=instance.api)}}
|
||||
{{#button class='btn p-x-2' action='exportCSV'}}Export CSV{{/button}}
|
||||
{{#if saveEnabled}}
|
||||
{{#button class='btn p-x-2' action='save' disabled=(not hasUnsavedChanges)}}Save to Archive{{/button}}
|
||||
{{/if}}
|
||||
{{/form}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.measurementLightTableView.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { measurementApi, timepointApi } = instance.data;
|
||||
|
||||
instance.data.measurementGroups = new ReactiveVar();
|
||||
|
||||
instance.path = 'viewer.studyViewer.measurements';
|
||||
instance.saveObserver = new Tracker.Dependency();
|
||||
|
||||
instance.api = {
|
||||
save() {
|
||||
|
||||
const successHandler = () => {
|
||||
OHIF.ui.unsavedChanges.clear(`${instance.path}.*`);
|
||||
instance.saveObserver.changed();
|
||||
};
|
||||
|
||||
// Display the error messages
|
||||
const errorHandler = data => {
|
||||
OHIF.ui.showDialog('dialogInfo', Object.assign({ class: 'themed' }, data));
|
||||
};
|
||||
|
||||
const promise = instance.data.measurementApi.storeMeasurements();
|
||||
promise.then(successHandler).catch(errorHandler);
|
||||
OHIF.ui.showDialog('dialogLoading', {
|
||||
promise,
|
||||
text: 'Measurements saved.'
|
||||
});
|
||||
|
||||
return promise;
|
||||
},
|
||||
exportCSV() {
|
||||
const { measurementApi, timepointApi } = instance.data;
|
||||
OHIF.measurements.exportCSV(measurementApi, timepointApi);
|
||||
}
|
||||
};
|
||||
|
||||
instance.autorun(() => {
|
||||
measurementApi.changeObserver.depend();
|
||||
const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi);
|
||||
instance.data.measurementGroups.set(data);
|
||||
});
|
||||
});
|
||||
|
||||
Template.measurementLightTableView.helpers({
|
||||
hasUnsavedChanges() {
|
||||
const instance = Template.instance();
|
||||
// Run this computation on save or every time any measurement / timepoint suffer changes
|
||||
OHIF.ui.unsavedChanges.depend();
|
||||
instance.saveObserver.depend();
|
||||
|
||||
return OHIF.ui.unsavedChanges.probe('viewer.*') !== 0;
|
||||
},
|
||||
|
||||
hasAnyMeasurement() {
|
||||
const instance = Template.instance();
|
||||
const groups = instance.data.measurementGroups.get();
|
||||
|
||||
if (!groups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const group = groups.find(item => item.measurementRows.length > 0);
|
||||
return group;
|
||||
},
|
||||
|
||||
saveEnabled() {
|
||||
const server = OHIF.servers.getCurrentServer();
|
||||
return (server && server.type === 'dicomWeb');
|
||||
}
|
||||
});
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measurementLightTableView
|
||||
.report-area
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
margin-top: 2px
|
||||
padding: 10px 0
|
||||
text-align: center
|
||||
|
||||
.btn
|
||||
theme('background-color', '$activeColor')
|
||||
theme('border', '1px solid $uiBorderColorActive')
|
||||
color: #000
|
||||
|
||||
.unsaved-changes-alert
|
||||
padding-bottom: 10px;
|
||||
font-size: 11px;
|
||||
color: #FF0;
|
||||
font-weight: 100;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<template name="measurementRelabel">
|
||||
<div class="measurement-relabel {{#if this.threeColumns}}tree-columns{{/if}} {{instance.state.get}}" tabindex="-1">
|
||||
</div>
|
||||
</template>
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Blaze } from 'meteor/blaze';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import $ from 'jquery';
|
||||
import {
|
||||
segmentedTerminologyList,
|
||||
segmentedTerminologyCommonList } from '../../../lib/getLabelTerminologyList';
|
||||
|
||||
Template.measurementRelabel.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.value = instance.data.currentValue || '';
|
||||
|
||||
instance.state = new ReactiveVar('closed');
|
||||
|
||||
instance.items = segmentedTerminologyList;
|
||||
instance.commonItems = segmentedTerminologyCommonList;
|
||||
});
|
||||
|
||||
Template.measurementRelabel.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const $measurementRelabel = instance.$('.measurement-relabel');
|
||||
|
||||
// Make the measure flow bounded by the window borders
|
||||
$measurementRelabel.bounded();
|
||||
|
||||
// Wait template rerender before rendering the selectTree
|
||||
Tracker.afterFlush(() => {
|
||||
// Get the click or rendering position
|
||||
const position = {
|
||||
left: event.clientX || instance.data.position.x,
|
||||
top: event.clientY || instance.data.position.y,
|
||||
};
|
||||
|
||||
// Define the data for selectTreeComponent
|
||||
const data = {
|
||||
key: 'label',
|
||||
items: instance.items,
|
||||
commonItems: instance.commonItems,
|
||||
hideCommon: instance.data.hideCommon,
|
||||
label: 'Assign label',
|
||||
search: true,
|
||||
searchPlaceholder: 'Search labels',
|
||||
threeColumns: instance.data.threeColumns,
|
||||
position
|
||||
};
|
||||
|
||||
// Define in which element the selectTree will be rendered in
|
||||
const parentElement = $measurementRelabel[0];
|
||||
|
||||
// Render the selectTree element
|
||||
instance.selectTreeView = Blaze.renderWithData(Template.selectTree, data, parentElement);
|
||||
|
||||
// Focus the measure flow to handle closing
|
||||
$measurementRelabel.focus();
|
||||
});
|
||||
});
|
||||
|
||||
Template.measurementRelabel.events({
|
||||
'click, mousedown, mouseup'(event, instance) {
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
'click .tree-leaf input'(event, instance) {
|
||||
const $target = $(event.currentTarget);
|
||||
const $measureFlow = instance.$('.measurement-relabel');
|
||||
instance.state.set('selected');
|
||||
|
||||
instance.value = $target.data('component').value();
|
||||
instance.data.updateCallback(instance.value);
|
||||
|
||||
$measureFlow.trigger('close')
|
||||
},
|
||||
|
||||
'blur .measurement-relabel'(event, instance) {
|
||||
const $measurementRelabel = $(event.currentTarget);
|
||||
const element = $measurementRelabel[0];
|
||||
Meteor.defer(() => {
|
||||
const focused = $(':focus')[0];
|
||||
if (element !== focused && !$.contains(element, focused)) {
|
||||
$measurementRelabel.trigger('close');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'mouseleave .measurement-relabel'(event, instance) {
|
||||
const $measurementRelabel = $(event.currentTarget);
|
||||
const canClose = instance.state.get() === 'selected';
|
||||
if (canClose && !$.contains($measurementRelabel[0], event.toElement)) {
|
||||
$measurementRelabel.trigger('close');
|
||||
}
|
||||
},
|
||||
|
||||
'mouseenter .measurement-relabel'(event, instance) {
|
||||
// Prevent from closing if user go out and in too fast
|
||||
clearTimeout(instance.closingTimeout);
|
||||
$(event.currentTarget).off('animationend').removeClass('fadeOut');
|
||||
},
|
||||
|
||||
'close .measurement-relabel'(event, instance) {
|
||||
// Clear the timeout to prevent executing the close process twice
|
||||
clearTimeout(instance.closingTimeout);
|
||||
instance.data.doneCallback();
|
||||
}
|
||||
});
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measurement-relabel
|
||||
outline: none
|
||||
position: fixed
|
||||
z-index: 1
|
||||
|
||||
&.fadeOut
|
||||
animateFadeOut()
|
||||
|
||||
.select-tree-root>.tree-content
|
||||
width: 140px
|
||||
|
||||
&>.tree-options
|
||||
position: relative
|
||||
z-index: 2
|
||||
|
||||
.tree-inputs
|
||||
position: relative
|
||||
|
||||
&>.tree-leaf
|
||||
position: relative
|
||||
|
||||
.icon-check
|
||||
theme('background-color', '$activeColor')
|
||||
animateZoomIn()
|
||||
left: -46px
|
||||
position: absolute
|
||||
top: 3px
|
||||
|
||||
span
|
||||
background: white
|
||||
font-weight: normal
|
||||
height: 46px
|
||||
line-height: 46px
|
||||
padding: 0 12px
|
||||
|
||||
input, span
|
||||
display: none
|
||||
|
||||
.actions
|
||||
padding-top: 16px
|
||||
opacity: 0
|
||||
|
||||
&:not(.fadeOut)
|
||||
animateFadeIn()
|
||||
animation-delay: 0.3s
|
||||
|
||||
&.fadeOut
|
||||
animateFadeOut()
|
||||
|
||||
&.selected>.tree-leaf
|
||||
|
||||
span
|
||||
display: block
|
||||
|
||||
&.tree-columns
|
||||
|
||||
.select-tree-root
|
||||
|
||||
&.navigated .select-tree-common .content
|
||||
animation-name: none
|
||||
|
||||
&.selected .select-tree-common .content
|
||||
animation-name: selectTreeCommonCloseLeft
|
||||
|
||||
.select-tree-common
|
||||
padding-left: 0
|
||||
padding-right: 6px
|
||||
right: auto
|
||||
left: 0
|
||||
width: 180px
|
||||
|
||||
&>.tree-content
|
||||
transform-origin(0% 50%)
|
||||
width: 280px
|
||||
|
||||
.select-tree-common .content
|
||||
right: -180px
|
||||
|
||||
&.started .select-tree-common .content
|
||||
right: 46px
|
||||
|
||||
&.navigated .select-tree-common .content
|
||||
animation-name: selectTreeCommonCloseRight
|
||||
|
||||
.tree-inputs>label
|
||||
float: left
|
||||
width: 50%
|
||||
|
||||
@keyframes selectTreeCommonCloseRight {
|
||||
from {
|
||||
height: 37px
|
||||
display: table
|
||||
right: 6px
|
||||
opacity: 1
|
||||
visibility: visible
|
||||
}
|
||||
to {
|
||||
height: 100%
|
||||
right: -100%
|
||||
opacity: 0
|
||||
visibility: hidden
|
||||
width: calc(100% - 6px)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Measurement Table Components imports
|
||||
import './measurementTable.html';
|
||||
import './measurementTable.styl';
|
||||
import './measurementTable.js';
|
||||
|
||||
import './measurementTableView/measurementTableView.html';
|
||||
import './measurementTableView/measurementTableView.styl';
|
||||
import './measurementTableView/measurementTableView.js';
|
||||
|
||||
import './measurementTableHUD/measurementTableHUD.html';
|
||||
import './measurementTableHUD/measurementTableHUD.styl';
|
||||
import './measurementTableHUD/measurementTableHUD.js';
|
||||
|
||||
import './measurementTableRow/measurementTableRow.html';
|
||||
import './measurementTableRow/measurementTableRow.styl';
|
||||
import './measurementTableRow/measurementTableRow.js';
|
||||
|
||||
import './measurementTableHeaderRow/measurementTableHeaderRow.html';
|
||||
import './measurementTableHeaderRow/measurementTableHeaderRow.styl';
|
||||
import './measurementTableHeaderRow/measurementTableHeaderRow.js';
|
||||
|
||||
import './measurementTableTimepointCell/measurementTableTimepointCell.html';
|
||||
import './measurementTableTimepointCell/measurementTableTimepointCell.styl';
|
||||
import './measurementTableTimepointCell/measurementTableTimepointCell.js';
|
||||
|
||||
import './measurementTableTimepointHeader/measurementTableTimepointHeader.html';
|
||||
import './measurementTableTimepointHeader/measurementTableTimepointHeader.styl';
|
||||
import './measurementTableTimepointHeader/measurementTableTimepointHeader.js';
|
||||
|
||||
import './measurementTableWarnings/measurementTableWarningsDialog.html';
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<template name="measurementTable">
|
||||
<div id="measurementTableContainer" class="flex-v">
|
||||
<div class="measurementTableHeader">
|
||||
{{#if this.timepoints.get.length}}
|
||||
<div class="measurementTableLayoutChanger viewerRoundedButtonGroup">
|
||||
{{>roundedButtonGroup buttonGroupData}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="measurementTableTimepointHeaderRow">
|
||||
<div class="warning-status {{#unless hasWarnings}}hidden{{/unless}}">
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-warning"}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
{{#each timepoint in this.timepoints.get}}
|
||||
{{>measurementTableTimepointHeader (clone this timepoint)}}
|
||||
{{/each}}
|
||||
<!-- TODO: Add header for cross-sectional / non-longitudinal data -->
|
||||
</div>
|
||||
</div>
|
||||
{{#scrollArea class='measurementTableView flex-grow fit' scrollStep=63}}
|
||||
{{>measurementTableView (clone this)}}
|
||||
{{/scrollArea}}
|
||||
</div>
|
||||
</template>
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import _ from 'underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.measurementTable.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.measurementTableLayout = new ReactiveVar('comparison');
|
||||
instance.data.timepoints = new ReactiveVar([]);
|
||||
|
||||
// Run this computation every time table layout changes
|
||||
instance.autorun(() => {
|
||||
// Get the current table layout
|
||||
const tableLayout = instance.data.measurementTableLayout.get();
|
||||
|
||||
const timepointApi = instance.data.timepointApi;
|
||||
let timepoints;
|
||||
if (!timepointApi) {
|
||||
timepoints = [];
|
||||
} else if (tableLayout === 'key') {
|
||||
timepoints = timepointApi.key();
|
||||
} else {
|
||||
timepoints = timepointApi.comparison();
|
||||
}
|
||||
|
||||
// Return key timepoints
|
||||
instance.data.timepoints.set(timepoints);
|
||||
});
|
||||
});
|
||||
|
||||
Template.measurementTable.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.autorun(() => {
|
||||
// Run this computation every time the lesion table layout is changed
|
||||
instance.data.measurementTableLayout.dep.depend();
|
||||
|
||||
if (instance.data.state.get('rightSidebar') !== 'measurements') {
|
||||
// Remove the amount attribute from sidebar element tag
|
||||
instance.$('#measurementTableContainer').closest('.sidebarMenu').removeAttr('data-timepoints');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the amount of timepoints being shown
|
||||
const timepointAmount = instance.data.timepoints.get().length;
|
||||
|
||||
// Set the amount in an attribute on sidebar element tag
|
||||
instance.$('#measurementTableContainer').closest('.sidebarMenu').attr('data-timepoints', timepointAmount);
|
||||
});
|
||||
});
|
||||
|
||||
Template.measurementTable.helpers({
|
||||
hasWarnings() {
|
||||
return Template.instance().data.conformanceCriteria.nonconformities.get();
|
||||
},
|
||||
|
||||
buttonGroupData() {
|
||||
const instance = Template.instance();
|
||||
return {
|
||||
value: instance.data.measurementTableLayout,
|
||||
options: [{
|
||||
value: 'comparison',
|
||||
text: 'Comparison'
|
||||
}, {
|
||||
value: 'key',
|
||||
text: 'Key Timepoints'
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTable.events({
|
||||
'click .warning-status'(event, instance) {
|
||||
const nonconformities = instance.data.conformanceCriteria.nonconformities.get();
|
||||
const messages = [];
|
||||
_.each(nonconformities, nonconformity => messages.push(nonconformity.message));
|
||||
|
||||
OHIF.ui.showDialog('measurementTableWarningsDialog', {
|
||||
messages,
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
#measurementTableContainer
|
||||
theme('background-color', '$primaryBackgroundColor')
|
||||
height: 100%
|
||||
width: 100%
|
||||
|
||||
.measurementTableTimepointHeaderRow
|
||||
display: flex
|
||||
padding: 0 2px 0 44px
|
||||
position: relative
|
||||
|
||||
.warning-status
|
||||
theme('border', '2px solid $uiYellow')
|
||||
border-radius: 16px
|
||||
cursor: pointer
|
||||
height: 32px
|
||||
left: 0
|
||||
margin: 4px 6px
|
||||
padding-top: 1px
|
||||
position: absolute
|
||||
text-align: center
|
||||
top: 0
|
||||
transition(border-color 0.3s ease)
|
||||
width: 32px
|
||||
|
||||
&:hover
|
||||
theme('border-color', '$hoverColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$hoverColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$uiYellow')
|
||||
display: inline
|
||||
height: 20px
|
||||
transition(all 0.3s ease)
|
||||
width: 22px
|
||||
|
||||
.measurementTableLayoutChanger
|
||||
margin: 0 auto
|
||||
padding: 20px
|
||||
text-align: center
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<template name="measurementTableHUD">
|
||||
<div id="measurementTableHUD" class="{{hudHidden}}">
|
||||
<div class="flex-v full-height">
|
||||
<div class="header">
|
||||
<span>Measurements</span>
|
||||
<svg class="buttonClose">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
{{#scrollArea class='measurementTableView flex-grow fit' scrollStep=63}}
|
||||
{{>measurementTableView (clone this isHud=true)}}
|
||||
{{/scrollArea}}
|
||||
<div class="footer">
|
||||
{{#each toolbarButton in toolbarButtons}}
|
||||
{{>toolbarSectionButton toolbarButton}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Session } from 'meteor/session';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
Template.measurementTableHUD.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const timepointApi = instance.data.timepointApi;
|
||||
|
||||
instance.isRemoved = true;
|
||||
if (timepointApi) {
|
||||
instance.data.timepoints = new ReactiveVar(timepointApi.currentAndPrior());
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTableHUD.onDestroyed(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.isRemoved = true;
|
||||
Session.set('measurementTableHudOpen', false);
|
||||
});
|
||||
|
||||
Template.measurementTableHUD.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.$('#measurementTableHUD').resizable().draggable().bounded();
|
||||
});
|
||||
|
||||
Template.measurementTableHUD.events({
|
||||
'click .buttonClose'(event, instance) {
|
||||
Session.set('measurementTableHudOpen', false);
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTableHUD.helpers({
|
||||
hudHidden() {
|
||||
let instance = Template.instance(),
|
||||
isOpen = Session.get('measurementTableHudOpen');
|
||||
|
||||
if (isOpen) {
|
||||
instance.isRemoved = false;
|
||||
return 'dialog-animated dialog-open';
|
||||
}
|
||||
|
||||
return instance.isRemoved !== true ? 'dialog-animated dialog-closed' : 'hidden';
|
||||
},
|
||||
|
||||
toolbarButtons() {
|
||||
let buttonData = [];
|
||||
|
||||
buttonData.push({
|
||||
id: 'bidirectional',
|
||||
title: 'Target',
|
||||
classes: 'imageViewerTool toolbarSectionButton',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-target'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'nonTarget',
|
||||
title: 'Non-Target',
|
||||
classes: 'imageViewerTool toolbarSectionButton',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-non-target'
|
||||
});
|
||||
|
||||
buttonData.push({
|
||||
id: 'length',
|
||||
title: 'Temp',
|
||||
classes: 'imageViewerTool toolbarSectionButton',
|
||||
svgLink: '/packages/ohif_viewerbase/assets/icons.svg#icon-tools-measure-temp'
|
||||
});
|
||||
|
||||
return buttonData;
|
||||
}
|
||||
});
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
$borderColor = rgba(77, 99, 110, 0.81)
|
||||
|
||||
#measurementTableHUD
|
||||
background: rgba(0, 0, 0, 0.95)
|
||||
border-radius: 8px
|
||||
border: solid 1px $borderColor;
|
||||
bottom: 3px
|
||||
height: 326px
|
||||
left: auto
|
||||
overflow: hidden
|
||||
position: absolute
|
||||
right: 3px
|
||||
top: auto
|
||||
width: 318px
|
||||
z-index: 1
|
||||
|
||||
.measurementTableView .scrollable
|
||||
margin-left: 0
|
||||
padding-left: 0
|
||||
|
||||
.header
|
||||
theme('background', '$uiGrayDarkest')
|
||||
theme('color', '$textSecondaryColor')
|
||||
border-bottom: $uiBorderThickness solid $borderColor
|
||||
font-weight: 300
|
||||
font-size: 20px
|
||||
height: 55px
|
||||
line-height: 55px
|
||||
position: relative
|
||||
text-align: center
|
||||
width: 100%
|
||||
|
||||
svg.buttonClose
|
||||
theme('color', '$defaultColor')
|
||||
cursor: pointer
|
||||
height: 14px
|
||||
position: absolute
|
||||
right: 14px
|
||||
theme('stroke', '$defaultColor')
|
||||
top: 20px
|
||||
width: 14px
|
||||
|
||||
.measurementTableView
|
||||
height: calc(100% - 55px - 70px)
|
||||
padding-bottom: 20px
|
||||
|
||||
.footer
|
||||
theme('background', '$uiGrayDarkest')
|
||||
border-top: $uiBorderThickness solid $borderColor
|
||||
height: 70px
|
||||
padding: 10px
|
||||
|
||||
.toolbarSectionButton
|
||||
display: inline-block
|
||||
theme('color', '$defaultColor')
|
||||
theme('fill', '$defaultColor')
|
||||
theme('stroke', '$defaultColor')
|
||||
padding: 0 10px
|
||||
height: $toolbarHeight
|
||||
min-width: 30px
|
||||
cursor: pointer
|
||||
text-align: center
|
||||
|
||||
&.disabled
|
||||
opacity: 0.5
|
||||
cursor: not-allowed
|
||||
|
||||
.buttonLabel
|
||||
font-size: 12px
|
||||
|
||||
.svgContainer
|
||||
margin: 0 auto
|
||||
text-align: center
|
||||
|
||||
svg
|
||||
background-color: transparent
|
||||
margin: 2px
|
||||
width: 21px
|
||||
height: 21px
|
||||
|
||||
&:active, &.active
|
||||
theme('color', '$activeColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$activeColor')
|
||||
theme('stroke', '$activeColor')
|
||||
|
||||
&:hover
|
||||
theme('color', '$hoverColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$hoverColor')
|
||||
theme('stroke', '$hoverColor')
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
<template name="measurementTableHeaderRow">
|
||||
<div class="measurementTableHeaderRow">
|
||||
<div class="type">{{toolGroup.name}}</div>
|
||||
<div class="max {{#if gt numberOfMeasurements maxNumMeasurements}}warning{{/if}}">
|
||||
{{#if getMax toolGroup.id}}
|
||||
<p class="maxNumMeasurements">Max {{getMax toolGroup.id}}</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="numberOfMeasurements">{{numberOfMeasurements toolGroup.id}}</div>
|
||||
</div>
|
||||
</template>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Viewerbase } from 'meteor/ohif:viewerbase';
|
||||
|
||||
Template.measurementTableHeaderRow.helpers({
|
||||
numberOfMeasurements(toolGroupId) {
|
||||
const { toolGroup, measurementRows } = Template.instance().data;
|
||||
if (toolGroup.id === 'newTargets') {
|
||||
let result = 0;
|
||||
|
||||
measurementRows.forEach(measurementRow => {
|
||||
const measurementData = measurementRow.entries[0];
|
||||
if (measurementData.isSplitLesion) return;
|
||||
result++;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return measurementRows.length ? measurementRows.length : null;
|
||||
},
|
||||
|
||||
getMax(toolGroupId) {
|
||||
const { conformanceCriteria } = Template.instance().data;
|
||||
if (!conformanceCriteria) return;
|
||||
|
||||
if (toolGroupId === 'targets') {
|
||||
return conformanceCriteria.maxTargets.get();
|
||||
} else if (toolGroupId === 'newTargets') {
|
||||
return conformanceCriteria.maxNewTargets.get();
|
||||
}
|
||||
}
|
||||
});
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
$headerRowHeight = 63px
|
||||
|
||||
.measurementTableHeaderRow
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
theme('color', '$textSecondaryColor')
|
||||
display: flex
|
||||
theme('fill', '$textSecondaryColor')
|
||||
height: $headerRowHeight
|
||||
line-height: $headerRowHeight
|
||||
margin-top: 2px
|
||||
width: 100%
|
||||
|
||||
&.inactive .add
|
||||
cursor: not-allowed
|
||||
|
||||
svg
|
||||
fill: #5A666D
|
||||
|
||||
div
|
||||
align-items: stretch
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
text-align: center
|
||||
|
||||
.add
|
||||
cursor: pointer
|
||||
max-width: 30px
|
||||
padding-left: 2px
|
||||
|
||||
svg
|
||||
fill: #C1D8E3
|
||||
height: $headerRowHeight
|
||||
max-width: 11px
|
||||
|
||||
&:hover svg
|
||||
theme('fill', '$hoverColor')
|
||||
|
||||
&:active svg
|
||||
theme('fill', '$activeColor')
|
||||
|
||||
.type
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 22px
|
||||
font-weight: 300
|
||||
line-height: $headerRowHeight
|
||||
padding: 0 10px 0 20px
|
||||
text-align: left
|
||||
|
||||
.numberOfMeasurements
|
||||
theme('color', '$uiSkyBlue')
|
||||
float: right
|
||||
font-weight: 300
|
||||
font-size: 40px
|
||||
max-width: 54px
|
||||
height: $headerRowHeight
|
||||
line-height: 66px
|
||||
|
||||
.max
|
||||
height: $headerRowHeight
|
||||
max-width: 64px
|
||||
text-align: right
|
||||
|
||||
.maxNumMeasurements
|
||||
theme('background-color', '$textSecondaryColor')
|
||||
border-radius: 3px
|
||||
color: black
|
||||
display: table
|
||||
font-size: 12px
|
||||
font-weight: 500
|
||||
height: 19px
|
||||
line-height: 17px
|
||||
margin-left: auto
|
||||
margin-top: 22px
|
||||
padding: 2px 6px 0
|
||||
text-transform: uppercase
|
||||
transition($sidebarTransition)
|
||||
|
||||
&.warning
|
||||
.maxNumMeasurements
|
||||
theme('background-color', '$uiYellow')
|
||||
theme('color', '$textPrimaryColor')
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<template name="measurementTableRow">
|
||||
<div class="measurementTableRow {{#if hasWarnings}}warning{{/if}}{{#if responseStatus}}response-status{{/if}}" data-measurementid="{{_id}}">
|
||||
<div class='measurementRowSidebar'>
|
||||
<div class="measurementNumber">
|
||||
{{rowItem.measurementNumber}}
|
||||
</div>
|
||||
{{#if hasWarnings}}
|
||||
<div class="warning-icon">
|
||||
<svg>
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-warning"}}></use>
|
||||
</svg>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if rowItem.responseStatus}}
|
||||
<div class="response-status-icon">
|
||||
{{rowItem.responseStatus}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="measurementDetails">
|
||||
<div class='location'>
|
||||
{{#if rowItem.location}}
|
||||
{{getLocationLabel rowItem.location}}
|
||||
{{else}}
|
||||
(No description)
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="timepointData">
|
||||
<!--Each time point as a column-->
|
||||
{{#each timepoint in timepoints.get}}
|
||||
{{>measurementTableTimepointCell (clone this timepoint)}}
|
||||
{{else}}
|
||||
{{>measurementTableTimepointCell}}
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="rowOptions">
|
||||
<div class="rowAction js-rename pull-left m-r-2" tabindex="1">
|
||||
<svg class="edit-icon">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-measurements-additional"}}></use>
|
||||
</svg>
|
||||
Relabel
|
||||
</div>
|
||||
<div class="rowAction js-delete pull-left" tabindex="1">
|
||||
<svg class="close-icon">
|
||||
<use xlink:href={{absoluteUrl "packages/ohif_viewerbase/assets/icons.svg#icon-ui-close"}}></use>
|
||||
</svg>
|
||||
Delete
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
Template.measurementTableRow.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.getWarningMessages = () => {
|
||||
const measurementTypeId = instance.data.rowItem.measurementTypeId;
|
||||
const measurementNumber = instance.data.rowItem.measurementNumber;
|
||||
const groupedNonConformities = instance.data.conformanceCriteria.groupedNonConformities.get() || {};
|
||||
const nonconformitiesByMeasurementTypeId = groupedNonConformities[measurementTypeId] || {};
|
||||
const nonconformitiesByMeasurementNumbers = nonconformitiesByMeasurementTypeId.measurementNumbers || {};
|
||||
const nonconformitiesByMeasurementNumber = nonconformitiesByMeasurementNumbers[measurementNumber] || {};
|
||||
|
||||
return _.uniq(nonconformitiesByMeasurementNumber.messages || []);
|
||||
};
|
||||
});
|
||||
|
||||
Template.measurementTableRow.helpers({
|
||||
hasWarnings() {
|
||||
return !!Template.instance().getWarningMessages().length;
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTableRow.events({
|
||||
'click .measurementRowSidebar .warning-icon'(event, instance) {
|
||||
event.stopPropagation();
|
||||
OHIF.ui.showDialog('measurementTableWarningsDialog', {
|
||||
messages: instance.getWarningMessages(),
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'click .measurementRowSidebar'(event, instance) {
|
||||
const $row = instance.$('.measurementTableRow');
|
||||
const rowItem = instance.data.rowItem;
|
||||
const timepoints = instance.data.timepoints.get();
|
||||
|
||||
$row.closest('.measurementTableView').find('.measurementTableRow').not($row).removeClass('active');
|
||||
$row.toggleClass('active');
|
||||
|
||||
const childToolKey = $(event.target).attr('data-child');
|
||||
OHIF.measurements.jumpToRowItem(rowItem, timepoints, childToolKey);
|
||||
},
|
||||
|
||||
'click .js-rename'(event, instance) {
|
||||
const rowItem = instance.data.rowItem;
|
||||
const entry = rowItem.entries[0];
|
||||
|
||||
// Show the measure flow for targets
|
||||
OHIF.measurements.toggleLabelButton({
|
||||
measurement: entry,
|
||||
element: document.body,
|
||||
measurementApi: instance.data.measurementApi,
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
},
|
||||
autoClick: true
|
||||
});
|
||||
},
|
||||
|
||||
'click .js-delete'(event, instance) {
|
||||
const dialogSettings = {
|
||||
class: 'themed',
|
||||
title: 'Delete measurements',
|
||||
message: 'Are you sure you want to delete the measurement across all timepoints?',
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(formData => {
|
||||
const measurementTypeId = instance.data.rowItem.measurementTypeId;
|
||||
const measurement = instance.data.rowItem.entries[0];
|
||||
const measurementNumber = measurement.measurementNumber;
|
||||
const { timepointApi, measurementApi } = instance.data;
|
||||
|
||||
// Remove all the measurements with the given type and number
|
||||
measurementApi.deleteMeasurements(measurementTypeId, { measurementNumber });
|
||||
|
||||
// Sync the new measurement data with cornerstone tools
|
||||
const baseline = timepointApi.baseline();
|
||||
measurementApi.sortMeasurements(baseline.timepointId);
|
||||
|
||||
// Repaint the images on all viewports without the removed measurements
|
||||
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
|
||||
|
||||
// Notify that viewer suffered changes
|
||||
OHIF.measurements.triggerTimepointUnsavedChanges('deleted');
|
||||
});
|
||||
}
|
||||
});
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measurementTableRow
|
||||
display: flex
|
||||
margin-left: -6px
|
||||
margin-top: 2px
|
||||
padding-left: 6px
|
||||
// required transformation to make inner fixed elements relative to this one
|
||||
transform(scale(1))
|
||||
width: calc(100% + 6px)
|
||||
|
||||
&.active
|
||||
|
||||
.measurementRowSidebar
|
||||
theme('color', '$activeColor')
|
||||
|
||||
.rowOptions
|
||||
height: 35px
|
||||
visibility: visible
|
||||
|
||||
&.response-status .measurementRowSidebar .response-status-icon
|
||||
theme('background-color', '$defaultColor')
|
||||
theme('color', '$uiGray')
|
||||
border-radius: 11px
|
||||
font-size: 12px
|
||||
font-weight: 700
|
||||
height: 21px
|
||||
line-height: 22px
|
||||
margin: 5px auto 0
|
||||
text-align: center
|
||||
width: 21px
|
||||
|
||||
&.warning .measurementRowSidebar
|
||||
theme('background-color', '$uiYellow')
|
||||
theme('color', '$textPrimaryColor')
|
||||
|
||||
.warning-icon, svg
|
||||
width: 22px
|
||||
height: 20px
|
||||
pointer-events: inherit
|
||||
|
||||
.warning-icon
|
||||
margin: 7px auto 0
|
||||
|
||||
svg
|
||||
theme('fill', '$textPrimaryColor')
|
||||
|
||||
.rowOptions
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
height: 0
|
||||
overflow: hidden
|
||||
transition(all 0.3s ease)
|
||||
visibility: hidden
|
||||
|
||||
.rowAction
|
||||
theme('color', '$defaultColor')
|
||||
cursor: pointer
|
||||
line-height: 35px
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&:hover, &:active
|
||||
theme('color', '$textPrimaryColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$textPrimaryColor')
|
||||
theme('stroke', '$textPrimaryColor')
|
||||
|
||||
svg
|
||||
theme('fill', '$defaultColor')
|
||||
theme('stroke', '$defaultColor')
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&.edit-icon
|
||||
width: 14px
|
||||
height: 13px
|
||||
|
||||
&.close-icon
|
||||
width: 11px
|
||||
height: 11px
|
||||
|
||||
.measurementRowSidebar
|
||||
theme('background', '$uiGray')
|
||||
theme('color', '$textSecondaryColor')
|
||||
cursor: pointer
|
||||
flex: 1
|
||||
max-width: 30px
|
||||
transition(all 0.3s ease)
|
||||
|
||||
.measurementNumber
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
margin-right: 5px
|
||||
padding-top: 10px
|
||||
text-align: center
|
||||
|
||||
.measurementDetails
|
||||
flex: 1
|
||||
padding: 5px 2px 0 0
|
||||
|
||||
&>*
|
||||
padding-left: 14px
|
||||
|
||||
.location
|
||||
font-weight: 400
|
||||
font-size: 14px
|
||||
theme('color', '$textSecondaryColor')
|
||||
height: 30px
|
||||
line-height: 30px
|
||||
margin-left: -2px
|
||||
width: 100%
|
||||
|
||||
.timepointData
|
||||
display: flex
|
||||
min-height: 27px
|
||||
margin-top: 1px
|
||||
|
||||
div
|
||||
flex: 1
|
||||
font-size: 14px
|
||||
font-weight: 400
|
||||
justify-content: space-around
|
||||
line-height: 18px
|
||||
padding-bottom: 4px
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<template name="measurementTableTimepointCell">
|
||||
{{#if hasDataAtThisTimepoint}}
|
||||
<div class="measurementTableTimepointCell noselect {{#if isLoading}}loading{{/if}}" tabindex="1">
|
||||
<i class="fa fa-spin fa-circle-o-notch fa-fw loading-spinner"></i>
|
||||
{{{displayData}}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="measurementTableTimepointCell noselect empty">
|
||||
...
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone } from 'meteor/ohif:cornerstone';
|
||||
|
||||
Template.measurementTableTimepointCell.helpers({
|
||||
hasDataAtThisTimepoint() {
|
||||
// This simple function just checks whether or not timepoint data
|
||||
// exists for this Measurement at this Timepoint
|
||||
const instance = Template.instance();
|
||||
const { rowItem, timepointId } = instance.data;
|
||||
|
||||
if (timepointId) {
|
||||
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
|
||||
return dataAtThisTimepoint.length > 0;
|
||||
} else {
|
||||
return rowItem.entries.length > 0;
|
||||
}
|
||||
},
|
||||
|
||||
displayData() {
|
||||
const instance = Template.instance();
|
||||
const { rowItem, timepointId } = instance.data;
|
||||
|
||||
let data;
|
||||
if (timepointId) {
|
||||
const dataAtThisTimepoint = _.where(rowItem.entries, { timepointId });
|
||||
if (dataAtThisTimepoint.length > 1) {
|
||||
throw 'More than one measurement was found at the same timepoint with the same measurement number?';
|
||||
}
|
||||
|
||||
data = dataAtThisTimepoint[0];
|
||||
} else {
|
||||
data = rowItem.entries[0];
|
||||
}
|
||||
|
||||
const config = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const measurementTools = config.measurementTools;
|
||||
|
||||
const toolGroup = _.findWhere(measurementTools, { id: rowItem.measurementTypeId });
|
||||
const tool = _.findWhere(toolGroup.childTools, { id: data.toolType });
|
||||
if (!tool) {
|
||||
// TODO: Figure out what is going on here?
|
||||
OHIF.log.warn('Something went wrong?');
|
||||
}
|
||||
|
||||
const { displayFunction } = tool.options.measurementTable;
|
||||
return displayFunction(data);
|
||||
},
|
||||
|
||||
isLoading() {
|
||||
const instance = Template.instance();
|
||||
const { rowItem, timepointId } = instance.data;
|
||||
const { entries } = rowItem;
|
||||
const measurementData = timepointId ? _.findWhere(entries, { timepointId }) : entries[0];
|
||||
const { studyInstanceUid } = measurementData;
|
||||
return OHIF.studies.loadingDict.get(studyInstanceUid) === 'loading';
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTableTimepointCell.events({
|
||||
'dblclick .measurementTableTimepointCell'(event, instance) {
|
||||
const { rowItem, timepointId } = instance.data;
|
||||
if (!timepointId) return;
|
||||
|
||||
const measurementData = _.findWhere(rowItem.entries, { timepointId });
|
||||
if (!measurementData || measurementData.toolType !== 'nonTarget') return;
|
||||
|
||||
const viewportIndex = rowItem.entries.indexOf(measurementData);
|
||||
const $viewports = $('#viewer .imageViewerViewport');
|
||||
let $element = $viewports.eq(viewportIndex);
|
||||
$element = $element.length ? $element : $viewports.eq(0);
|
||||
|
||||
OHIF.ui.showDialog('dialogNonTargetMeasurement', {
|
||||
event,
|
||||
title: 'Change Lesion Location',
|
||||
element: $element[0],
|
||||
measurementData,
|
||||
edit: true
|
||||
});
|
||||
},
|
||||
|
||||
'keydown .measurementTableTimepointCell'(event, instance) {
|
||||
// Delete a lesion if Ctrl+D or DELETE is pressed while a lesion is selected
|
||||
const keys = {
|
||||
D: 68,
|
||||
DELETE: 46
|
||||
};
|
||||
const keyCode = event.which;
|
||||
|
||||
if (keyCode === keys.DELETE || keyCode === keys.BACKSPACE || (keyCode === keys.D && event.ctrlKey === true)) {
|
||||
const timepointId = instance.data.timepointId;
|
||||
|
||||
const offset = $(event.currentTarget).offset();
|
||||
const dialogSettings = {
|
||||
class: 'themed',
|
||||
title: 'Delete measurements',
|
||||
message: 'Are you sure you want to delete this measurement?',
|
||||
position: {
|
||||
x: offset.left,
|
||||
y: offset.top
|
||||
}
|
||||
};
|
||||
|
||||
OHIF.ui.showDialog('dialogConfirm', dialogSettings).then(() => {
|
||||
const measurementTypeId = instance.data.rowItem.measurementTypeId;
|
||||
const measurement = instance.data.rowItem.entries[0];
|
||||
const measurementNumber = measurement.measurementNumber;
|
||||
const { timepointApi, measurementApi } = instance.data;
|
||||
|
||||
// Remove all the measurements with the given type and number
|
||||
measurementApi.deleteMeasurements(measurementTypeId, {
|
||||
measurementNumber,
|
||||
timepointId
|
||||
});
|
||||
|
||||
// Sync the new measurement data with cornerstone tools
|
||||
const baseline = timepointApi.baseline();
|
||||
measurementApi.sortMeasurements(baseline.timepointId);
|
||||
|
||||
// Repaint the images on all viewports without the removed measurements
|
||||
_.each($('.imageViewerViewport'), element => cornerstone.updateImage(element));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
.measurementTableTimepointCell
|
||||
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
cursor: pointer
|
||||
padding: 0 10px
|
||||
position: relative
|
||||
|
||||
.loading-spinner
|
||||
display: none
|
||||
font-size: 16px
|
||||
position: absolute
|
||||
right: 5px
|
||||
top: 3px
|
||||
|
||||
span
|
||||
display: block
|
||||
|
||||
&.loading .loading-spinner
|
||||
display: block
|
||||
|
||||
&, span
|
||||
theme('color', '$textPrimaryColor')
|
||||
|
||||
&:hover, span:hover
|
||||
theme('color', '$activeColor')
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<template name="measurementTableTimepointHeader">
|
||||
<div class="measurementTableTimepointHeader">
|
||||
<div class="timepointName">
|
||||
{{timepointName this}}
|
||||
</div>
|
||||
<div class="timepointDate">
|
||||
{{formatDA latestDate "DD-MMM-YY"}}
|
||||
</div>
|
||||
<div class="case-progress-container">
|
||||
{{>caseProgress}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.measurementTableTimepointHeader.helpers({
|
||||
timepointName(timepoint) {
|
||||
return Template.instance().data.timepointApi.name(timepoint);
|
||||
}
|
||||
});
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
.measurementTableTimepointHeader
|
||||
flex: 1
|
||||
justify-content: space-around
|
||||
padding-top: 2px
|
||||
position: relative
|
||||
|
||||
.timepointName, .timepointDate
|
||||
theme('border-left', '%s solid $uiBorderColor' % $uiBorderThickness)
|
||||
font-weight: 400
|
||||
padding-left: 12px
|
||||
text-align: left
|
||||
|
||||
.timepointName
|
||||
theme('color', '$textSecondaryColor')
|
||||
font-size: 12px
|
||||
line-height: 12px
|
||||
|
||||
.timepointDate
|
||||
theme('color', '$textPrimaryColor')
|
||||
font-size: 14px
|
||||
line-height: 20px
|
||||
padding-bottom: 6px
|
||||
|
||||
.case-progress-container
|
||||
position: absolute
|
||||
right: 4px
|
||||
top: 0
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<template name="measurementTableView">
|
||||
{{#let config=measurementConfiguration groups=measurementGroups.get}}
|
||||
{{#each measurementGroup in groups}}
|
||||
{{! #if hasMeasurements measurementGroup.toolGroup.id}}
|
||||
{{>measurementTableHeaderRow (clone this
|
||||
toolGroup=measurementGroup.toolGroup
|
||||
measurementRows=measurementGroup.measurementRows)}}
|
||||
{{! /if}}
|
||||
{{#each rowItem in measurementGroup.measurementRows}}
|
||||
{{>measurementTableRow (clone this rowItem=rowItem)}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
{{#each newLesionGroup in config.newLesions}}
|
||||
{{#let toolGroup=(getNewLesionsToolGroup newLesionGroup)}}
|
||||
{{#let newMeasurements=(newLesionsMeasurements toolGroup)}}
|
||||
{{#if newMeasurements.length}}
|
||||
{{#let collection=newMeasurements}}
|
||||
{{>measurementTableHeaderRow (clone this
|
||||
toolGroup=toolGroup
|
||||
measurementRows=collection)}}
|
||||
|
||||
{{#each entry in collection}}
|
||||
{{>measurementTableRow (clone this rowItem=entry)}}
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
{{#unless this.isHud}}
|
||||
{{#if or (hasMeasurements 'targets') (hasMeasurements 'nonTargets')}}
|
||||
<div class="report-area">
|
||||
<div class="btn btn-sm js-pdf">Generate Report</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</template>
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import _ from 'underscore';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
|
||||
Template.measurementTableView.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
const { measurementApi, timepointApi } = instance.data;
|
||||
|
||||
instance.data.measurementGroups = new ReactiveVar();
|
||||
|
||||
Tracker.autorun(() => {
|
||||
measurementApi.changeObserver.depend();
|
||||
const data = OHIF.measurements.getMeasurementsGroupedByNumber(measurementApi, timepointApi);
|
||||
instance.data.measurementGroups.set(data);
|
||||
});
|
||||
});
|
||||
|
||||
Template.measurementTableView.events({
|
||||
'click .js-pdf'(event, instance) {
|
||||
const { measurementApi, timepointApi } = instance.data;
|
||||
OHIF.measurements.exportPdf(measurementApi, timepointApi);
|
||||
}
|
||||
});
|
||||
|
||||
Template.measurementTableView.helpers({
|
||||
hasMeasurements(toolGroupId) {
|
||||
const instance = Template.instance();
|
||||
const groups = instance.data.measurementGroups.get();
|
||||
|
||||
if (!groups) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const group = _.find(groups, item => item.toolGroup.id === toolGroupId);
|
||||
return group && !!group.measurementRows.length;
|
||||
},
|
||||
|
||||
getNewLesionsToolGroup(newLesionGroup) {
|
||||
const configuration = OHIF.measurements.MeasurementApi.getConfiguration();
|
||||
const toolGroup = _.findWhere(configuration.measurementTools, { id: newLesionGroup.toolGroupId });
|
||||
|
||||
return {
|
||||
id: newLesionGroup.id,
|
||||
name: newLesionGroup.name,
|
||||
childTools: toolGroup.childTools,
|
||||
measurementTypeId: toolGroup.id
|
||||
};
|
||||
},
|
||||
|
||||
newLesionsMeasurements(toolGroup) {
|
||||
const { measurementApi, timepointApi } = Template.instance().data;
|
||||
const current = timepointApi.current();
|
||||
const baseline = timepointApi.baseline();
|
||||
|
||||
if (!measurementApi || !timepointApi || !current || !baseline) return;
|
||||
|
||||
// If this is a baseline, stop here since there are no new measurements to display
|
||||
if (!current || current.timepointType === 'baseline') {
|
||||
OHIF.log.info('Skipping New Measurements section');
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve all the data for this Measurement type (e.g. 'targets')
|
||||
// which was recorded at baseline.
|
||||
const measurementTypeId = toolGroup.measurementTypeId;
|
||||
const atBaseline = measurementApi.fetch(measurementTypeId, {
|
||||
timepointId: baseline.timepointId
|
||||
});
|
||||
|
||||
// Obtain a list of the Measurement Numbers from the
|
||||
// measurements which have baseline data
|
||||
const numbers = atBaseline.map(m => m.measurementNumber);
|
||||
|
||||
// Retrieve all the data for this Measurement type which
|
||||
// do NOT match the Measurement Numbers obtained above
|
||||
const data = measurementApi.fetch(measurementTypeId, {
|
||||
measurementNumber: { $nin: numbers }
|
||||
});
|
||||
|
||||
// Group the Measurements by Measurement Number
|
||||
const groupObject = _.groupBy(data, entry => entry.measurementNumber);
|
||||
|
||||
// Reformat the data for display in the table
|
||||
return Object.keys(groupObject).map(key => ({
|
||||
measurementTypeId: measurementTypeId,
|
||||
measurementNumber: key,
|
||||
location: OHIF.measurements.getLocation(groupObject[key]),
|
||||
responseStatus: false, // TODO: Get the latest timepoint and determine the response status
|
||||
entries: groupObject[key]
|
||||
}));
|
||||
}
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.measurementTableView
|
||||
|
||||
.report-area
|
||||
theme('background-color', '$uiGrayDarker')
|
||||
margin-top: 2px
|
||||
padding: 10px 0
|
||||
|
||||
.btn.js-pdf
|
||||
theme('background-color', '$activeColor')
|
||||
theme('border', '1px solid $uiBorderColorActive')
|
||||
color: #000
|
||||
display: table
|
||||
margin: 0 auto
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
<template name="measurementTableWarningsDialog">
|
||||
{{#dialogSimple (extend this class='themed' dialogClass='modal-sm' title='Criteria nonconformities')}}
|
||||
<ol>
|
||||
{{#each message in this.messages}}
|
||||
<li>{{message}}</li>
|
||||
{{/each}}
|
||||
</ol>
|
||||
{{/dialogSimple}}
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
// selectTree
|
||||
import './selectTree/selectTree.html';
|
||||
import './selectTree/selectTree.styl';
|
||||
import './selectTree/selectTree.js';
|
||||
|
||||
// selectTreeCommon
|
||||
import './selectTreeCommon/selectTreeCommon.html';
|
||||
import './selectTreeCommon/selectTreeCommon.js';
|
||||
@@ -0,0 +1,91 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import _ from 'underscore';
|
||||
|
||||
/*
|
||||
* input: controls a tree selection component
|
||||
*/
|
||||
OHIF.mixins.selectTree = new OHIF.Mixin({
|
||||
dependencies: 'group',
|
||||
composition: {
|
||||
onCreated() {
|
||||
const instance = Template.instance();
|
||||
const component = instance.component;
|
||||
const rootComponent = instance.data.root || component;
|
||||
const rootInstance = rootComponent.templateInstance;
|
||||
|
||||
// Store the component's current value
|
||||
instance.currentValue = null;
|
||||
|
||||
// Create the component's value storage property
|
||||
instance.data.currentNode = new ReactiveVar(null);
|
||||
|
||||
// Share the component in data property
|
||||
instance.data.component = component;
|
||||
|
||||
// Override the default value method
|
||||
component.value = value => {
|
||||
const isGet = _.isUndefined(value);
|
||||
|
||||
// Return the current value
|
||||
if (isGet) {
|
||||
return component.parseData(rootInstance.currentValue);
|
||||
}
|
||||
|
||||
// Change the current value
|
||||
rootInstance.currentValue = value;
|
||||
|
||||
// Trigger the change event
|
||||
component.$element.trigger('change');
|
||||
};
|
||||
|
||||
// Method to manipulate nodes
|
||||
component.node = node => {
|
||||
const isGet = _.isUndefined(node);
|
||||
|
||||
// Return the current node
|
||||
if (isGet) {
|
||||
const currentNode = instance.data.currentNode.get();
|
||||
return currentNode ? currentNode.value : null;
|
||||
}
|
||||
|
||||
// get the items array
|
||||
const items = component.templateInstance.data.items;
|
||||
|
||||
// Get the node for the selected value
|
||||
const currentNode = _.findWhere(items, {
|
||||
value: node
|
||||
});
|
||||
|
||||
// Change the current node
|
||||
instance.data.currentNode.set(currentNode);
|
||||
};
|
||||
|
||||
// Return plain data for all the leaves inside current node
|
||||
component.getLeaves = () => {
|
||||
const recursiveSeek = (items, result=[]) => {
|
||||
_.each(items, item => {
|
||||
if (item.items) {
|
||||
recursiveSeek(item.items, result);
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
return recursiveSeek(instance.data.items);
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
onRendered() {
|
||||
const instance = Template.instance();
|
||||
const component = instance.component;
|
||||
|
||||
// Define the main element
|
||||
component.$element = instance.$('.select-tree:first').first();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
<template name="selectTree">
|
||||
{{#group (extend this
|
||||
class=(concat
|
||||
'select-tree '
|
||||
(valueIf this.root '' 'select-tree-root ')
|
||||
this.class
|
||||
)
|
||||
mixins=(concat 'selectTree ' this.mixins)
|
||||
)}}
|
||||
{{>UI.contentBlock}}
|
||||
<div class="tree-content">
|
||||
{{#let currentNode=this.currentNode.get}}
|
||||
{{#if (and (not this.root) (ne this.search false))}}
|
||||
{{#inputText
|
||||
label=this.label
|
||||
labelClass='tree-search'
|
||||
placeholder=(choose this.searchPlaceholder 'Search')
|
||||
}}
|
||||
<i class="fa fa-search"></i>
|
||||
{{/inputText}}
|
||||
{{/if}}
|
||||
<div class="tree-options {{#if currentNode}}collapsed{{/if}}">
|
||||
{{#if this.root}}
|
||||
{{>selectTreeBreadcrumb component=this.component}}
|
||||
{{/if}}
|
||||
<div class="tree-inputs clearfix">
|
||||
{{#each item in treeItems}}
|
||||
{{>inputRadio
|
||||
id=(concat this.storageKey '_' (encodeId item.value))
|
||||
class=this.radioClass
|
||||
name=this.key
|
||||
itemData=item
|
||||
value=item.value
|
||||
label=item.label
|
||||
labelClass=(valueIf (isArray item.items) 'tree-node' 'tree-leaf')
|
||||
checked=(eq item.value (reactive this.value))
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{#if (and currentNode (isArray currentNode.items))}}
|
||||
{{>selectTree (clone this
|
||||
items=currentNode.items
|
||||
root=(choose this.root this.component)
|
||||
class='select-tree-node'
|
||||
)}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/let}}
|
||||
</div>
|
||||
{{#if (and (not this.root) (or this.storageKey this.commonItems))}}
|
||||
{{>selectTreeCommon (clone this component=this.component treeInstance=instance)}}
|
||||
{{/if}}
|
||||
{{/group}}
|
||||
</template>
|
||||
|
||||
<template name="selectTreeBreadcrumb">
|
||||
<div class="tree-breadcrumb clearfix">
|
||||
{{>selectTreeBreadcrumbParent
|
||||
component=this.component.parent
|
||||
index=1
|
||||
}}
|
||||
{{#let currentNode=this.component.parent.templateInstance.data.currentNode.get}}
|
||||
<span class="tree-current-node">{{currentNode.label}}</span>
|
||||
{{/let}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template name="selectTreeBreadcrumbParent">
|
||||
{{#let parent=this.component.parent}}
|
||||
{{#let parentNode=(reactive parent.templateInstance.data.currentNode)}}
|
||||
{{#if parentNode}}
|
||||
{{>selectTreeBreadcrumbParent
|
||||
component=parent
|
||||
index=(sum this.index 1)
|
||||
}}
|
||||
{{/if}}
|
||||
<span class="path-link">
|
||||
<a href="#" class="tree-back" data-index="{{this.index}}">
|
||||
{{#if parentNode}}
|
||||
<span>{{parentNode.label}}</span>
|
||||
{{else}}
|
||||
<i class="fa fa-fast-backward"></i>
|
||||
{{/if}}
|
||||
</a>
|
||||
{{#if parentNode}}<span>/</span>{{/if}}
|
||||
</span>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
</template>
|
||||
@@ -0,0 +1,394 @@
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
|
||||
// TODO: use npm dependency
|
||||
import transition from 'meteor/ohif:core/client/lib/third-party/transition-to-from-auto';
|
||||
|
||||
Template.selectTree.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Return the common items
|
||||
instance.getCommonItems = () => {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Return the common items if given
|
||||
if (instance.data.commonItems) {
|
||||
return instance.data.commonItems;
|
||||
}
|
||||
|
||||
// Get all the tree leaves
|
||||
const leaves = instance.data.component.getLeaves();
|
||||
|
||||
// Generate an object with encoded keys from the tree leaves
|
||||
const leavesObject = {};
|
||||
_.each(leaves, leaf => {
|
||||
leavesObject[OHIF.string.encodeId(leaf.value)] = leaf;
|
||||
});
|
||||
|
||||
// Get the current items ranking
|
||||
const ranking = OHIF.user.getData(instance.data.storageKey);
|
||||
|
||||
// Sort the items based on how many times each one was used
|
||||
const sorted = [];
|
||||
_.each(ranking, (count, key) => sorted.push([key, count]));
|
||||
sorted.sort((a, b) => b[1] - a[1]);
|
||||
|
||||
// Create the result and push every item respecting the ranking order
|
||||
const result = [];
|
||||
_.each(sorted, item => {
|
||||
const current = leavesObject[item[0]];
|
||||
if (current) {
|
||||
result.push(current);
|
||||
}
|
||||
});
|
||||
|
||||
// Return the resulting array
|
||||
return result;
|
||||
};
|
||||
|
||||
// Create a reactive variable to control the search
|
||||
instance.searchTerm = new ReactiveVar('');
|
||||
});
|
||||
|
||||
Template.selectTree.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
const component = instance.$('.select-tree:first').data('component');
|
||||
const rootComponent = instance.data.root || component;
|
||||
const rootInstance = rootComponent.templateInstance;
|
||||
const $treeRoot = rootInstance.$('.select-tree-root:first').first();
|
||||
|
||||
instance.component = component;
|
||||
|
||||
// Force to hardware acceleration to move element if browser supports translate property
|
||||
instance.useTransform = OHIF.ui.styleProperty.check('transform', 'translate(1px, 1px)');
|
||||
|
||||
// Set the margin property and width
|
||||
const isthreeColumns = instance.data.threeColumns;
|
||||
if (!rootInstance.marginProperty) {
|
||||
rootInstance.marginProperty = isthreeColumns ? 'margin-left' : 'margin-right';
|
||||
rootInstance.marginWidth = isthreeColumns ? $treeRoot.width() / 2 : $treeRoot.width();
|
||||
}
|
||||
|
||||
// Define a function to set the margin and toggle common section
|
||||
instance.setMargin = isHidden => {
|
||||
const marginWidth = isHidden ? '' : rootInstance.marginWidth;
|
||||
$treeRoot.children('.tree-content').css(rootInstance.marginProperty, marginWidth);
|
||||
};
|
||||
|
||||
// Set the margin to display the common section
|
||||
if (!$treeRoot.hasClass('started')) {
|
||||
instance.setMargin(false);
|
||||
}
|
||||
|
||||
// Make the component respect the window boundaries
|
||||
if (rootComponent === component) {
|
||||
$treeRoot.bounded();
|
||||
}
|
||||
|
||||
// Check if the component will be rendered on a specific position
|
||||
const position = instance.data.position;
|
||||
if (component === rootComponent && position) {
|
||||
// Get the dimensions and move it with the given position as its center
|
||||
const width = $treeRoot.outerWidth();
|
||||
const height = $treeRoot.outerHeight();
|
||||
position.left -= Math.round(width / 2);
|
||||
position.top -= Math.round(height / 2);
|
||||
|
||||
// Change the component's position and trigger the bounded event
|
||||
$treeRoot.css(_.extend({}, position, {
|
||||
position: 'fixed'
|
||||
})).trigger('spatialChanged');
|
||||
}
|
||||
|
||||
Meteor.defer(() => {
|
||||
// Start the component transitions
|
||||
$treeRoot.addClass('started');
|
||||
|
||||
// Focus the search box
|
||||
$treeRoot.find('.tree-search input').focus();
|
||||
});
|
||||
|
||||
// Update the component's viewport height
|
||||
instance.updateHeight = _.throttle(searchTerm => {
|
||||
let height;
|
||||
|
||||
// Check if there's a search term
|
||||
if (searchTerm) {
|
||||
// Set the viewport height as same as listed options height
|
||||
height = rootInstance.$('.tree-inputs').height();
|
||||
} else {
|
||||
// Set the viewport height as same as selected node's options height
|
||||
height = rootInstance.$('.tree-options:last').data('height');
|
||||
}
|
||||
|
||||
// Update the viewport's height
|
||||
rootInstance.$('.tree-options:first').first().height(height);
|
||||
}, 100, { trailing: false });
|
||||
|
||||
// Update the opened node
|
||||
instance.updateOpen = () => {
|
||||
rootInstance.$('.select-tree').removeClass('open');
|
||||
rootInstance.$('.select-tree:last').addClass('open');
|
||||
};
|
||||
|
||||
// Put the component in selected state
|
||||
instance.setSelected = (flag) => {
|
||||
if (flag) {
|
||||
$treeRoot.addClass('selected');
|
||||
} else {
|
||||
$treeRoot.removeClass('selected');
|
||||
}
|
||||
};
|
||||
|
||||
// Store the element's initial height
|
||||
const $treeOptions = instance.$('.tree-options:first').first();
|
||||
const height = $treeOptions.height();
|
||||
$treeOptions.data('height', height);
|
||||
|
||||
$treeOptions.off('transitionend').on('transitionend', event => {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
$treeRoot.trigger('spatialChanged');
|
||||
});
|
||||
|
||||
instance.autorun(() => {
|
||||
// Run this computation everytime the current node is changed
|
||||
instance.data.currentNode.dep.depend();
|
||||
|
||||
// Run this computation everytime the search term is changed
|
||||
const searchTerm = instance.searchTerm.get();
|
||||
|
||||
// Clean the current node selection if the user started the search
|
||||
if (searchTerm) {
|
||||
rootInstance.data.currentNode.set(null);
|
||||
}
|
||||
|
||||
// Update the viewport height
|
||||
Tracker.afterFlush(() => {
|
||||
instance.updateHeight(searchTerm);
|
||||
instance.updateOpen();
|
||||
});
|
||||
});
|
||||
|
||||
const selector = '.select-tree:first>.tree-content>.tree-options>.tree-breadcrumb .path-link';
|
||||
instance.$(selector).css('width', 0).each((index, element) => {
|
||||
transition({
|
||||
element: element,
|
||||
prop: 'width',
|
||||
style: 'width 0.3s ease',
|
||||
val: 'auto'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.selectTree.events({
|
||||
'input .tree-search input'(event, instance) {
|
||||
const $target = $(event.currentTarget);
|
||||
// Get the tree root
|
||||
const $treeRoot = $target.closest('.select-tree-root');
|
||||
|
||||
// Get the search term
|
||||
const searchTerm = $target.val();
|
||||
|
||||
// Change the search term to update the tree items
|
||||
instance.searchTerm.set(searchTerm);
|
||||
|
||||
// Change the component state
|
||||
const method = searchTerm ? 'addClass' : 'removeClass';
|
||||
$treeRoot[method]('navigated');
|
||||
instance.setMargin(!!searchTerm);
|
||||
},
|
||||
|
||||
'keydown .tree-search input'(event, instance) {
|
||||
// Get the search term
|
||||
const searchTerm = $(event.currentTarget).val().trim();
|
||||
|
||||
const $labels = instance.$('.tree-inputs:first>label');
|
||||
|
||||
// Select the label if ENTER was pressed and there is only one result
|
||||
if (searchTerm.length && $labels.length === 1 && event.which === 13) {
|
||||
$labels.find('input').click();
|
||||
}
|
||||
},
|
||||
|
||||
'change .select-tree:first>.tree-content>.tree-options>.tree-inputs>label>input'(event, instance) {
|
||||
const component = instance.component;
|
||||
const $target = $(event.currentTarget);
|
||||
const $treeRoot = $target.closest('.select-tree-root');
|
||||
const $label = $target.closest('label');
|
||||
const eventComponent = $target.data('component');
|
||||
const rootComponent = instance.data.root || component;
|
||||
const rootInstance = rootComponent.templateInstance;
|
||||
const offsetTop = $target.closest('.wrapperLabel').offset().top;
|
||||
|
||||
// Change the component's node
|
||||
component.node(eventComponent.value());
|
||||
|
||||
// Unset the active leaf
|
||||
rootInstance.$('label').removeClass('active').css('width', '');
|
||||
|
||||
// Unset the selected state
|
||||
instance.setSelected(false);
|
||||
|
||||
// Detect the first interaction with the component and do the animation
|
||||
instance.setMargin(true);
|
||||
$treeRoot.addClass('navigated');
|
||||
|
||||
// Check if the clicked element is a node or a leaf
|
||||
const isLeaf = $label.hasClass('tree-leaf');
|
||||
if (isLeaf) {
|
||||
// Change the active item
|
||||
$label.css('width', $label.outerWidth()).addClass('active');
|
||||
|
||||
// Ckeck if there's a storageKey defined
|
||||
const storageKey = instance.data.storageKey;
|
||||
if (storageKey) {
|
||||
// Get the current stored data
|
||||
const storedData = _.extend({}, OHIF.user.getData(storageKey));
|
||||
|
||||
// Increment or create a counter for the clicked leaf
|
||||
const itemKey = OHIF.string.encodeId($target.val());
|
||||
if (storedData[itemKey]) {
|
||||
storedData[itemKey]++;
|
||||
} else {
|
||||
storedData[itemKey] = 1;
|
||||
}
|
||||
|
||||
// Update the stored data with the new count
|
||||
OHIF.user.setData(storageKey, storedData);
|
||||
}
|
||||
|
||||
// Set the selected leaf value in the root component
|
||||
const itemData = $target.data('component').templateInstance.data.itemData;
|
||||
rootComponent.value(itemData);
|
||||
|
||||
// Mark the component as selected
|
||||
instance.setSelected(true);
|
||||
} else {
|
||||
// Get the position of the clicked element
|
||||
const position = $label.position();
|
||||
|
||||
const getPosition = $element => {
|
||||
if (instance.useTransform) {
|
||||
const matrixToArray = str => str.match(/(-?[0-9\.]+)/g);
|
||||
const transformMatrix = matrixToArray($element.css('transform')) || [];
|
||||
return {
|
||||
x: parseFloat(transformMatrix[4]) || 0,
|
||||
y: parseFloat(transformMatrix[5]) || 0
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
x: parseFloat($element.css('left')),
|
||||
y: parseFloat($element.css('top'))
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const setPosition = ($element, position) => {
|
||||
if (instance.useTransform) {
|
||||
const translation = `translate(${position.x}px, ${position.y}px)`;
|
||||
OHIF.ui.styleProperty.set($element[0], 'transform', translation);
|
||||
} else {
|
||||
$element.css('left', `${position.x}px`);
|
||||
$element.css('top', `${position.y}px`);
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.defer(() => {
|
||||
const $treeNode = $label.parent().siblings('.select-tree');
|
||||
|
||||
// Do the transition from the clicked position to top
|
||||
$treeNode.css(position);
|
||||
setTimeout(() => $treeNode.css('top', 0));
|
||||
|
||||
const optionsTop = $target.closest('.tree-options').position().top;
|
||||
const treeOffsetTop = $treeRoot.offset().top;
|
||||
const treeRootPosition = getPosition($treeRoot);
|
||||
treeRootPosition.y += offsetTop - treeOffsetTop - optionsTop;
|
||||
setPosition($treeRoot, treeRootPosition);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'click .select-tree:first>.tree-content>.tree-options>.tree-breadcrumb .tree-back'(event, instance) {
|
||||
const $target = $(event.currentTarget);
|
||||
const rootComponent = instance.data.root || instance.component;
|
||||
const rootInstance = rootComponent.templateInstance;
|
||||
|
||||
// Prevent the default element action
|
||||
event.preventDefault();
|
||||
|
||||
// Reset the checked inputs to unchecked state
|
||||
rootInstance.$('input').removeAttr('checked');
|
||||
|
||||
// Unset the selected state
|
||||
instance.setSelected(false);
|
||||
instance.setMargin(false);
|
||||
$target.closest('.select-tree-root').removeClass('navigated');
|
||||
|
||||
// Get the index of the breadcrumb's clicked option
|
||||
const index = parseInt($target.attr('data-index'));
|
||||
|
||||
// Set the current instance
|
||||
let currentInstance = instance.component.parent.templateInstance;
|
||||
|
||||
// Iterate over all parents until gets into the clicked node
|
||||
for (let i = 1; i <= index; i++) {
|
||||
// Unset the selected node
|
||||
currentInstance.data.currentNode.set(null);
|
||||
|
||||
// Check if it's not the root node
|
||||
if (i < index) {
|
||||
// Change the current instance
|
||||
currentInstance = currentInstance.component.parent.templateInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.selectTree.helpers({
|
||||
treeItems() {
|
||||
const instance = Template.instance();
|
||||
|
||||
// Run this computation everytime the search term is changed
|
||||
const searchTerm = instance.searchTerm.get();
|
||||
|
||||
// Get all the items
|
||||
let items = instance.data.items;
|
||||
|
||||
// Check if the search term was informed
|
||||
if (searchTerm) {
|
||||
// Search the items by the give search term
|
||||
items = OHIF.string.search(items, searchTerm, 'label');
|
||||
|
||||
// Filter only the tree leaves
|
||||
items = _.filter(items, item => !item.items);
|
||||
} else if (instance.data.hideCommon) {
|
||||
// Get the values of common items
|
||||
const commonValues = _.pluck(instance.getCommonItems(), 'value');
|
||||
|
||||
// Filter only the items that are not common
|
||||
items = _.filter(items, item => !_.contains(commonValues, item.value));
|
||||
}
|
||||
|
||||
// Return the items sorted for tree columns
|
||||
if (instance.data.threeColumns) {
|
||||
const begin = items.splice(0, Math.ceil(items.length / 2));
|
||||
const sortedItems = [];
|
||||
_.each(begin, (item, index) => {
|
||||
sortedItems.push(item);
|
||||
if (items[index]) {
|
||||
sortedItems.push(items[index]);
|
||||
}
|
||||
});
|
||||
items = sortedItems;
|
||||
}
|
||||
|
||||
// Return the tree items
|
||||
return items;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,264 @@
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
// TODO: [design] can't we use colors that are already in common pallete?
|
||||
$gray1 = #A3A3A3
|
||||
$gray2 = #A0A0A0
|
||||
$gray3 = #C4C4C4
|
||||
$gray4 = #C0C0C0
|
||||
$gray5 = #9B9B9B
|
||||
$gray6 = #303030
|
||||
|
||||
.select-tree-root
|
||||
|
||||
.tree-content
|
||||
background-color: white
|
||||
position: relative
|
||||
transition(transform 0.3s ease\, background-color 0.3s ease\, border-color 0.3s ease\, border-radius 0.3s ease\, margin-right 0.3s ease)
|
||||
|
||||
.tree-search
|
||||
a.tree-back
|
||||
.tree-breadcrumb span
|
||||
.tree-node
|
||||
.tree-leaf
|
||||
transition(all 0.3s ease)
|
||||
|
||||
&.selected
|
||||
|
||||
label.tree-leaf.active
|
||||
box-shadow: 0 0 0 10px white, inset 0 0 0 200px white
|
||||
|
||||
.tree-content
|
||||
background-color: transparent
|
||||
|
||||
&>.tree-content
|
||||
border-color: transparent
|
||||
border-radius: 0
|
||||
|
||||
.tree-search
|
||||
background-color: transparent
|
||||
border-color: transparent
|
||||
color: transparent
|
||||
|
||||
input,
|
||||
i
|
||||
opacity: 0
|
||||
|
||||
&, .select-tree
|
||||
|
||||
&:not(.open)>.tree-content
|
||||
|
||||
&>.tree-inputs
|
||||
&>.tree-breadcrumb
|
||||
display: none
|
||||
|
||||
&>.tree-content>.tree-options
|
||||
|
||||
&>.tree-inputs>label:not(.active)
|
||||
&>.tree-breadcrumb a
|
||||
&>.tree-breadcrumb span
|
||||
color: transparent
|
||||
|
||||
&>.tree-inputs>label.tree-node:not(.active)
|
||||
border-color: transparent
|
||||
|
||||
&.common-clicked .tree-leaf.active
|
||||
background-color: transparent
|
||||
box-shadow: none
|
||||
color: transparent
|
||||
|
||||
.select-tree-root
|
||||
display: inline-block
|
||||
position: relative
|
||||
z-index: 0
|
||||
|
||||
&>.tree-content
|
||||
border: 1px solid $gray3
|
||||
border-radius: 5px
|
||||
overflow: hidden
|
||||
|
||||
&>.tree-content>.tree-options
|
||||
transform(scale(1))
|
||||
transition(height 0.3s ease)
|
||||
|
||||
.tree-search
|
||||
background-color: $gray4
|
||||
border-bottom: 1px solid $gray3
|
||||
color: $gray6
|
||||
display: flex
|
||||
margin: 0
|
||||
padding: 5px
|
||||
position: relative
|
||||
text-align: center
|
||||
|
||||
span
|
||||
font-weight: bold
|
||||
line-height: 24px
|
||||
|
||||
input
|
||||
border-color: $gray5
|
||||
font-weight: normal
|
||||
padding-left: 34px
|
||||
transition(opacity 0.3s ease)
|
||||
|
||||
& ~ i
|
||||
color: $gray1
|
||||
font-size: 18px
|
||||
left: 16px
|
||||
position: absolute
|
||||
top: 37px
|
||||
transition(opacity 0.3s ease)
|
||||
|
||||
.tree-breadcrumb
|
||||
line-height: 32px
|
||||
padding: 5px 12px
|
||||
|
||||
.path-link, .tree-current-node
|
||||
display: inline-block
|
||||
font-weight: normal
|
||||
float: left
|
||||
overflow: hidden
|
||||
text-overflow: ellipsis
|
||||
white-space: nowrap
|
||||
|
||||
i
|
||||
margin-right: 5px
|
||||
|
||||
.select-tree-node
|
||||
left: 0
|
||||
overflow: hidden
|
||||
position: fixed
|
||||
width: 100%
|
||||
z-index: 2
|
||||
|
||||
&
|
||||
.tree-content
|
||||
.tree-inputs
|
||||
height: 100%
|
||||
|
||||
.tree-options
|
||||
overflow: hidden
|
||||
position: relative
|
||||
z-index: 2
|
||||
|
||||
.tree-node, .tree-leaf
|
||||
cursor: pointer
|
||||
display: block
|
||||
font-weight: normal
|
||||
margin-bottom: 0
|
||||
padding: 0 12px
|
||||
position: relative
|
||||
z-index: 1
|
||||
|
||||
&>input
|
||||
display: none
|
||||
|
||||
.tree-node
|
||||
height: 41px
|
||||
border-bottom: 1px solid $gray3
|
||||
border-top: 1px solid $gray3
|
||||
line-height: 41px
|
||||
margin-top: -1px
|
||||
|
||||
&:last-child
|
||||
border-bottom: 0
|
||||
|
||||
.tree-leaf
|
||||
box-shadow: 0 0 0 200px transparent, inset 0 0 0 200px transparent
|
||||
line-height: 26px
|
||||
|
||||
&.active
|
||||
box-shadow: 0 0 0 200px white, inset 0 0 0 200px white
|
||||
transition(box-shadow 0.3s ease)
|
||||
z-index: 0
|
||||
|
||||
&:last-child:after
|
||||
display: block
|
||||
content: ''
|
||||
height: 10px
|
||||
|
||||
&.collapsed
|
||||
z-index: 1
|
||||
|
||||
.select-tree-common
|
||||
height: 100%
|
||||
padding-left: 6px
|
||||
position: absolute
|
||||
right: 0
|
||||
top: 0
|
||||
width: 50%
|
||||
|
||||
.content
|
||||
background-color: $gray6
|
||||
border: 1px solid $gray5
|
||||
border-radius: 5px
|
||||
color: white
|
||||
opacity: 0.9
|
||||
padding: 0 15px 12px
|
||||
position: absolute
|
||||
top: 50%
|
||||
transform(translateY(-50%))
|
||||
width: calc(100% - 6px)
|
||||
|
||||
h5.title
|
||||
color: $gray5
|
||||
font-weight: normal
|
||||
line-height: 20px
|
||||
margin: 12px 0 16px
|
||||
|
||||
div.labels
|
||||
position: relative
|
||||
|
||||
label
|
||||
cursor: pointer
|
||||
display: block
|
||||
font-weight: 300
|
||||
line-height: 26px
|
||||
margin: 0
|
||||
padding-left: 3px
|
||||
transition(padding 0.3s ease)
|
||||
|
||||
&:hover
|
||||
padding-left: 0
|
||||
|
||||
.select-tree-root
|
||||
|
||||
&>.tree-content
|
||||
transform(scale(0))
|
||||
transform-origin(100% 50%)
|
||||
|
||||
.select-tree-common
|
||||
z-index: -1
|
||||
|
||||
.content
|
||||
margin-left: calc(-100% - 6px)
|
||||
transform(scale(0))
|
||||
transition(margin-left 0.3s ease 0.3s\, transform 0s linear 0.3s)
|
||||
|
||||
&.started
|
||||
|
||||
&>.tree-content
|
||||
transform(scale(1))
|
||||
|
||||
.select-tree-common
|
||||
|
||||
.content
|
||||
margin-left: 0
|
||||
transform(scale(1) translateY(-50%))
|
||||
|
||||
&.navigated, &.selected
|
||||
|
||||
.select-tree-common
|
||||
|
||||
.content
|
||||
opacity: 0
|
||||
transform(translateX(-100%) scale(0))
|
||||
transition(transform 0.3s ease\, opacity 0.3s ease)
|
||||
|
||||
@keyframes selectTreeCommonOverflow {
|
||||
from {
|
||||
overflow: hidden
|
||||
}
|
||||
to {
|
||||
overflow: visible
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<template name="selectTreeCommon">
|
||||
<div class="select-tree-common">
|
||||
<div class="content">
|
||||
<h5 class="title">Common</h5>
|
||||
<div class="labels">
|
||||
{{#each item in items}}
|
||||
<label for="{{this.storageKey}}_{{encodeId item.value}}">{{item.label}}</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
|
||||
Template.selectTreeCommon.events({
|
||||
'click .select-tree-common label'(event, instance) {
|
||||
event.preventDefault();
|
||||
|
||||
// Get the clicked label
|
||||
const $target = $(event.currentTarget);
|
||||
|
||||
// Store the offset position for further use
|
||||
$target.data('offset', $target.offset());
|
||||
|
||||
// Build the input selector based on the label target
|
||||
const inputSelector = '#' + $target.attr('for');
|
||||
|
||||
// Get the tree instance
|
||||
const treeInstance = instance.data.treeInstance;
|
||||
|
||||
// Check if the input is not rendered
|
||||
if (!$(inputSelector).length) {
|
||||
// Change the search terms with the clicked label string
|
||||
|
||||
treeInstance.$('.tree-search input').val($target.text()).trigger('input');
|
||||
// Wait for options rerendering
|
||||
|
||||
Tracker.afterFlush(() => {
|
||||
const $treeRoot = treeInstance.$('.select-tree-root:first').first();
|
||||
$treeRoot.addClass('common-clicked');
|
||||
|
||||
// Wait for components initialization
|
||||
Meteor.defer(() => $(inputSelector).click());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Template.selectTreeCommon.helpers({
|
||||
// Get the common items
|
||||
items() {
|
||||
return Template.instance().data.treeInstance.getCommonItems();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import './serverInformationDicomWeb/serverInformationDicomWeb.html';
|
||||
import './serverInformationDicomWeb/serverInformationDicomWeb.js';
|
||||
|
||||
import './serverInformationDimse/serverInformationDimse.html';
|
||||
import './serverInformationDimse/serverInformationDimse.js';
|
||||
|
||||
import './serverInformationForm/serverInformationForm.html';
|
||||
import './serverInformationForm/serverInformationForm.js';
|
||||
|
||||
import './serverInformationList/serverInformationList.html';
|
||||
import './serverInformationList/serverInformationList.js';
|
||||
|
||||
import './serverInformationModal/serverInformationModal.html';
|
||||
import './serverInformationModal/serverInformationModal.styl';
|
||||
import './serverInformationModal/serverInformationModal.js';
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<template name="serverInformationDicomWeb">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputSelect labelClass='form-group' key='imageRendering' hideSearch=true
|
||||
options=(clone placeholder='Select an option')}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputSelect labelClass='form-group' key='thumbnailRendering' hideSearch=true
|
||||
options=(clone placeholder='Select an option')}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='qidoRoot'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<strong>QIDO options</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control">
|
||||
<div class="checkbox">
|
||||
{{>inputCheckbox key='qidoSupportsIncludeField'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h4>Request options</h4>
|
||||
{{#group class='row p-y-1' key='requestOptions'}}
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='auth'}}
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='requestFromBrowser'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<strong>Logging options</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control clearfix">
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logRequests'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logResponses'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='logTiming'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/group}}
|
||||
</template>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
|
||||
Template.serverInformationDicomWeb.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.autorun(function() {
|
||||
const mode = instance.data.mode.get();
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<template name="serverInformationDimse">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='wadoUriRoot'}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default panel-body">
|
||||
<div class="clearfix">
|
||||
<h4 class="pull-left">Peer List</h4>
|
||||
{{#button class='btn btn-primary pull-right'
|
||||
action='newPeer' params=peer
|
||||
}}New peer{{/button}}
|
||||
</div>
|
||||
<hr>
|
||||
{{#group class='row' key='peers' arrayValues=true}}
|
||||
{{#each peer in instance.peers.get}}
|
||||
{{#group class='col-lg-6'}}
|
||||
<div class="panel panel-default panel-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='aeTitle'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='hostAE'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='host'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputText class='form-control' key='port'}}
|
||||
</div>
|
||||
<div class="col-lg-12">
|
||||
<strong>Settings</strong>
|
||||
<div class="form-group">
|
||||
<div class="form-control height-auto clearfix">
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='default'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='server'}}
|
||||
</div>
|
||||
<div class="checkbox pull-left">
|
||||
{{>inputCheckbox key='supportsInstanceRetrievalByStudyUid'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 text-right">
|
||||
{{#button class='btn btn-sm btn-danger'
|
||||
action='removePeer' params=@index
|
||||
}}Remove{{/button}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/group}}
|
||||
{{/each}}
|
||||
{{/group}}
|
||||
</div>
|
||||
<hr>
|
||||
</template>
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import _ from 'underscore';
|
||||
|
||||
Template.serverInformationDimse.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
instance.peers = new ReactiveVar([]);
|
||||
instance.autorun(() => {
|
||||
const currentItem = instance.data.currentItem.get();
|
||||
if (currentItem) {
|
||||
instance.peers.set(currentItem.peers || []);
|
||||
}
|
||||
});
|
||||
const api = instance.data.api;
|
||||
_.extend(api, {
|
||||
newPeer() {
|
||||
const peers = instance.peers.get();
|
||||
peers.push({});
|
||||
instance.peers.set(peers);
|
||||
},
|
||||
|
||||
removePeer(peerIndex) {
|
||||
const peers = instance.peers.get();
|
||||
peers.splice(peerIndex, 1);
|
||||
instance.peers.set(peers);
|
||||
|
||||
const data = instance.data.currentItem.get();
|
||||
data.peers = peers;
|
||||
Tracker.afterFlush(() => instance.data.form.value(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.serverInformationDimse.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
instance.autorun(() => {
|
||||
const mode = instance.data.mode.get();
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<template name="serverInformationForm">
|
||||
{{#form class='server-information-form' schema=instance.currentSchema api=this.api}}
|
||||
{{>inputHidden key='_id'}}
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
{{>inputText labelClass='form-group' key='name'}}
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
{{>inputSelect labelClass='form-group' key='type'
|
||||
hideSearch=true disabled=(eq this.mode.get 'edit')
|
||||
options=(clone placeholder='Choose a server type')}}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
{{#if eq this.serverType.get 'dicomWeb'}}
|
||||
{{>serverInformationDicomWeb this}}
|
||||
{{/if}}
|
||||
{{#if eq this.serverType.get 'dimse'}}
|
||||
{{>serverInformationDimse this}}
|
||||
{{/if}}
|
||||
{{#if this.serverType.get}}
|
||||
<div>
|
||||
{{#button class='btn btn-primary' action='save'}}Save{{/button}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/form}}
|
||||
</template>
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { DICOMWebServer as dicomSchema } from 'meteor/ohif:servers/both/schema/servers.js';
|
||||
import { DIMSEServer as dimseSchema } from 'meteor/ohif:servers/both/schema/servers.js';
|
||||
|
||||
Template.serverInformationForm.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.api = {
|
||||
save() {
|
||||
// Stop here if the form validation fails
|
||||
if (!instance.data.form.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current form data
|
||||
const formData = instance.data.form.value();
|
||||
|
||||
// Call the save method
|
||||
Meteor.call('serverSave', formData, function(error) {
|
||||
if (error) {
|
||||
// TODO: check for errors: not-authorized, data-write
|
||||
OHIF.log.error(error);
|
||||
}
|
||||
|
||||
instance.data.resetState();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
instance.currentSchema = new ReactiveVar(dicomSchema);
|
||||
});
|
||||
|
||||
Template.serverInformationForm.onRendered(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.data.$form = instance.$('form').first();
|
||||
instance.data.form = instance.data.$form.data('component');
|
||||
|
||||
// Handle the server type
|
||||
instance.autorun(() => {
|
||||
// Get the server type component
|
||||
const typeComponent = instance.$('[data-key=type] :input').data('component');
|
||||
|
||||
// Run this computation every time the user change the server type
|
||||
typeComponent.depend();
|
||||
|
||||
// Get the current server type value
|
||||
const type = typeComponent.value();
|
||||
|
||||
// Set the serverType reactive value
|
||||
instance.data.serverType.set(type);
|
||||
|
||||
// Change the schema based on the selected server type
|
||||
if (type === 'dimse') {
|
||||
instance.currentSchema.set(dimseSchema);
|
||||
} else {
|
||||
instance.currentSchema.set(dicomSchema);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the form mode (edit or add)
|
||||
instance.autorun(() => {
|
||||
const mode = instance.data.mode.get();
|
||||
|
||||
// Check if it is on edit mode and load the saved data
|
||||
if (mode === 'edit') {
|
||||
const data = instance.data.currentItem.get();
|
||||
instance.data.form.value(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<template name="serverInformationList">
|
||||
{{#form api=instance.api}}
|
||||
<table id="tblServer" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th class="text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each server in servers}}
|
||||
<tr>
|
||||
<td>{{server.name}}</td>
|
||||
<td>{{server.type}}</td>
|
||||
<td class="text-right">
|
||||
<div class="btn-group" role="group" aria-label="Actions">
|
||||
{{#button class='btn btn-sm btn-default' action='use' params=server title='Use this server' disabled=(isActive server)}}
|
||||
{{#if isActive server}}
|
||||
<i class="fa fa-check-square-o"></i>
|
||||
{{else}}
|
||||
<i class="fa fa-square-o"></i>
|
||||
{{/if}}
|
||||
{{/button}}
|
||||
{{#button class='btn btn-sm btn-default' action='edit' params=server title='Edit' disabled=(eq server.origin 'json')}}
|
||||
<i class="fa fa-pencil"></i>
|
||||
{{/button}}
|
||||
{{#button class='btn btn-sm btn-danger' action='delete' params=server title='Remove' disabled=(eq server.origin 'json')}}
|
||||
<i class="fa fa-trash"></i>
|
||||
{{/button}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td class="text-center" colspan="3">No servers found</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{#button class='btn btn-sm btn-default' action='add'}}
|
||||
<i class="fa fa-plus"></i> Add a new server
|
||||
{{/button}}
|
||||
{{/form}}
|
||||
</template>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Template } from 'meteor/templating';
|
||||
import { Servers, CurrentServer } from 'meteor/ohif:servers/both/collections';
|
||||
|
||||
Template.serverInformationList.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
|
||||
instance.api = {
|
||||
add: () => instance.data.mode.set('create'),
|
||||
|
||||
edit(server) {
|
||||
instance.data.currentItem.set(server);
|
||||
instance.data.mode.set('edit');
|
||||
},
|
||||
|
||||
delete(server) {
|
||||
// TODO: Replace this for confirmation dialog after LT-refactor is merged back to master
|
||||
if (!window.confirm('Are you sure you want to remove this peer?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Meteor.call('serverRemove', server._id, error => {
|
||||
// TODO: check for errors: data-write
|
||||
});
|
||||
},
|
||||
|
||||
use(server) {
|
||||
Meteor.call('serverSetActive', server._id, error => {
|
||||
// TODO: check for errors: data-write
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.serverInformationList.helpers({
|
||||
isActive: function(server) {
|
||||
const currentServer = CurrentServer.findOne();
|
||||
if (!currentServer) {
|
||||
return;
|
||||
}
|
||||
|
||||
return server._id === currentServer.serverId;
|
||||
},
|
||||
servers: function() {
|
||||
const options = {
|
||||
sort: {
|
||||
origin: -1,
|
||||
type: 1,
|
||||
name: 1
|
||||
}
|
||||
};
|
||||
return Servers.find({}, options).fetch();
|
||||
}
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<template name="serverInformationModal">
|
||||
{{#dialogSimple (extend this dialogClass='modal-lg' title='Server Information')}}
|
||||
{{#if (eq instance.container.mode.get "list")}}
|
||||
{{>serverInformationList instance.container}}
|
||||
{{else}}
|
||||
{{>serverInformationForm instance.container}}
|
||||
{{/if}}
|
||||
{{#section 'dialogFooter'}}{{#unless (eq instance.container.mode.get "list")}}{{!
|
||||
}}<button type="button" class="btn btn-default js-back">Back</button>{{!
|
||||
}}{{/unless}}{{/section}}
|
||||
{{/dialogSimple}}
|
||||
</template>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
|
||||
Template.serverInformationModal.onCreated(function() {
|
||||
const instance = Template.instance();
|
||||
instance.container = {
|
||||
mode: new ReactiveVar('list'),
|
||||
serverType: new ReactiveVar(null),
|
||||
currentItem: new ReactiveVar(null),
|
||||
$form: null,
|
||||
resetState: function() {
|
||||
instance.container.mode.set('list');
|
||||
instance.container.serverType.set(null);
|
||||
instance.container.currentItem.set(null);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Template.serverInformationModal.events({
|
||||
'click .js-back'(event, instance) {
|
||||
instance.container.resetState();
|
||||
}
|
||||
});
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
.server-information-form
|
||||
label.wrapper
|
||||
display: block
|
||||
.checkbox
|
||||
margin: 0 8px 0 0
|
||||
@@ -0,0 +1,3 @@
|
||||
import './themeSelectorModal/themeSelectorModal.html';
|
||||
import './themeSelectorModal/themeSelectorModal.styl';
|
||||
import './themeSelectorModal/themeSelectorModal.js';
|
||||
@@ -0,0 +1,17 @@
|
||||
<template name="themeSelectorModal">
|
||||
{{#dialogForm (extend this
|
||||
id='themeSelectorModal' dialogClass='themed modal-lg' confirmLabel='Apply theme'
|
||||
title='Themes' titleIcon='fa fa-sliders'
|
||||
)}}
|
||||
<ul class="theme-list text-center">
|
||||
{{#each theme in themes}}
|
||||
<li class="text-center preview-theme {{printClassIfSelected theme}}" data-theme="{{theme}}">
|
||||
<div class="icon-wrapper">
|
||||
<span class="theme-icon-{{theme}}"></span>
|
||||
</div>
|
||||
<h4 class="theme-title">{{ucFirst theme}}</h4>
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/dialogForm}}
|
||||
</template>
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Template } from 'meteor/templating';
|
||||
import { ReactiveDict } from 'meteor/reactive-dict';
|
||||
|
||||
const DEFAULT_THEME = 'tide';
|
||||
|
||||
const getCurrentTheme = () => sessionStorage.getItem('theme');
|
||||
|
||||
const setActualTheme = theme => sessionStorage.setItem('theme', theme);
|
||||
|
||||
const addThemeToBody = theme => document.body.classList.add('theme-' + theme);
|
||||
|
||||
const removeThemesFromBody = () => {
|
||||
const classList = document.body.classList;
|
||||
|
||||
for (let i = classList.length - 1; i >= 0; i--) {
|
||||
if (classList[i].search('theme-') !== -1) {
|
||||
document.body.classList.remove(classList[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Template.themeSelectorModal.onCreated(() => {
|
||||
const instance = Template.instance();
|
||||
let currentTheme = getCurrentTheme();
|
||||
if (!currentTheme) {
|
||||
currentTheme = DEFAULT_THEME;
|
||||
setActualTheme(currentTheme);
|
||||
}
|
||||
|
||||
instance.state = new ReactiveDict();
|
||||
instance.state.set('selectedTheme', currentTheme);
|
||||
|
||||
addThemeToBody(currentTheme);
|
||||
|
||||
instance.container = {
|
||||
currentTheme,
|
||||
|
||||
previewTheme(theme, state) {
|
||||
state.set('selectedTheme', theme);
|
||||
removeThemesFromBody();
|
||||
addThemeToBody(theme);
|
||||
},
|
||||
|
||||
applyTheme(state) {
|
||||
setActualTheme(state.get('selectedTheme'));
|
||||
},
|
||||
|
||||
resetState(state) {
|
||||
const theme = getCurrentTheme();
|
||||
|
||||
state.set('selectedTheme', theme);
|
||||
removeThemesFromBody();
|
||||
addThemeToBody(theme);
|
||||
}
|
||||
};
|
||||
|
||||
const { promise } = instance.data;
|
||||
promise.then(() => instance.container.applyTheme(instance.state));
|
||||
promise.catch(() => instance.container.resetState(instance.state));
|
||||
});
|
||||
|
||||
Template.themeSelectorModal.helpers({
|
||||
themes: [ 'crickets', 'honeycomb', 'mint', 'overcast', 'quartz', 'tide', 'tigerlily' ],
|
||||
|
||||
ucFirst(text) {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
||||
},
|
||||
|
||||
printClassIfSelected(theme) {
|
||||
const instance = Template.instance();
|
||||
|
||||
return theme === instance.state.get('selectedTheme') ? 'selected' : '';
|
||||
}
|
||||
});
|
||||
|
||||
Template.themeSelectorModal.events({
|
||||
'click .preview-theme'(event, instance) {
|
||||
instance.container.previewTheme(event.currentTarget.dataset.theme, instance.state);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
@import "{ohif:viewerbase}/app.styl"
|
||||
|
||||
#themeSelectorModal
|
||||
.theme-list
|
||||
list-style: none
|
||||
padding: 0
|
||||
|
||||
li
|
||||
display: inline-block
|
||||
cursor: pointer
|
||||
margin-right: 10px
|
||||
margin-bottom: 10px
|
||||
padding: 10px
|
||||
width: 110px
|
||||
theme('border', '1px solid $uiBorderColorDark')
|
||||
|
||||
&:hover
|
||||
background-color: rgba(21, 25, 30, 0.7)
|
||||
|
||||
&:last-child
|
||||
margin-right: 0
|
||||
|
||||
&.selected
|
||||
theme('border', '3px solid $uiBorderColorActive')
|
||||
padding: 7px;
|
||||
|
||||
.icon-wrapper
|
||||
height: 64px
|
||||
width: 64px
|
||||
margin: 0 auto
|
||||
line-height: 6.5
|
||||
|
||||
.theme-title
|
||||
font-weight: normal
|
||||
font-size: 1.1em
|
||||
margin-bottom: 0
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "{ohif:design}/app"
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
#imageViewerViewports
|
||||
height: 100%
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import "{ohif:design}/app"
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
#viewer
|
||||
height: "calc(100% - %s)" % $topBarHeight
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@import "{ohif:design}/app"
|
||||
@import "{ohif:viewerbase}/app"
|
||||
|
||||
.annotationDialog
|
||||
theme('border', '1px solid $uiBorderColor', 0.95)
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Template } from 'meteor/templating';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { Session } from 'meteor/session';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { _ } from 'meteor/underscore';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import _ from 'underscore';
|
||||
import $ from 'jquery';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstoneTools } from 'meteor/ohif:cornerstone';
|
||||
import { viewportUtils } from '../../../lib/viewportUtils';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@require '{ohif:design}/app'
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
#cineDialog
|
||||
theme('background', '$uiGrayDarkest', 0.85)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Template } from 'meteor/templating';
|
||||
import { Tracker } from 'meteor/tracker';
|
||||
import { ReactiveVar } from 'meteor/reactive-var';
|
||||
import { Session } from 'meteor/session';
|
||||
import { $ } from 'meteor/jquery';
|
||||
import $ from 'jquery';
|
||||
import { SimpleSchema } from 'meteor/aldeed:simple-schema';
|
||||
import { OHIF } from 'meteor/ohif:core';
|
||||
import { cornerstone, cornerstoneTools } from 'meteor/ohif:cornerstone';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@require '{ohif:design}/app'
|
||||
@require '{ohif:viewerbase}/app'
|
||||
|
||||
#imageDownloadDialog
|
||||
|
||||
|
||||
Loaded 100 of 479 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user