Replacing studyListContextMenu for dropdown

This commit is contained in:
Bruno Alves de Faria 2017-03-14 14:16:17 -03:00
parent 4daf7d8bf8
commit 485f82f64c
22 changed files with 250 additions and 325 deletions

View File

@ -15,7 +15,7 @@ Template.app.onCreated(() => {
ViewerData = Session.get('ViewerData') || {};
OHIF.header.setDropdownItems([{
OHIF.header.dropdown.setItems([{
action: OHIF.user.audit,
text: 'View Audit Log',
iconClasses: 'log',

View File

@ -20,11 +20,12 @@ OHIF.mixins.action = new OHIF.Mixin({
event.preventDefault();
const component = instance.component;
// Extract action, params and disabled state
const { action, params, disabled } = instance.data;
// Extract action, disabled state and params
const { action } = instance.data;
const params = instance.data.params ? instance.data.params : event;
// Stop here if the component is disabled
if (disabled) return;
if (component.$element.hasClass('disabled')) return;
// Get the current component's API
const api = component.getApi();

View File

@ -42,7 +42,8 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
// Close the dropdown resolving or rejecting the promise
instance.close = (isResolve, result) => {
const method = instance.data[isResolve ? 'promiseResolve' : 'promiseReject'];
method(result);
const param = result instanceof Promise ? null : result;
method(param);
};
// Stop here and destroy the view if no items was given
@ -52,7 +53,7 @@ OHIF.mixins.dropdown = new OHIF.Mixin({
dropdown.oncontextmenu = () => false;
const cssBefore = { 'z-index': 10000 };
const cssBefore = {};
if (event) {
cssBefore.position = 'fixed';
$dropdownMenu.bounded();

View File

@ -6,9 +6,13 @@
ul.dropdown-menu
display: block
z-index: 1000
transform(scale(0))
transition(transform 0.3s ease)
&.dropdown-menu-left
transform-origin(0% 0%)
&.dropdown-menu-right
transform-origin(100% 0%)

View File

@ -0,0 +1,38 @@
import { Tracker } from 'meteor/tracker';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
class Dropdown {
constructor() {
this.observer = new Tracker.Dependency();
this._items = [];
this.observer.changed();
}
clearItems() {
this._items = [];
this.observer.changed();
}
setItems(items) {
this._items = items;
this.observer.changed();
}
addItem(item) {
this._items.push(item);
this.observer.changed();
}
removeItem(item) {
this._items = _.without(this._items, item);
this.observer.changed();
}
getItems() {
this.observer.depend();
return this._items;
}
}
OHIF.ui.Dropdown = Dropdown;

View File

@ -1,29 +0,0 @@
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { _ } from 'meteor/underscore';
import { OHIF } from 'meteor/ohif:core';
OHIF.ui.showFormDropdown = (templateName, dropdownData) => {
// Check if the given template exists
const template = Template[templateName];
if (!template) {
throw {
name: 'TEMPLATE_NOT_FOUND',
message: `Template ${templateName} not found.`
};
}
// Prepare the method to destroy the view
let view;
const destroyView = () => Blaze.remove(view);
const templateData = _.extend({}, dropdownData, {
destroyView
});
// Render the dialog with the given template and data
view = Blaze.renderWithData(template, templateData, document.body);
// Return the dropdown element to enable position manipulation
const dropdown = view.firstNode();
return dropdown;
};

View File

@ -4,7 +4,7 @@ import './dialog/display.js';
import './dialog/spatial.js';
import './dialog/unsavedChangesDialog.js';
import './draggable/draggable.js';
import './dropdown/dropdown.js';
import './dropdown/form.js';
import './dropdown/class.js';
import './dropdown/display.js';
import './resizable/resizable.js';
import './unsavedChanges/unsavedChanges.js';

View File

@ -7,8 +7,8 @@ Template.header.onCreated(() => {
instance.dropdownItems = [];
instance.autorun(() => {
OHIF.header.dropdownObserver.depend();
instance.dropdownItems = OHIF.header.getDropdownItems();
OHIF.header.dropdown.observer.depend();
instance.dropdownItems = OHIF.header.dropdown.getItems();
});
});

View File

@ -1,35 +0,0 @@
import { Tracker } from 'meteor/tracker';
import { _ } from 'meteor/underscore';
export class Header {
constructor() {
this.dropdownObserver = new Tracker.Dependency();
this._dropdownItems = [];
this.dropdownObserver.changed();
}
clearDropDownItems() {
this._dropdownItems = [];
this.dropdownObserver.changed();
}
setDropdownItems(items) {
this._dropdownItems = items;
this.dropdownObserver.changed();
}
addDropdownItem(item) {
this._dropdownItems.push(item);
this.dropdownObserver.changed();
}
removeDropdownItem(item) {
this._dropdownItems = _.without(this._dropdownItems, item);
this.dropdownObserver.changed();
}
getDropdownItems() {
this.dropdownObserver.depend();
return this._dropdownItems;
}
}

View File

@ -1,10 +1,10 @@
import { OHIF } from 'meteor/ohif:core';
import { Header } from './client/lib/header';
/*
* Defines the base OHIF header object
*/
const header = new Header();
const dropdown = new OHIF.ui.Dropdown();
const header = { dropdown };
OHIF.header = header;

View File

@ -1,13 +1,10 @@
import { Template } from 'meteor/templating';
import { Random } from 'meteor/random';
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core';
// Use Aldeed's meteor-template-extension package to replace the
// default StudyListStudy template.
// See https://github.com/aldeed/meteor-template-extension
const defaultTemplate = 'studyContextMenu';
function getAssociationAssessment() {
/**
* Loads multiple unassociated studies in the Viewer
*/
const getAssociationAssessment = () => {
// default result value
const assessment = {
selected: 0,
@ -33,49 +30,40 @@ function getAssociationAssessment() {
}
return assessment;
}
};
Template.longitudinalStudyListContextMenu.helpers({
/**
* Loads multiple unassociated studies in the Viewer
*/
const viewStudies = () => {
OHIF.log.info('viewStudies');
const selectedStudies = OHIF.studylist.getSelectedStudies();
getAssociationClasses() {
const disabledClass = 'disabled';
let classList = '';
const assessment = getAssociationAssessment();
// if (assessment.selected < 1 || assessment.associated > 0) {
// TODO: REMOVE - Temporary for RSNA
if (assessment.selected < 1) {
classList += disabledClass;
}
return classList;
},
getRemoveAssociationClasses() {
const disabledClass = 'disabled';
let classList = '';
const assessment = getAssociationAssessment();
if (assessment.selected < 1 || assessment.selected !== assessment.associated) {
classList += disabledClass;
}
return classList;
if (!selectedStudies || !selectedStudies.length) {
return;
}
});
Template.longitudinalStudyListContextMenu.replaces(defaultTemplate);
Template[defaultTemplate].inheritsHelpersFrom('longitudinalStudyListContextMenu');
const title = selectedStudies[0].patientName;
const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid);
const contentId = 'viewerTab';
StudyList.functions.launchStudyAssociation = () => OHIF.ui.showDialog('dialogStudyAssociation');
StudyList.functions.removeTimepointAssociations = removeTimepointAssociations;
StudyList.functions.exportSelectedStudies = exportSelectedStudies;
StudyList.functions.viewStudies = viewStudies;
ViewerData = window.ViewerData || ViewerData;
// Update the ViewerData global object
ViewerData[contentId] = {
title: title,
contentId: contentId,
studyInstanceUids: studyInstanceUids
};
// Switch to the new tab
switchToTab(contentId);
};
/**
* Removes all present study / timepoint associations from the Clinical Trial
*/
function removeTimepointAssociations($study, event) {
const removeTimepointAssociations = event => {
const dialogSettings = {
title: 'Remove Association',
message: 'Measurements related to this Study and Timepoint will be erased. Do you really want to delete this association?',
@ -105,47 +93,44 @@ function removeTimepointAssociations($study, event) {
timepointApi.disassociateStudy(timepointIds, studyInstanceUid);
});
});
}
};
// ---------- TODO: Remove these duplicated functions below -------------
/**
* Exports all selected studies on the studylist
*/
function exportSelectedStudies() {
const selectedStudies = OHIF.studylist.getSelectedStudies();
if (!selectedStudies || !selectedStudies.length) {
return;
}
OHIF.studylist.exportStudies(selectedStudies);
}
/**
* Loads multiple unassociated studies in the Viewer
*/
function viewStudies() {
OHIF.log.info('viewStudies');
const selectedStudies = OHIF.studylist.getSelectedStudies();
if (!selectedStudies || !selectedStudies.length) {
return;
}
const title = selectedStudies[0].patientName;
const studyInstanceUids = selectedStudies.map(study => study.studyInstanceUid);
const contentId = 'viewerTab';
ViewerData = window.ViewerData || ViewerData;
// Update the ViewerData global object
ViewerData[contentId] = {
title: title,
contentId: contentId,
studyInstanceUids: studyInstanceUids
};
// Switch to the new tab
switchToTab(contentId);
}
Meteor.startup(() => {
OHIF.studylist.dropdown.setItems([{
action: viewStudies,
text: 'View',
separatorAfter: true
}, {
action: () => OHIF.ui.showDialog('dialogStudyAssociation'),
text: 'Associate',
disabled: () => {
const assessment = getAssociationAssessment();
return assessment.selected < 1;
}
}, {
action: removeTimepointAssociations,
text: 'Remove Association',
separatorAfter: true,
disabled: () => {
const assessment = getAssociationAssessment();
return assessment.selected < 1 || assessment.selected !== assessment.associated;
}
}, {
action: OHIF.studylist.viewSeriesDetails,
text: 'View Series Details'
}, {
text: 'Anonymize',
disabled: true
}, {
text: 'Send',
disabled: true,
separatorAfter: true
}, {
action: OHIF.studylist.exportSelectedStudies,
text: 'Export',
title: 'Export Selected Studies'
}, {
text: 'Delete',
disabled: true
}]);
});

View File

@ -3,8 +3,7 @@ import './longitudinalStudyListStudy/longitudinalStudyListStudy.html';
import './longitudinalStudyListStudy/longitudinalStudyListStudy.styl';
import './longitudinalStudyListStudy/longitudinalStudyListStudy.js';
import './longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.html';
import './longitudinalStudyListContextMenu/longitudinalStudyListContextMenu.js';
import './longitudinalViewportOverlay/longitudinalViewportOverlay.html';
import './longitudinalViewportOverlay/longitudinalViewportOverlay.js';
import './dropdown.js';

View File

@ -1,31 +0,0 @@
<template name="longitudinalStudyListContextMenu">
{{#dropdownForm}}
<li><a id="viewStudies" type="button" title="View Studies">View</a></li>
<li class="divider" role="separator"></li>
{{#let classes=getAssociationClasses}}
<li class="{{classes}}">
<a id="launchStudyAssociation" class="{{classes}}"
title="Launch Study Association">Associate</a>
</li>
{{/let}}
{{#let classes=getRemoveAssociationClasses}}
<li class="{{classes}}">
<a id="removeTimepointAssociations" type="button" class="{{classes}}"
title="Remove Timepoint Association">Remove Association</a>
</li>
{{/let}}
<li class="divider" role="separator"></li>
<li>
<a id="viewSeriesDetails" type="button"
title="View Series Details">View Series Details</a>
</li>
<li class="disabled"><a class="disabled">Anonymize</a></li>
<li class="disabled"><a class="disabled">Send</a></li>
<li class="divider" role="separator"></li>
<li><a id="exportSelectedStudies" title="Export Selected Studies">Export</a></li>
<li class="disabled"><a class="disabled">Delete</a></li>
{{/dropdownForm}}
</template>

View File

@ -1,5 +1,4 @@
import './seriesDetailsModal';
import './serverInformation';
import './studyContextMenu';
import './studylist';
import './themeSelector';

View File

@ -1,89 +1,23 @@
import { Template } from 'meteor/templating';
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core';
import { $ } from 'meteor/jquery';
/**
* This function is used inside the StudyList package to define a right click callback
*
* @param event
*/
openStudyContextMenu = event => {
if (!OHIF.uiSettings.studyListFunctionsEnabled) {
return;
}
StudyList.functions.exportSelectedStudies = exportSelectedStudies;
StudyList.functions.viewSeriesDetails = viewSeriesDetails;
Template.studyContextMenu.$study = $(event.currentTarget);
const dropdown = OHIF.ui.showFormDropdown('studyContextMenu');
const $dropdown = $(dropdown);
const $dropdownMenu = $dropdown.children('.dropdown-menu');
dropdown.oncontextmenu = () => false;
$dropdownMenu.css({
visibility: 'hidden',
position: 'fixed',
'z-index': 10000,
}).bounded().focus();
// Postpone position change to allow boundaries restriction
Meteor.defer(() => {
$dropdownMenu.css({
visibility: 'visible',
left: `${event.clientX}px`,
top: `${event.clientY}px`
}).trigger('spatialChanged');
});
};
/**
* Exports all selected studies on the studylist
*/
function exportSelectedStudies($study, event) {
const selectedStudies = OHIF.studylist.getSelectedStudies();
const studiesCount = selectedStudies.length;
const studyText = studiesCount > 1 ? 'Studies' : 'Study';
OHIF.ui.showDialog('dialogConfirm', {
element: event.element,
title: `Export ${studyText}`,
message: `Would you like to export ${studiesCount} ${studyText.toLowerCase()}?`
}).then(() => {
OHIF.studylist.exportStudies(selectedStudies);
});
}
/**
* Display series details of study in modal
*/
function viewSeriesDetails() {
const selectedStudies = OHIF.studylist.getSelectedStudies();
if (!selectedStudies) {
return;
}
OHIF.ui.showDialog('seriesDetailsModal', { selectedStudies });
}
Template.studyContextMenu.events({
'click a'(event, instance) {
const $target = $(event.currentTarget);
if ($target.hasClass('disabled')) {
return;
}
const id = $target.attr('id');
if (id in StudyList.functions) {
const fn = StudyList.functions[id];
if (typeof fn === 'function') {
fn(Template.studyContextMenu.$study, event);
}
}
}
Meteor.startup(() => {
OHIF.studylist.dropdown.setItems([{
action: OHIF.studylist.viewSeriesDetails,
text: 'View Series Details'
}, {
text: 'Anonymize',
disabled: true
}, {
text: 'Send',
disabled: true,
separatorAfter: true
}, {
text: 'Delete',
disabled: true
}, {
action: OHIF.studylist.exportSelectedStudies,
text: 'Export',
title: 'Export Selected Studies'
}]);
});

View File

@ -1,3 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { OHIF } from 'meteor/ohif:core';
// Maybe we should use regular StudyList collection?
@ -45,7 +48,7 @@ function doUnselectRow(studyRow, data) {
studyRow.removeClass('active');
}
function handleShiftClick(studyRow, data) {
function handleShiftClick($studyRow, data) {
//OHIF.log.info('shiftKey');
let study, previous = StudyList.previouslySelected ? $(StudyList.previouslySelected) : null;
@ -59,49 +62,49 @@ function handleShiftClick(studyRow, data) {
// Select all rows in between these two rows
if (previous) {
let rowsInBetween;
if (previous.index() < studyRow.index()) {
let $rowsInBetween;
if (previous.index() < $studyRow.index()) {
// The previously selected row is above (lower index) the
// currently selected row.
// Fill in the rows upwards from the previously selected row
rowsInBetween = previous.nextAll('tr');
} else if (previous.index() > studyRow.index()) {
$rowsInBetween = previous.nextAll('tr');
} else if (previous.index() > $studyRow.index()) {
// The previously selected row is below the currently
// selected row.
// Fill in the rows upwards from the previously selected row
rowsInBetween = previous.prevAll('tr');
$rowsInBetween = previous.prevAll('tr');
} else {
// nothing to do since previous.index() === studyRow.index()
// nothing to do since previous.index() === $studyRow.index()
// the user is shift-clicking the same row...
return;
}
// Loop through the rows in between current and previous selected studies
rowsInBetween.each(function() {
let row = $(this);
$rowsInBetween.forEach(row => {
const $row = $(row);
if (row.hasClass('active')) {
if ($row.hasClass('active')) {
// If we find one that is already selected, do nothing
return;
}
// Get the relevant studyInstanceUid
let studyInstanceUid = row.attr('studyInstanceUid');
let studyInstanceUid = $row.attr('studyInstanceUid');
// Retrieve the data context through Blaze
let data = Blaze.getData(this);
// Set the current study as selected
doSelectRow(row, data);
doSelectRow($row, data);
// When we reach the currently clicked-on row, stop the loop
return !row.is(studyRow);
// When we reach the currently clicked-on $row, stop the loop
return !$row.is($studyRow);
});
} else {
// Set the current study as selected
doSelectSingleRow(studyRow, data);
doSelectSingleRow($studyRow, data);
}
}
@ -139,59 +142,58 @@ Template.studylistStudy.onRendered(function() {
});
Template.studylistStudy.events({
'click tr.studylistStudy': function(e) {
var studyRow = $(e.currentTarget);
var data = this;
'click tr.studylistStudy'(event, instance) {
const $studyRow = $(event.currentTarget);
const data = instance.data;
// Remove the ID so we can directly insert this into our client-side collection
delete data._id;
if (e.shiftKey) {
handleShiftClick(studyRow, data);
} else if (e.ctrlKey || e.metaKey) {
handleCtrlClick(studyRow, data);
if (event.shiftKey) {
handleShiftClick($studyRow, data);
} else if (event.ctrlKey || event.metaKey) {
handleCtrlClick($studyRow, data);
} else {
doSelectSingleRow(studyRow, data);
doSelectSingleRow($studyRow, data);
}
},
'mousedown tr.studylistStudy': function(e) {
'mousedown tr.studylistStudy'(event, instance) {
// This event handler is meant to handle middle-click on a study
if (e.which !== 2) {
if (event.which !== 2) {
return;
}
var data = this;
var middleClickOnStudy = StudyList.callbacks.middleClickOnStudy;
const middleClickOnStudy = StudyList.callbacks.middleClickOnStudy;
if (middleClickOnStudy && typeof middleClickOnStudy === 'function') {
middleClickOnStudy(data);
middleClickOnStudy(instance.data);
}
},
'dblclick tr.studylistStudy, doubletap tr.studylistStudy': function(e) {
if (e.which !== undefined && e.which !== 1) {
'dblclick tr.studylistStudy, doubletap tr.studylistStudy'(event, instance) {
if (event.which !== undefined && event.which !== 1) {
return;
}
var data = this;
var dblClickOnStudy = StudyList.callbacks.dblClickOnStudy;
const dblClickOnStudy = StudyList.callbacks.dblClickOnStudy;
if (dblClickOnStudy && typeof dblClickOnStudy === 'function') {
dblClickOnStudy(data);
dblClickOnStudy(instance.data);
}
},
'contextmenu tr.studylistStudy, press tr.studylistStudy': function(e, template) {
var studyRow = $(e.currentTarget),
data = this;
'contextmenu tr.studylistStudy, press tr.studylistStudy'(event, instance) {
const $studyRow = $(event.currentTarget);
if (!isStudySelected(data)) {
doSelectSingleRow(studyRow, data);
if (!isStudySelected(instance.data)) {
doSelectSingleRow($studyRow, instance.data);
}
if (typeof openStudyContextMenu === 'function') {
e.preventDefault();
openStudyContextMenu(e, template);
return false;
}
event.preventDefault();
OHIF.ui.showDropdown(OHIF.studylist.dropdown.getItems(), {
event,
menuClasses: 'dropdown-menu-left'
});
return false;
}
});

View File

@ -0,0 +1,25 @@
import { Meteor } from 'meteor/meteor';
import { OHIF } from 'meteor/ohif:core';
Meteor.startup(() => {
OHIF.studylist.dropdown = new OHIF.ui.Dropdown();
OHIF.studylist.dropdown.setItems([{
action: OHIF.studylist.viewSeriesDetails,
text: 'View Series Details'
}, {
text: 'Anonymize',
disabled: true
}, {
text: 'Send',
disabled: true,
separatorAfter: true
}, {
text: 'Delete',
disabled: true
}, {
action: OHIF.studylist.exportSelectedStudies,
text: 'Export',
title: 'Export Selected Studies'
}]);
});

View File

@ -1,3 +1,4 @@
import './collections';
import './lib';
import './components';
import './dropdown';

View File

@ -0,0 +1,19 @@
import { OHIF } from 'meteor/ohif:core';
/**
* Exports all selected studies on the studylist
* @param event Event that triggered the export
*/
OHIF.studylist.exportSelectedStudies = event => {
const selectedStudies = OHIF.studylist.getSelectedStudies();
const studiesCount = selectedStudies.length;
const studyText = studiesCount > 1 ? 'Studies' : 'Study';
OHIF.ui.showDialog('dialogConfirm', {
element: event.element,
title: `Export ${studyText}`,
message: `Would you like to export ${studiesCount} ${studyText.toLowerCase()}?`
}).then(() => {
OHIF.studylist.exportStudies(selectedStudies);
});
};

View File

@ -1,5 +1,6 @@
import './third-party/jquery.twbsPagination.min.js';
import './exportSelectedStudies.js';
import './exportStudies.js';
import './getSelectedStudies.js';
import './getStudyMetadata.js';
@ -10,3 +11,4 @@ import './queryStudies.js';
import './retrieveStudyMetadata.js';
import './studylist.js';
import './switchToTab.js';
import './viewSeriesDetails.js';

View File

@ -0,0 +1,10 @@
import { OHIF } from 'meteor/ohif:core';
/**
* Display series details of study in a modal
*/
OHIF.studylist.viewSeriesDetails = () => {
const selectedStudies = OHIF.studylist.getSelectedStudies();
if (!selectedStudies) return;
OHIF.ui.showDialog('seriesDetailsModal', { selectedStudies });
};