Fix double-click to enlarge viewport bugs. Add annotation dialog box to OHIF Viewer

This commit is contained in:
Erik Ziegler committed 2016-08-05 12:16:47 +02:00
1 parent f963fec586
commit 81b011f111
15 files changed
+773 -1214

No files matched your search

@@ -0,0 +1,411 @@
var dialogPolyfill = (function() {
var supportCustomEvent = window.CustomEvent;
if (!supportCustomEvent || typeof supportCustomEvent == "object") {
supportCustomEvent = function CustomEvent(event, x) {
x = x || {};
var ev = document.createEvent('CustomEvent');
ev.initCustomEvent(event, !!x.bubbles, !!x.cancelable, x.detail || null);
return ev;
};
supportCustomEvent.prototype = window.Event.prototype;
}
/**
* Finds the nearest <dialog> from the passed element.
*
* @param {Element} el to search from
* @param {HTMLDialogElement} dialog found
*/
function findNearestDialog(el) {
while (el) {
if (el.nodeName == 'DIALOG') {
return el;
}
el = el.parentElement;
}
return null;
}
var dialogPolyfill = {};
dialogPolyfill.reposition = function(element) {
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
var topValue = scrollTop + (window.innerHeight - element.offsetHeight) / 2;
element.style.top = Math.max(0, topValue) + 'px';
element.dialogPolyfillInfo.isTopOverridden = true;
};
dialogPolyfill.inNodeList = function(nodeList, node) {
for (var i = 0; i < nodeList.length; ++i) {
if (nodeList[i] == node)
return true;
}
return false;
};
dialogPolyfill.isInlinePositionSetByStylesheet = function(element) {
for (var i = 0; i < document.styleSheets.length; ++i) {
var styleSheet = document.styleSheets[i];
var cssRules = null;
// Some browsers throw on cssRules.
try {
cssRules = styleSheet.cssRules;
} catch (e) {}
if (!cssRules)
continue;
for (var j = 0; j < cssRules.length; ++j) {
var rule = cssRules[j];
var selectedNodes = null;
// Ignore errors on invalid selector texts.
try {
selectedNodes = document.querySelectorAll(rule.selectorText);
} catch(e) {}
if (!selectedNodes || !dialogPolyfill.inNodeList(selectedNodes, element))
continue;
var cssTop = rule.style.getPropertyValue('top');
var cssBottom = rule.style.getPropertyValue('bottom');
if ((cssTop && cssTop != 'auto') || (cssBottom && cssBottom != 'auto'))
return true;
}
}
return false;
};
dialogPolyfill.needsCentering = function(dialog) {
var computedStyle = window.getComputedStyle(dialog);
if (computedStyle.position != 'absolute') {
return false;
}
// We must determine whether the top/bottom specified value is non-auto. In
// WebKit/Blink, checking computedStyle.top == 'auto' is sufficient, but
// Firefox returns the used value. So we do this crazy thing instead: check
// the inline style and then go through CSS rules.
if ((dialog.style.top != 'auto' && dialog.style.top != '') ||
(dialog.style.bottom != 'auto' && dialog.style.bottom != ''))
return false;
return !dialogPolyfill.isInlinePositionSetByStylesheet(dialog);
};
dialogPolyfill.showDialog = function(isModal) {
if (this.open) {
throw 'InvalidStateError: showDialog called on open dialog';
}
this.open = true; // TODO: should be a getter mapped to attribute
this.setAttribute('open', 'open');
if (isModal) {
// Find element with `autofocus` attribute or first form control
var first_form_ctrl = null;
var autofocus = null;
var findElementToFocus = function(root) {
if (!root.children) {
return;
}
for (var i = 0; i < root.children.length; i++) {
var elem = root.children[i];
if (first_form_ctrl === null && !elem.disabled && (
elem.nodeName == 'BUTTON' ||
elem.nodeName == 'INPUT' ||
elem.nodeName == 'KEYGEN' ||
elem.nodeName == 'SELECT' ||
elem.nodeName == 'TEXTAREA')) {
first_form_ctrl = elem;
}
if (elem.autofocus) {
autofocus = elem;
return;
}
findElementToFocus(elem);
if (autofocus !== null) return;
}
};
findElementToFocus(this);
if (autofocus !== null) {
autofocus.focus();
} else if (first_form_ctrl !== null) {
first_form_ctrl.focus();
}
}
if (dialogPolyfill.needsCentering(this))
dialogPolyfill.reposition(this);
if (isModal) {
this.dialogPolyfillInfo.modal = true;
dialogPolyfill.dm.pushDialog(this);
}
// IE sometimes complains when calling .focus() that it
// "Can't move focus to the control because it is invisible, not enabled, or of a type that does not accept the focus."
try {
if (autofocus !== null) {
autofocus.focus();
} else if (first_form_ctrl !== null) {
first_form_ctrl.focus();
}
} catch(e) {}
this.style.zoom = 1;
};
dialogPolyfill.close = function(retval) {
if (!this.open && !window.HTMLDialogElement) {
// Native implementations will set .open to false, so ignore this error.
throw 'InvalidStateError: close called on closed dialog';
}
this.open = false;
this.removeAttribute('open');
// Leave returnValue untouched in case it was set directly on the element
if (typeof retval != 'undefined') {
this.returnValue = retval;
}
// This won't match the native <dialog> exactly because if the user sets top
// on a centered polyfill dialog, that top gets thrown away when the dialog is
// closed. Not sure it's possible to polyfill this perfectly.
if (this.dialogPolyfillInfo.isTopOverridden) {
this.style.top = 'auto';
}
if (this.dialogPolyfillInfo.modal) {
dialogPolyfill.dm.removeDialog(this);
}
// Triggering "close" event for any attached listeners on the <dialog>
var event;
if (document.createEvent) {
event = document.createEvent('HTMLEvents');
event.initEvent('close', true, true);
} else {
event = new Event('close');
}
this.dispatchEvent(event);
return this.returnValue;
};
dialogPolyfill.registerDialog = function(element) {
if (element.show) {
console.warn("This browser already supports <dialog>, the polyfill " +
"may not work correctly.");
}
element.show = dialogPolyfill.showDialog.bind(element, false);
element.showModal = dialogPolyfill.showDialog.bind(element, true);
element.close = dialogPolyfill.close.bind(element);
element.dialogPolyfillInfo = {};
element.open = false;
};
// The overlay is used to simulate how a modal dialog blocks the document. The
// blocking dialog is positioned on top of the overlay, and the rest of the
// dialogs on the pending dialog stack are positioned below it. In the actual
// implementation, the modal dialog stacking is controlled by the top layer,
// where z-index has no effect.
var TOP_LAYER_ZINDEX = 100000;
var MAX_PENDING_DIALOGS = 100000;
dialogPolyfill.DialogManager = function() {
this.pendingDialogStack = [];
this.overlay = document.createElement('div');
this.overlay.style.width = '100%';
this.overlay.style.height = '100%';
this.overlay.style.position = 'fixed';
this.overlay.style.left = '0px';
this.overlay.style.top = '0px';
this.overlay.style.backgroundColor = 'rgba(0,0,0,0.0)';
this.focusPageLast = this.createFocusable();
this.overlay.appendChild(this.focusPageLast);
this.overlay.addEventListener('click', function(e) {
var redirectedEvent = document.createEvent('MouseEvents');
redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
document.body.dispatchEvent(redirectedEvent);
});
// TODO: Only install when any dialogs are open.
document.addEventListener('submit', function(ev) {
var method = ev.target.getAttribute('method').toLowerCase();
if (method != 'dialog') { return; }
ev.preventDefault();
var dialog = findNearestDialog(ev.target);
if (!dialog) { return; }
// FIXME: The original event doesn't contain the INPUT element used to
// submit the form (if any). Look in some possible places.
var returnValue;
var cands = [document.activeElement, ev.explicitOriginalTarget];
cands.some(function(cand) {
if (cand && cand.nodeName == 'INPUT' && cand.form == ev.target) {
returnValue = cand.value;
return true;
}
});
dialog.close(returnValue);
}, true);
};
dialogPolyfill.DialogManager.prototype.createFocusable = function(tabIndex) {
var span = document.createElement('span');
span.tabIndex = tabIndex || 0;
span.style.opacity = 0;
span.style.position = 'static';
return span;
};
dialogPolyfill.DialogManager.prototype.blockDocument = function() {
if (!document.body.contains(this.overlay)) {
document.body.appendChild(this.overlay);
// On Safari/Mac (and possibly other browsers), the documentElement is
// not focusable. This is required for modal dialogs as it is the first
// element to be hit by a tab event, and further tabs are redirected to
// the most visible dialog.
if (this.needsDocumentElementFocus === undefined) {
document.documentElement.focus();
this.needsDocumentElementFocus =
(document.activeElement != document.documentElement);
}
if (this.needsDocumentElementFocus) {
document.documentElement.tabIndex = 1;
}
}
};
dialogPolyfill.DialogManager.prototype.unblockDocument = function() {
document.body.removeChild(this.overlay);
if (this.needsDocumentElementFocus) {
// TODO: Restore the previous tabIndex, rather than clearing it.
document.documentElement.tabIndex = '';
}
};
dialogPolyfill.DialogManager.prototype.updateStacking = function() {
if (this.pendingDialogStack.length == 0) {
this.unblockDocument();
return;
}
this.blockDocument();
var zIndex = TOP_LAYER_ZINDEX;
for (var i = 0; i < this.pendingDialogStack.length; i++) {
if (i == this.pendingDialogStack.length - 1)
this.overlay.style.zIndex = zIndex++;
var dialog = this.pendingDialogStack[i];
dialog.dialogPolyfillInfo.backdrop.style.zIndex = zIndex++;
dialog.style.zIndex = zIndex++;
}
};
dialogPolyfill.DialogManager.prototype.handleKey = function(event) {
var dialogCount = this.pendingDialogStack.length;
if (dialogCount == 0) {
return;
}
var dialog = this.pendingDialogStack[dialogCount - 1];
var pfi = dialog.dialogPolyfillInfo;
switch (event.keyCode) {
case 9: /* tab */
var activeElement = document.activeElement;
var forward = !event.shiftKey;
if (forward) {
// Tab forward, so look for document or fake last focus element.
if (activeElement == document.documentElement ||
activeElement == document.body ||
activeElement == pfi.backdrop) {
pfi.focusFirst.focus();
} else if (activeElement == pfi.focusLast) {
// TODO: Instead of wrapping to focusFirst, escape to browser chrome.
pfi.focusFirst.focus();
}
} else {
// Tab backwards, so look for fake first focus element.
if (activeElement == pfi.focusFirst) {
// TODO: Instead of wrapping to focusLast, escape to browser chrome.
pfi.focusLast.focus();
} else if (activeElement == this.focusPageLast) {
// The focus element is at the end of the page (e.g., shift-tab from
// the window chrome): move current focus to the last element in the
// dialog instead.
pfi.focusLast.focus();
}
}
break;
case 27: /* esc */
event.preventDefault();
event.stopPropagation();
var cancelEvent = new supportCustomEvent('cancel', {
bubbles: false,
cancelable: true
});
if (dialog.dispatchEvent(cancelEvent)) {
dialog.close();
}
break;
}
};
dialogPolyfill.DialogManager.prototype.pushDialog = function(dialog) {
if (this.pendingDialogStack.length >= MAX_PENDING_DIALOGS) {
throw "Too many modal dialogs";
}
var backdrop = document.createElement('div');
backdrop.className = 'backdrop';
var clickEventListener = function(e) {
var redirectedEvent = document.createEvent('MouseEvents');
redirectedEvent.initMouseEvent(e.type, e.bubbles, e.cancelable, window,
e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey,
e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget);
dialog.dispatchEvent(redirectedEvent);
};
backdrop.addEventListener('click', clickEventListener);
dialog.parentNode.insertBefore(backdrop, dialog.nextSibling);
dialog.dialogPolyfillInfo.backdrop = backdrop;
dialog.dialogPolyfillInfo.clickEventListener = clickEventListener;
this.pendingDialogStack.push(dialog);
this.updateStacking();
dialog.dialogPolyfillInfo.focusFirst = this.createFocusable();
dialog.dialogPolyfillInfo.focusLast = this.createFocusable();
dialog.appendChild(dialog.dialogPolyfillInfo.focusLast);
dialog.insertBefore(
dialog.dialogPolyfillInfo.focusFirst, dialog.firstChild);
};
dialogPolyfill.DialogManager.prototype.removeDialog = function(dialog) {
var index = this.pendingDialogStack.indexOf(dialog);
if (index == -1) {
return;
}
this.pendingDialogStack.splice(index, 1);
var backdrop = dialog.dialogPolyfillInfo.backdrop;
var clickEventListener = dialog.dialogPolyfillInfo.clickEventListener;
backdrop.removeEventListener('click', clickEventListener);
backdrop.parentNode.removeChild(backdrop);
dialog.dialogPolyfillInfo.backdrop = null;
dialog.dialogPolyfillInfo.clickEventListener = null;
this.updateStacking();
dialog.removeChild(dialog.dialogPolyfillInfo.focusFirst);
dialog.removeChild(dialog.dialogPolyfillInfo.focusLast);
dialog.dialogPolyfillInfo.focusFirst = null;
dialog.dialogPolyfillInfo.focusLast = null;
};
dialogPolyfill.dm = new dialogPolyfill.DialogManager();
document.addEventListener('keydown',
dialogPolyfill.dm.handleKey.bind(dialogPolyfill.dm));
return dialogPolyfill;
})();
@@ -0,0 +1,34 @@
dialog
position: absolute
left: 0
right: 0
width: -moz-fit-content
width: -webkit-fit-content
width: fit-content
height: -moz-fit-content
height: -webkit-fit-content
height: fit-content
margin: auto
border: solid
padding: 1em
background: white
color: black
display: none
dialog[open]
display: block
dialog + .backdrop
position: fixed
top: 0
right: 0
bottom: 0
left: 0
background: rgba(0,0,0,0.1)
/* for small devices, modal dialogs go full-screen */
@media screen and (max-width: 540px)
dialog[_polyfill_modal]
top: 0
width: auto
margin: 1em
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,33 @@
<template name="annotationDialogs">
<dialog id="annotationDialog"
class="annotationDialog noselect">
<h5>Enter your annotation</h5>
<div class="annotationTextInputOptions">
<label for="annotationTextInput">New label</label>
<input name="annotationTextInput"
class="annotationTextInput"
type="text"
tabindex="-1"
autocomplete="off"/>
</div>
<a class="annotationDialogConfirm btn btn-sm btn-primary">OK</a>
</dialog>
<dialog id="relabelAnnotationDialog"
class="annotationDialog noselect"
oncontextmenu="return false">
<h5>Edit your annotation</h5>
<div class="annotationTextInputOptions">
<label for="annotationTextInput">New label</label>
<input name="annotationTextInput"
class="annotationTextInput"
type="text"
tabindex="-1"
autocomplete="off"/>
</div>
<div>
<a class="relabelRemove btn btn-sm btn-secondary">Remove marker</a>
<a class="relabelConfirm btn btn-sm btn-primary">OK</a>
</div>
</dialog>
</template>
@@ -0,0 +1,126 @@
// ------ Tool configuration functions ----- //
getAnnotationTextCallback = function(doneChangingTextCallback) {
// This handles the text entry for the annotation tool
function keyPressHandler(e) {
// If Enter or Esc are pressed, close the dialog
if (e.which === 13 || e.which === 27) {
closeHandler();
}
}
function closeHandler() {
dialog.get(0).close();
doneChangingTextCallback(getTextInput.val());
// Reset the text value
getTextInput.val('');
// Reset the focus to the active viewport element
// This makes the mobile Safari keyboard close
const element = getActiveViewportElement();
$(element).focus();
}
const dialog = $('#annotationDialog');
if (dialog.get(0).open === true) {
return;
}
const getTextInput = $('.annotationTextInput');
// Focus on the text input to open the Safari keyboard
getTextInput.focus();
dialog.get(0).showModal();
const confirm = dialog.find('.annotationDialogConfirm');
confirm.off('click');
confirm.on('click', () => {
closeHandler();
});
// Use keydown since keypress doesn't handle ESC in Chrome
dialog.off('keydown');
dialog.on('keydown', keyPressHandler);
};
changeAnnotationTextCallback = function(data, eventData, doneChangingTextCallback) {
const dialog = $('#relabelAnnotationDialog');
if (dialog.get(0).open === true) {
return;
}
if (isTouchDevice()) {
// Center the dialog on screen on touch devices
dialog.css({
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 'auto'
});
} else {
// Place the dialog above the tool that is being relabelled
dialog.css({
top: eventData.currentPoints.page.y - dialog.outerHeight() - 20,
left: eventData.currentPoints.page.x - dialog.outerWidth() / 2
});
}
const getTextInput = dialog.find('.annotationTextInput');
const confirm = dialog.find('.relabelConfirm');
const remove = dialog.find('.relabelRemove');
getTextInput.val(data.text);
// Focus on the text input to open the Safari keyboard
getTextInput.focus();
dialog.get(0).showModal();
confirm.off('click');
confirm.on('click', function() {
dialog.get(0).close();
doneChangingTextCallback(data, getTextInput.val());
});
// If the remove button is clicked, delete this marker
remove.off('click');
remove.on('click', function() {
dialog.get(0).close();
doneChangingTextCallback(data, undefined, true);
});
dialog.off('keydown');
dialog.on('keydown', keyPressHandler);
function keyPressHandler(e) {
// If Enter is pressed, close the dialog
if (e.which === 13) {
closeHandler();
}
}
function closeHandler() {
dialog.get(0).close();
doneChangingTextCallback(data, getTextInput.val());
// Reset the text value
getTextInput.val('');
// Reset the focus to the active viewport element
// This makes the mobile Safari keyboard close
const element = getActiveViewportElement();
$(element).focus();
}
};
Template.annotationDialogs.onRendered(() => {
const instance = Template.instance();
const dialogIds = ['annotationDialog', 'relabelAnnotationDialog'];
dialogIds.forEach(id => {
const dialog = instance.$('#' + id);
dialog.draggable();
dialogPolyfill.registerDialog(dialog.get(0));
});
});
@@ -0,0 +1,34 @@
@import "{design}/app.styl"
.annotationDialog
display: none
z-index: 1000
position: absolute
top: 0
bottom: 0
left: 0
right: 0
margin: auto
overflow: hidden
padding: 10px
width: 300px
height: 140px
opacity: 0.95
border-radius: 8px
border: 1px solid $uiBorderColor
background: $uiGrayDarkest
color: $textSecondaryColor
h5, label
font-weight: 100
.annotationTextInputOptions
padding: 10px 0
.annotationTextInput
margin-left: 5px
.annotationDialogConfirm
float: right
@@ -1,7 +1,5 @@
@import "{design}/app.styl"
$borderColor = rgba(77, 99, 110, 0.81)
#cineDialog
display: none
z-index: 1000
@@ -564,14 +564,17 @@ Template.imageViewerViewport.onDestroyed(function() {
Template.imageViewerViewport.events({
'ActivateViewport .imageViewerViewport': function(e) {
console.log('activateViewport');
log.info('imageViewerViewport ActivateViewport');
setActiveViewport(e.currentTarget);
},
'click .imageViewerViewport': function(e) {
console.log('click imageViewerViewport handler');
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
Session.set('activeViewport', viewportIndex);
},
'dblclick .imageViewerViewport': function(e) {
'CornerstoneToolsMouseDoubleClick .imageViewerViewport, CornerstoneToolsDoubleTap .imageViewerViewport': function(e) {
console.log('CornerstoneToolsMouseDoubleClick handler');
var viewportIndex = $('.imageViewerViewport').index(e.currentTarget);
layoutManager.toggleEnlargement(viewportIndex);
}