Adding series and scrolling to quickSwitch

This commit is contained in:
Bruno Alves de Faria 2017-10-18 17:35:46 -02:00
parent a583f5a124
commit 0417bbd0ff
17 changed files with 400 additions and 70 deletions

View File

@ -1,5 +1,6 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
Template.viewerSection.onCreated(() => {
@ -28,6 +29,26 @@ Template.viewerSection.events({
if (!event.target.classList.contains('sidebarMenu')) return;
window.ResizeViewportManager.handleResize();
},
'ohif.lesiontracker.timepoint.changeViewType .timepoint-browser-list'(event, instance, viewType) {
const $browserList = $(event.currentTarget);
const $allBrowserItems = $browserList.find('.timepoint-browser-item');
// Removes all active classes to collapse the timepoints and studies
$allBrowserItems.removeClass('active');
if (viewType === 'key') {
const { timepointIds, currentTimepointId } = OHIF.viewer.data;
timepointIds.forEach(timepointId => {
const $browserItem = $allBrowserItems.filter(`[data-id=${timepointId}]`);
$browserItem.find('.timepoint-item').trigger('ohif.lesiontracker.timepoint.load');
});
// Show only current timepoint expanded on key timepoints tab
const $browserItem = $allBrowserItems.filter(`[data-id=${currentTimepointId}]`);
$browserItem.find('.timepoint-item').trigger('click');
}
}
});

View File

@ -1,2 +1,3 @@
import './scrollArea.html';
import './scrollArea.js';
import './scrollArea.styl';

View File

@ -3,5 +3,7 @@
<div class="scrollable {{this.scrollableClass}} {{#if instance.config.scrollX}}scroll-x{{/if}} {{#if instance.config.scrollY}}scroll-y{{/if}}">
{{>Template.contentBlock}}
</div>
<div class="scroll-nav scroll-nav-up"></div>
<div class="scroll-nav scroll-nav-down"></div>
</div>
</template>

View File

@ -7,7 +7,8 @@ Template.scrollArea.onCreated(() => {
const defaultConfig = {
hideScrollbar: true,
scrollY: true,
scrollX: false
scrollX: false,
scrollStep: 100
};
instance.config = _.defaults(data || {}, defaultConfig);
@ -26,4 +27,55 @@ Template.scrollArea.onRendered(() => {
'margin-bottom': 0 - (scrollable.offsetHeight - scrollable.clientHeight) * x
});
}
instance.$scrollable = instance.$('.scrollable').first();
instance.scrollHandler = _.throttle(event => {
const $scrollable = event ? $(event.currentTarget) : instance.$scrollable;
const $scrollArea = $scrollable.closest('.scroll-area');
if ($scrollable[0] !== instance.$('.scrollable')[0]) return;
$scrollArea.removeClass('can-scroll-up can-scroll-down');
const height = $scrollable.outerHeight();
const scrollTop = $scrollable.scrollTop();
const { scrollHeight } = $scrollable[0];
// Stop here if unable to scroll
if (scrollHeight <= height) return;
// Check if can scroll up
if (scrollTop) {
$scrollArea.addClass('can-scroll-up');
}
// Check if can scroll down
if (scrollTop + height < scrollHeight) {
$scrollArea.addClass('can-scroll-down');
}
}, 300);
instance.scrollHandler();
});
Template.scrollArea.events({
'scroll .scrollable, mouseenter .scrollable, transitionend .scrollable'(event, instance) {
instance.scrollHandler(event);
},
'click .scroll-nav-down'(event, instance) {
const $scrollable = $(event.currentTarget).siblings('.scrollable');
const height = $scrollable.outerHeight();
const currentTop = $scrollable.scrollTop();
const { scrollHeight } = $scrollable[0];
const limit = scrollHeight - height;
let scrollTop = currentTop + instance.data.scrollStep;
scrollTop = scrollTop > limit ? limit : scrollTop;
$scrollable.stop().animate({ scrollTop }, 150, 'swing');
},
'click .scroll-nav-up'(event, instance) {
const $scrollable = $(event.currentTarget).siblings('.scrollable');
const currentTop = $scrollable.scrollTop();
let scrollTop = currentTop - instance.data.scrollStep;
scrollTop = scrollTop < 0 ? 0 : scrollTop;
$scrollable.stop().animate({ scrollTop }, 150, 'swing');
},
});

View File

@ -0,0 +1,78 @@
@require '{ohif:design}/app'
$scrollNavSize = 24px
.scroll-area
overflow: hidden
position: relative
.scrollable
max-height: inherit
overflow: hidden
&.scroll-x
overflow-x: scroll
&.scroll-y
overflow-y: scroll
&.fit
height: 100%
width: 100%
.scrollable
bottom: 0
left: 0
max-height: none
position: absolute
right: 0
top: 0
.scroll-nav
background-color: rgba(0, 0, 0, 0.75)
box-shadow(0 0 10px 10px rgba(0, 0, 0, 0.75))
cursor: pointer
height: $scrollNavSize
left: 10px
opacity: 0
position: absolute
right: 10px
transition(transform 0.3s ease\, opacity 0.3s ease\, background-color 0.3s ease\, box-shadow 0.3s ease)
&:after
theme('color', '$activeColor')
display: block
font-family: FontAwesome
font-size: 20px
text-align: center
transition(color 0.3s ease)
&:hover
background-color: rgba(0, 0, 0, 0.9)
box-shadow(0 0 10px 10px rgba(0, 0, 0, 0.9))
&:after
theme('color', '$hoverColor')
.scroll-nav-up
border-bottom-left-radius($scrollNavSize / 2)
border-bottom-right-radius($scrollNavSize / 2)
top: 0
transform(translateY(- $scrollNavSize))
&:after
content: '\f102'
.scroll-nav-down
border-top-left-radius($scrollNavSize / 2)
border-top-right-radius($scrollNavSize / 2)
bottom: 0
transform(translateY($scrollNavSize))
&:after
content: '\f103'
&.can-scroll-up .scroll-nav-up,
&.can-scroll-down .scroll-nav-down
opacity: 1
transform(translateY(0))

View File

@ -1,4 +1,4 @@
@import "{ohif:design}/app"
@require '{ohif:design}/app'
html body
theme('background-color', '$primaryBackgroundColor')
@ -140,16 +140,3 @@ body .select2-container--default .select2-results>.select2-results__options
.flex-grow
flex-grow: 1
.scroll-area
overflow: hidden
.scrollable
max-height: inherit
overflow: hidden
&.scroll-x
overflow-x: scroll
&.scroll-y
overflow-y: scroll

View File

@ -16,6 +16,18 @@ animation(a)
border-radius(r)
vendorize(border-radius, r)
border-top-left-radius(r)
vendorize(border-top-left-radius, r)
border-top-right-radius(r)
vendorize(border-top-right-radius, r)
border-bottom-left-radius(r)
vendorize(border-bottom-left-radius, r)
border-bottom-right-radius(r)
vendorize(border-bottom-right-radius, r)
box-shadow(s)
vendorize(box-shadow, s)

View File

@ -1,5 +1,5 @@
<template name="timepointBrowserItem">
<div class="timepoint-browser-item">
<div class="timepoint-browser-item" data-id="{{this.timepoint.timepointId}}">
<div class="timepoint-item p-y-2">
<div class="timepoint-details clearfix">
<div class="timepoint-title pull-left">

View File

@ -5,6 +5,8 @@ import { $ } from 'meteor/jquery';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
const studySearchPromises = new Map();
Template.timepointBrowserItem.onCreated(() => {
const instance = Template.instance();
const { timepoint, timepointApi } = instance.data;
@ -28,10 +30,22 @@ Template.timepointBrowserItem.onCreated(() => {
};
const filter = { studyInstanceUid: timepoint.studyInstanceUids };
instance.loadStudies = () => OHIF.studies.searchStudies(filter).then(studiesData => {
instance.loadStudies = () => {
const promiseKey = JSON.stringify(filter);
if (studySearchPromises.has(promiseKey)) {
return studySearchPromises.get(promiseKey);
} else {
const promise = instance.performLoad();
studySearchPromises.set(promiseKey, promise);
return promise;
}
};
instance.performLoad = () => OHIF.studies.searchStudies(filter).then(studiesData => {
timepoint.studiesData = studiesData;
timepointApi.timepoints.update(timepoint._id, { $set: { studiesData } });
instance.loaded = true;
instance.setModalitiesSummary();
instance.loading.set(false);
}).catch(error => {
const text = 'An error has occurred while retrieving studies information';
@ -45,6 +59,10 @@ Template.timepointBrowserItem.onCreated(() => {
});
Template.timepointBrowserItem.events({
'ohif.lesiontracker.timepoint.load .timepoint-item'(event, instance) {
instance.loadStudies();
},
'click .timepoint-item'(event, instance) {
const element = event.currentTarget.parentElement;
const $element = $(element);
@ -55,10 +73,7 @@ Template.timepointBrowserItem.events({
if (!instance.loaded) {
instance.summary.set('Loading...');
instance.loadStudies().then(() => {
Tracker.afterFlush(triggerClick);
instance.setModalitiesSummary();
});
instance.loadStudies().then(() => Tracker.afterFlush(triggerClick));
} else {
triggerClick();
}

View File

@ -5,7 +5,7 @@
{{>roundedButtonGroup viewTypeButtonGroupData}}
</div>
</div>
{{#scrollArea class='flex-grow' scrollableClass='p-a-1'}}
{{#scrollArea class='flex-grow' class='fit' scrollableClass='p-a-1' scrollStep=191}}
{{>timepointBrowserList (extend timepointBrowserData)}}
{{/scrollArea}}
</div>

View File

@ -11,18 +11,19 @@ Template.timepointBrowserSidebar.onCreated(() => {
Template.timepointBrowserSidebar.onRendered(() => {
const instance = Template.instance();
instance.lastType = '';
// Collapse all timepoints but first when timepoint view type changes
instance.$browserList = instance.$('.timepoint-browser-list').first();
instance.autorun(() => {
// Runs this computation every time the timepointViewType is changed
const type = instance.timepointViewType.get();
// Removes all active classes to collapse the timepoints and studies
instance.$('.timepoint-browser-item, .study-browser-item').removeClass('active');
if (type === 'key') {
// Show only first timepoint expanded for key timepoints
instance.$('.timepoint-browser-item:first .timepoint-item').trigger('click');
if (type !== instance.lastType) {
const eventKey = 'ohif.lesiontracker.timepoint.changeViewType';
instance.$browserList.trigger(eventKey, type);
}
instance.lastType = type;
});
});

View File

@ -1,5 +1,5 @@
<template name="studyBrowserSidebar">
{{#scrollArea class='full-height' scrollableClass='p-a-1'}}
{{#scrollArea class='full-height' class='fit' scrollableClass='p-a-1' scrollStep=191}}
{{>studyBrowserList (clone this studyChildTemplate='studyBrowserSeries')}}
{{/scrollArea}}
</template>

View File

@ -23,7 +23,6 @@ $seriesCountBackgroundColor = #678696
word-wrap: break-word
&.info-only
min-height: auto
.seriesDescription
display: none

View File

@ -4,17 +4,15 @@
<div class="series-switch rp-t-1 rp-x-1">
<div class="title-label">Series</div>
<div class="series-box r-font">
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item"></div>
<div class="series-item count">37</div>
{{#scrollArea class='series-browser'}}
<div>BBBBB</div>
{{#each seriesItem in seriesItems}}
<div class="series-item {{seriesItem.class}}">{{seriesItem.value}}</div>
{{/each}}
{{#scrollArea class='series-browser' scrollStep=201}}
<div class="clearfix thumbnails-wrapper">
{{#each thumbnail in (studyThumbnails instance.currentStudy.get)}}
{{>thumbnailEntry (clone this thumbnail=thumbnail)}}
{{/each}}
</div>
{{/scrollArea}}
</div>
</div>

View File

@ -1,9 +1,57 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { $ } from 'meteor/jquery';
import { OHIF } from 'meteor/ohif:core';
// FIXME >>>> REMOVE
$(document.body).on('keydown', e => (e.keyCode === 69 && $('.quickSwitchWrapper').toggle()) || 1);
OHIF.cornerstone.renderer = '';
Template.seriesQuickSwitch.onCreated(() => {
const instance = Template.instance();
// Defines the study being shown in the current viewport
instance.currentStudy = new ReactiveVar();
// Gets the viewport data for the given viewport index
instance.getViewportData = viewportIndex => {
const layoutManager = OHIF.viewerbase.layoutManager;
return layoutManager && layoutManager.viewportData && layoutManager.viewportData[viewportIndex];
};
// Gets the current viewport data
const viewportIndex = instance.data.viewportIndex;
instance.study = {};
instance.lastStudy = {};
instance.autorun(() => {
OHIF.viewerbase.layoutManager.observer.depend();
const viewportData = instance.getViewportData(viewportIndex);
// @TypeSafeStudies
if (viewportData) {
// Finds the current study and return it
instance.study = OHIF.viewer.Studies.findBy({
studyInstanceUid: viewportData.studyInstanceUid
});
} else {
instance.study = OHIF.viewer.Studies.getElementByIndex(0);
}
if (!instance.study) {
return;
}
if (instance.study.studyInstanceUid !== instance.lastStudy.studyInstanceUid) {
// Change the current study to update the thumbnails
instance.currentStudy.set(instance.study);
instance.lastStudy = instance.study;
}
});
});
Template.seriesQuickSwitch.helpers({
shallDisplay() {
@ -17,13 +65,54 @@ Template.seriesQuickSwitch.helpers({
const { columns, viewportIndex } = instance.data;
if (columns === 1) return '';
return viewportIndex === 0 ? 'left' : 'right';
},
seriesItems() {
const instance = Template.instance();
OHIF.viewerbase.layoutManager.observer.depend();
const { viewportIndex } = instance.data;
const viewportData = OHIF.viewerbase.layoutManager.viewportData[viewportIndex];
const study = instance.currentStudy.get();
const seriesItems = [];
const items = study.displaySets.length;
for (let i = 0; i < items; i++) {
const displaySet = study.displaySets[i];
const item = { class: '' };
seriesItems.push(item);
if (i === 8 && items !== 9) {
item.class += ' count';
item.value = items;
break;
}
if (displaySet.displaySetInstanceUid === viewportData.displaySetInstanceUid) {
item.class += ' active';
}
}
return seriesItems;
}
});
Template.seriesQuickSwitch.events({
'mouseenter .series-switch'(event, instance) {
// Control the width of the series browser
const $switch = $(event.currentTarget);
const browserWidth = $switch.offset().left + $switch.outerWidth();
$switch.find('.series-browser').width(browserWidth);
const $seriesBrowser = $switch.find('.series-browser');
$seriesBrowser.width(browserWidth);
instance.$('.series-quick-switch').addClass('series-triggered');
},
'mouseleave .series-browser'(event, instance) {
$(event.currentTarget).children('.scrollable').stop().animate({ scrollTop: 0 }, 300, 'swing');
},
'mouseleave .series-quick-switch'(event, instance) {
$(event.currentTarget).removeClass('series-triggered');
}
});

View File

@ -42,7 +42,13 @@
right: auto
transform-origin(1em calc(1em + 15px))
.series-box .series-item
.thumbnails-wrapper
transform-origin(0% 0%)
.thumbnailEntry
float: left
.series-item
float: left
.title-label
@ -50,8 +56,10 @@
font-size: 12px
font-weight: 500
line-height: 12px
opacity: 1
padding-bottom: 3px
text-align: center
transition(opacity 0.3s ease)
.series-switch,
.study-switch
@ -65,51 +73,114 @@
width: 285px
.series-browser
background: blue
max-width: 711px
right: 0
transform-origin(calc(100% - 1em) calc(1em + 15px))
.thumbnails-wrapper
transition(transform 0.3s ease)
transform-origin(100% 0%)
.thumbnailEntry
float: right
.seriesDetails
opacity: 0
transform(translateY(-100%))
transition(transform 0.3s ease\, opacity 0.3s ease)
.scrollable
padding-top: 15px
transition(padding-bottom 0.3s ease)
.series-browser,
.study-browser
max-height: 100vh
min-height: 120px
opacity: 0
position: absolute
top: 0
transform(scale(0))
transition(transform 0.3s ease)
transition(transform 0.3s ease\, opacity 0.3s ease)
z-index: 3
.series-box
.series-box,
.study-box
height: 57px
width: 57px
.series-item
theme('background-color', '$boxBackgroundColor')
border-radius(3px)
float: right
height: 15px
margin: 2px
width: 15px
&.count
theme('color', '$textPrimaryColor')
background-color: transparent
font-size: 12px
font-weight: 500
line-height: 17px
text-align: center
&.active
theme('background-color', '$activeColor')
.study-box
theme('background-color', '$uiGrayDark')
theme('border', 'solid 2px $uiBorderColorDark')
border-radius: 11px
height: 57px
width: 57px
border-radius(11px)
.series-item
theme('background-color', '$boxBackgroundColor')
border-radius(3px)
float: right
height: 15px
margin: 2px
transition(opacity 0.3s ease)
width: 15px
&.count
theme('color', '$textPrimaryColor')
background-color: transparent
font-size: 12px
font-weight: 500
line-height: 17px
text-align: center
&.active
theme('background-color', '$activeColor')
&.series-triggered .series-browser,
.series-switch:hover .series-browser,
.study-switch:hover .study-browser
opacity: 1
transform(scale(1))
&.series-triggered .series-item,
.series-switch:hover .title-label
opacity: 0
&.series-triggered
.thumbnails-wrapper
transform(scale(0.9) translateY(80px))
.series-browser:not(:hover)
&>.scrollable
padding-bottom: 80px
&>.scroll-nav
opacity: 0
.series-switch:hover .series-browser
.thumbnails-wrapper
transform(scale(1) translateY(0))
.thumbnailEntry .seriesDetails
opacity: 1
transform(translateY(0))
transition-delay(0.3s)
@media screen and (max-width: 1599px)
.series-quick-switch
.series-box,
.study-box
width: 36px
height: 36px
.study-box
border-radius(7px)
.series-item
height: 10px
margin: 1px
width: 10px
&.count
font-size: 10px
font-weight: 300
line-height: 10px

View File

@ -18,6 +18,7 @@ export class LayoutManager {
constructor(parentNode, studies) {
OHIF.log.info('LayoutManager constructor');
this.observer = new Tracker.Dependency();
this.parentNode = parentNode;
this.studies = studies;
this.viewportData = [];
@ -30,7 +31,10 @@ export class LayoutManager {
this.isZoomed = false;
const updateSessionFn = () => Tracker.afterFlush(() => Session.set('LayoutManagerUpdated', Math.random()));
const updateSessionFn = () => Tracker.afterFlush(() => {
Session.set('LayoutManagerUpdated', Math.random());
this.observer.changed();
});
this.updateSession = _.throttle(updateSessionFn, 300);
}