diff --git a/OHIFViewer/client/components/viewer/viewer.html b/OHIFViewer/client/components/viewer/viewer.html
index e1d7a79b5..cd9d8fb68 100644
--- a/OHIFViewer/client/components/viewer/viewer.html
+++ b/OHIFViewer/client/components/viewer/viewer.html
@@ -3,6 +3,7 @@
{{> cineDialog}}
{{> layoutChooser }}
+ {{> annotationDialogs }}
{{ >ruleEntryDialog }}
diff --git a/Packages/cornerstone/client/cornerstoneTools.js b/Packages/cornerstone/client/cornerstoneTools.js
index 62dfc9fa1..31d2a7893 100644
--- a/Packages/cornerstone/client/cornerstoneTools.js
+++ b/Packages/cornerstone/client/cornerstoneTools.js
@@ -1,4 +1,4 @@
-/*! cornerstoneTools - v0.7.8 - 2016-05-17 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */
+/*! cornerstoneTools - v0.7.8 - 2016-08-05 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneTools */
// Begin Source: src/header.js
if (typeof cornerstone === 'undefined') {
cornerstone = {};
@@ -22,64 +22,39 @@ if (typeof cornerstoneTools === 'undefined') {
'use strict';
- var scrollTimeout;
- var scrollTimeoutDelay = 1;
-
function mouseWheel(e) {
- clearTimeout(scrollTimeout);
+ // !!!HACK/NOTE/WARNING!!!
+ // for some reason I am getting mousewheel and DOMMouseScroll events on my
+ // mac os x mavericks system when middle mouse button dragging.
+ // I couldn't find any info about this so this might break other systems
+ // webkit hack
+ if (e.originalEvent.type === 'mousewheel' && e.originalEvent.wheelDeltaY === 0) {
+ return;
+ }
+ // firefox hack
+ if (e.originalEvent.type === 'DOMMouseScroll' && e.originalEvent.axis === 1) {
+ return;
+ }
- scrollTimeout = setTimeout(function() {
- // !!!HACK/NOTE/WARNING!!!
- // for some reason I am getting mousewheel and DOMMouseScroll events on my
- // mac os x mavericks system when middle mouse button dragging.
- // I couldn't find any info about this so this might break other systems
- // webkit hack
- if (e.originalEvent.type === 'mousewheel' && e.originalEvent.wheelDeltaY === 0) {
- return;
- }
- // firefox hack
- if (e.originalEvent.type === 'DOMMouseScroll' && e.originalEvent.axis === 1) {
- return;
- }
+ var element = e.currentTarget;
+ var startingCoords = cornerstone.pageToPixel(element, e.pageX || e.originalEvent.pageX, e.pageY || e.originalEvent.pageY);
- var element = e.currentTarget;
+ e = window.event || e; // old IE support
+ var wheelDelta = e.wheelDelta || -e.detail || -e.originalEvent.detail;
+ var direction = Math.max(-1, Math.min(1, (wheelDelta)));
- var x;
- var y;
+ var mouseWheelData = {
+ element: element,
+ viewport: cornerstone.getViewport(element),
+ image: cornerstone.getEnabledElement(element).image,
+ direction: direction,
+ pageX: e.pageX || e.originalEvent.pageX,
+ pageY: e.pageY || e.originalEvent.pageY,
+ imageX: startingCoords.x,
+ imageY: startingCoords.y
+ };
- if (e.pageX !== undefined && e.pageY !== undefined) {
- x = e.pageX;
- y = e.pageY;
- } else if (e.originalEvent &&
- e.originalEvent.pageX !== undefined &&
- e.originalEvent.pageY !== undefined) {
- x = e.originalEvent.pageX;
- y = e.originalEvent.pageY;
- } else {
- // IE9 & IE10
- x = e.x;
- y = e.y;
- }
-
- var startingCoords = cornerstone.pageToPixel(element, x, y);
-
- e = window.event || e; // old IE support
- var wheelDelta = e.wheelDelta || -e.detail || -e.originalEvent.detail || -e.originalEvent.deltaY;
- var direction = Math.max(-1, Math.min(1, (wheelDelta)));
-
- var mouseWheelData = {
- element: element,
- viewport: cornerstone.getViewport(element),
- image: cornerstone.getEnabledElement(element).image,
- direction: direction,
- pageX: x,
- pageY: y,
- imageX: startingCoords.x,
- imageY: startingCoords.y
- };
-
- $(element).trigger('CornerstoneToolsMouseWheel', mouseWheelData);
- }, scrollTimeoutDelay);
+ $(element).trigger('CornerstoneToolsMouseWheel', mouseWheelData);
}
var mouseWheelEvents = 'mousewheel DOMMouseScroll';
@@ -2138,7 +2113,6 @@ if (typeof cornerstoneTools === 'undefined') {
$(mouseEventData.element).on('CornerstoneToolsMouseMove', eventData, cornerstoneTools.arrowAnnotate.mouseMoveCallback);
$(mouseEventData.element).on('CornerstoneToolsMouseDown', eventData, cornerstoneTools.arrowAnnotate.mouseDownCallback);
$(mouseEventData.element).on('CornerstoneToolsMouseDownActivate', eventData, cornerstoneTools.arrowAnnotate.mouseDownActivateCallback);
- $(mouseEventData.element).on('CornerstoneToolsMouseDoubleClick', eventData, cornerstoneTools.arrowAnnotate.mouseDoubleClickCallback);
}
// associate this data with this imageId so we can render it and manipulate it
@@ -2149,7 +2123,6 @@ if (typeof cornerstoneTools === 'undefined') {
$(mouseEventData.element).off('CornerstoneToolsMouseMove', cornerstoneTools.arrowAnnotate.mouseMoveCallback);
$(mouseEventData.element).off('CornerstoneToolsMouseDown', cornerstoneTools.arrowAnnotate.mouseDownCallback);
$(mouseEventData.element).off('CornerstoneToolsMouseDownActivate', cornerstoneTools.arrowAnnotate.mouseDownActivateCallback);
- $(mouseEventData.element).off('CornerstoneToolsMouseDoubleClick', cornerstoneTools.arrowAnnotate.mouseDoubleClickCallback);
cornerstone.updateImage(mouseEventData.element);
cornerstoneTools.moveNewHandle(mouseEventData, toolType, measurementData, measurementData.handles.end, function() {
@@ -2429,7 +2402,7 @@ if (typeof cornerstoneTools === 'undefined') {
// now check to see if there is a handle we can move
if (!toolData) {
- return false;
+ return;
}
for (var i = 0; i < toolData.data.length; i++) {
@@ -2445,8 +2418,6 @@ if (typeof cornerstoneTools === 'undefined') {
return false;
}
}
-
- return false; // false = causes jquery to preventDefault() and stopPropagation() this event
}
function pressCallback(e, eventData) {
@@ -3415,7 +3386,7 @@ if (typeof cornerstoneTools === 'undefined') {
};
// First we calculate the ellipse points (top, left, right, and bottom)
- var ellipsePoints = [ {
+ var ellipsePoints = [{
// Top middle point of ellipse
x: leftCanvas + widthCanvas / 2,
y: topCanvas
@@ -3431,14 +3402,14 @@ if (typeof cornerstoneTools === 'undefined') {
// Right middle point of ellipse
x: leftCanvas + widthCanvas,
y: topCanvas + heightCanvas / 2
- } ];
+ }];
// We obtain the link starting point by finding the closest point on the ellipse to the
// center of the textbox
link.start = cornerstoneMath.point.findClosestPoint(ellipsePoints, link.end);
// Next we calculate the corners of the textbox bounding box
- var boundingBoxPoints = [ {
+ var boundingBoxPoints = [{
// Top middle point of bounding box
x: boundingBox.left + boundingBox.width / 2,
y: boundingBox.top
@@ -3464,7 +3435,7 @@ if (typeof cornerstoneTools === 'undefined') {
context.beginPath();
context.strokeStyle = color;
context.lineWidth = lineWidth;
- context.setLineDash([ 2, 3 ]);
+ context.setLineDash([2, 3]);
context.moveTo(link.start.x, link.start.y);
context.lineTo(link.end.x, link.end.y);
context.stroke();
@@ -3489,8 +3460,7 @@ if (typeof cornerstoneTools === 'undefined') {
toolType: toolType
});
-})($, cornerstone, cornerstoneMath, cornerstoneTools);
-
+})($, cornerstone, cornerstoneMath, cornerstoneTools);
// End Source; src/imageTools/ellipticalRoi.js
// Begin Source: src/imageTools/freehand.js
@@ -5636,8 +5606,7 @@ if (typeof cornerstoneTools === 'undefined') {
x: mouseEventData.currentPoints.image.x,
y: mouseEventData.currentPoints.image.y,
highlight: true,
- active: true,
- hasBoundingBox: true
+ active: true
}
}
};
@@ -5686,13 +5655,12 @@ if (typeof cornerstoneTools === 'undefined') {
///////// BEGIN IMAGE RENDERING ///////
function pointNearTool(element, data, coords) {
- if (!data.handles.end.boundingBox) {
+ if (!data.textBoundingBox) {
return;
}
- var distanceToPoint = cornerstoneMath.rect.distanceToPoint(data.handles.end.boundingBox, coords);
- var insideBoundingBox = cornerstoneTools.pointInsideBoundingBox(data.handles.end, coords);
- return (distanceToPoint < 10) || insideBoundingBox;
+ var distanceToPoint = cornerstoneMath.rect.distanceToPoint(data.textBoundingBox, coords);
+ return (distanceToPoint < 10);
}
function onImageRendered(e, eventData) {
@@ -5739,7 +5707,7 @@ if (typeof cornerstoneTools === 'undefined') {
};
var boundingBox = cornerstoneTools.drawTextBox(context, data.text, textCoords.x, textCoords.y - 10, color, options);
- data.handles.end.boundingBox = boundingBox;
+ data.textBoundingBox = boundingBox;
context.restore();
}
@@ -6379,94 +6347,118 @@ if (typeof cornerstoneTools === 'undefined') {
// Now that the scale has been updated, determine the offset we need to apply to the center so we can
// keep the original start location in the same position
var newCoords = cornerstone.pageToPixel(element, eventData.startPoints.page.x, eventData.startPoints.page.y);
+
+ // The shift we will use is the difference between the original image coordinates of the point we've selected
+ // and the image coordinates of the same point on the page after the viewport scaling above has been performed
+ // This shift is in image coordinates, and is designed to keep the target location fixed on the page.
var shift = {
x: eventData.startPoints.image.x - newCoords.x,
y: eventData.startPoints.image.y - newCoords.y
};
+ // Correct the required shift using the viewport rotation and flip parameters
shift = correctShift(shift, viewport);
+
+ // Apply the shift to the Viewport's translation setting
viewport.translation.x -= shift.x;
viewport.translation.y -= shift.y;
+
+ // Update the Viewport with the new translation value
cornerstone.setViewport(element, viewport);
}
function translateStrategy(eventData, ticks) {
var element = eventData.element;
var image = eventData.image;
+ var config = cornerstoneTools.zoom.getConfiguration();
// Calculate the new scale factor based on how far the mouse has changed
+ // Note that in this case we don't need to update the viewport after the initial
+ // zoom step since we aren't don't intend to keep the target position static on
+ // the page
var viewport = changeViewportScale(eventData.viewport, ticks);
- cornerstone.setViewport(element, viewport);
- var config = cornerstoneTools.zoom.getConfiguration();
- var shift,
- newCoords;
+ // Define the default shift to take place during this zoom step
+ var shift = {
+ x: 0,
+ y: 0
+ };
- var outwardsTranslateSpeed = 8;
- var inwardsTranslateSpeed = 8;
+ // Define the parameters for the translate strategy
+ var translateSpeed = 8;
var outwardsMinScaleToTranslate = 3;
var minTranslation = 0.01;
if (ticks < 0) {
// Zoom outwards from the image center
- shift = {
- x: viewport.scale < outwardsMinScaleToTranslate ? viewport.translation.x / outwardsTranslateSpeed : 0,
- y: viewport.scale < outwardsMinScaleToTranslate ? viewport.translation.y / outwardsTranslateSpeed : 0
- };
-
- if (Math.abs(viewport.translation.x) < minTranslation) {
- viewport.translation.x = 0;
- shift.x = 0;
- } else if (Math.abs(viewport.translation.y) < minTranslation) {
- viewport.translation.y = 0;
- shift.y = 0;
- } else if (Math.abs(viewport.translation.x) < minTranslation &&
- Math.abs(viewport.translation.y) < minTranslation) {
- cornerstone.setViewport(element, viewport);
- return false;
+ if (viewport.scale < outwardsMinScaleToTranslate) {
+ // If the current translation is smaller than the minimum desired translation,
+ // set the translation to zero
+ if (Math.abs(viewport.translation.x) < minTranslation) {
+ viewport.translation.x = 0;
+ } else {
+ shift.x = viewport.translation.x / translateSpeed;
+ }
+
+ // If the current translation is smaller than the minimum desired translation,
+ // set the translation to zero
+ if (Math.abs(viewport.translation.y) < minTranslation) {
+ viewport.translation.y = 0;
+ } else {
+ shift.y = viewport.translation.y / translateSpeed;
+ }
}
} else {
- newCoords = cornerstone.pageToPixel(element, startPoints.page.x, startPoints.page.y);
+ // Zoom inwards to the current image point
+
+ // Identify the coordinates of the point the user is trying to zoom into
+ // If we are not allowed to zoom outside the image, bound the user-selected position to
+ // a point inside the image
if (config && config.preventZoomOutsideImage) {
startPoints.image = boundPosition(startPoints.image, image.width, image.height);
- newCoords = boundPosition(newCoords, image.width, image.height);
}
- // Zoom inwards to the current image point
+
+ // Calculate the translation value that would place the desired image point in the center
+ // of the viewport
var desiredTranslation = {
x: image.width / 2 - startPoints.image.x,
y: image.height / 2 - startPoints.image.y
};
+ // Correct the target location using the viewport rotation and flip parameters
+ desiredTranslation = correctShift(desiredTranslation, viewport);
+
+ // Calculate the difference between the current viewport translation value and the
+ // final desired translation values
var distanceToDesired = {
x: viewport.translation.x - desiredTranslation.x,
y: viewport.translation.y - desiredTranslation.y
};
- shift = {
- x: distanceToDesired.x / inwardsTranslateSpeed,
- y: distanceToDesired.y / inwardsTranslateSpeed
- };
-
+ // If the current translation is smaller than the minimum desired translation,
+ // stop translating in the x-direction
if (Math.abs(distanceToDesired.x) < minTranslation) {
viewport.translation.x = desiredTranslation.x;
- shift.x = 0;
- } else if (Math.abs(distanceToDesired.y) < minTranslation) {
+ } else {
+ // Otherwise, shift the viewport by one step
+ shift.x = distanceToDesired.x / translateSpeed;
+ }
+
+ // If the current translation is smaller than the minimum desired translation,
+ // stop translating in the y-direction
+ if (Math.abs(distanceToDesired.y) < minTranslation) {
viewport.translation.y = desiredTranslation.y;
- shift.y = 0;
- } else if (Math.abs(distanceToDesired.x) < minTranslation &&
- Math.abs(distanceToDesired.y) < minTranslation) {
- cornerstone.setViewport(element, viewport);
- return false;
+ } else {
+ // Otherwise, shift the viewport by one step
+ shift.y = distanceToDesired.y / translateSpeed;
}
}
- shift = correctShift(shift, viewport);
- if (!shift.x && !shift.y) {
- return false;
- }
-
+ // Apply the shift to the Viewport's translation setting
viewport.translation.x -= shift.x;
viewport.translation.y -= shift.y;
+
+ // Update the Viewport with the new translation value
cornerstone.setViewport(element, viewport);
}
diff --git a/Packages/lesiontracker/client/compatibility/bidirectionalTool.js b/Packages/lesiontracker/client/compatibility/bidirectionalTool.js
index 0907a25dd..7913b1991 100644
--- a/Packages/lesiontracker/client/compatibility/bidirectionalTool.js
+++ b/Packages/lesiontracker/client/compatibility/bidirectionalTool.js
@@ -1082,7 +1082,7 @@
// now check to see if there is a handle we can move
if (!toolData) {
- return false;
+ return;
}
for (var i = 0; i < toolData.data.length; i++) {
@@ -1097,8 +1097,6 @@
return false;
}
}
-
- return false; // false = causes jquery to preventDefault() and stopPropagation() this event
}
// module exports
diff --git a/Packages/lesiontracker/client/compatibility/crunexTool.js b/Packages/lesiontracker/client/compatibility/crunexTool.js
index b6cbf050d..5b1111efe 100644
--- a/Packages/lesiontracker/client/compatibility/crunexTool.js
+++ b/Packages/lesiontracker/client/compatibility/crunexTool.js
@@ -374,7 +374,7 @@
// now check to see if there is a handle we can move
if (!toolData) {
- return false;
+ return;
}
for (var i = 0; i < toolData.data.length; i++) {
@@ -389,8 +389,6 @@
return false;
}
}
-
- return false; // false = causes jquery to preventDefault() and stopPropagation() this event
}
diff --git a/Packages/lesiontracker/client/compatibility/nonTargetTool.js b/Packages/lesiontracker/client/compatibility/nonTargetTool.js
index 24eff4c5b..d5408d6ae 100644
--- a/Packages/lesiontracker/client/compatibility/nonTargetTool.js
+++ b/Packages/lesiontracker/client/compatibility/nonTargetTool.js
@@ -321,7 +321,7 @@
// now check to see if there is a handle we can move
if (!toolData) {
- return false;
+ return;
}
for (var i = 0; i < toolData.data.length; i++) {
@@ -336,8 +336,6 @@
return false;
}
}
-
- return false; // false = causes jquery to preventDefault() and stopPropagation() this event
}
cornerstoneTools.nonTarget = cornerstoneTools.mouseButtonTool({
diff --git a/Packages/viewerbase/client/compatibility/dialogPolyfill.js b/Packages/viewerbase/client/compatibility/dialogPolyfill.js
new file mode 100644
index 000000000..1ff46a6fc
--- /dev/null
+++ b/Packages/viewerbase/client/compatibility/dialogPolyfill.js
@@ -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
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 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
+ 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 , 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;
+})();
\ No newline at end of file
diff --git a/Packages/viewerbase/client/compatibility/dialogPolyfill.styl b/Packages/viewerbase/client/compatibility/dialogPolyfill.styl
new file mode 100644
index 000000000..fc571789a
--- /dev/null
+++ b/Packages/viewerbase/client/compatibility/dialogPolyfill.styl
@@ -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
\ No newline at end of file
diff --git a/Packages/viewerbase/client/compatibility/validate.js b/Packages/viewerbase/client/compatibility/validate.js
deleted file mode 100644
index ec942ac7a..000000000
--- a/Packages/viewerbase/client/compatibility/validate.js
+++ /dev/null
@@ -1,1086 +0,0 @@
-/*!
- * validate.js 0.9.0
- *
- * (c) 2013-2015 Nicklas Ansman, 2013 Wrapp
- * Validate.js may be freely distributed under the MIT license.
- * For all details and documentation:
- * http://validatejs.org/
- */
-
-(function(exports, module, define) {
- "use strict";
-
- // The main function that calls the validators specified by the constraints.
- // The options are the following:
- // - format (string) - An option that controls how the returned value is formatted
- // * flat - Returns a flat array of just the error messages
- // * grouped - Returns the messages grouped by attribute (default)
- // * detailed - Returns an array of the raw validation data
- // - fullMessages (boolean) - If `true` (default) the attribute name is prepended to the error.
- //
- // Please note that the options are also passed to each validator.
- var validate = function(attributes, constraints, options) {
- options = v.extend({}, v.options, options);
-
- var results = v.runValidations(attributes, constraints, options)
- , attr
- , validator;
-
- for (attr in results) {
- for (validator in results[attr]) {
- if (v.isPromise(results[attr][validator])) {
- throw new Error("Use validate.async if you want support for promises");
- }
- }
- }
- return validate.processValidationResults(results, options);
- };
-
- var v = validate;
-
- // Copies over attributes from one or more sources to a single destination.
- // Very much similar to underscore's extend.
- // The first argument is the target object and the remaining arguments will be
- // used as sources.
- v.extend = function(obj) {
- [].slice.call(arguments, 1).forEach(function(source) {
- for (var attr in source) {
- obj[attr] = source[attr];
- }
- });
- return obj;
- };
-
- v.extend(validate, {
- // This is the version of the library as a semver.
- // The toString function will allow it to be coerced into a string
- version: {
- major: 0,
- minor: 9,
- patch: 0,
- metadata: "development",
- toString: function() {
- var version = v.format("%{major}.%{minor}.%{patch}", v.version);
- if (!v.isEmpty(v.version.metadata)) {
- version += "+" + v.version.metadata;
- }
- return version;
- }
- },
-
- // Below is the dependencies that are used in validate.js
-
- // The constructor of the Promise implementation.
- // If you are using Q.js, RSVP or any other A+ compatible implementation
- // override this attribute to be the constructor of that promise.
- // Since jQuery promises aren't A+ compatible they won't work.
- Promise: typeof Promise !== "undefined" ? Promise : /* istanbul ignore next */ null,
-
- EMPTY_STRING_REGEXP: /^\s*$/,
-
- // Runs the validators specified by the constraints object.
- // Will return an array of the format:
- // [{attribute: "", error: ""}, ...]
- runValidations: function(attributes, constraints, options) {
- var results = []
- , attr
- , validatorName
- , value
- , validators
- , validator
- , validatorOptions
- , error;
-
- if (v.isDomElement(attributes) || v.isJqueryElement(attributes)) {
- attributes = v.collectFormValues(attributes);
- }
-
- // Loops through each constraints, finds the correct validator and run it.
- for (attr in constraints) {
- value = v.getDeepObjectValue(attributes, attr);
- // This allows the constraints for an attribute to be a function.
- // The function will be called with the value, attribute name, the complete dict of
- // attributes as well as the options and constraints passed in.
- // This is useful when you want to have different
- // validations depending on the attribute value.
- validators = v.result(constraints[attr], value, attributes, attr, options, constraints);
-
- for (validatorName in validators) {
- validator = v.validators[validatorName];
-
- if (!validator) {
- error = v.format("Unknown validator %{name}", {name: validatorName});
- throw new Error(error);
- }
-
- validatorOptions = validators[validatorName];
- // This allows the options to be a function. The function will be
- // called with the value, attribute name, the complete dict of
- // attributes as well as the options and constraints passed in.
- // This is useful when you want to have different
- // validations depending on the attribute value.
- validatorOptions = v.result(validatorOptions, value, attributes, attr, options, constraints);
- if (!validatorOptions) {
- continue;
- }
- results.push({
- attribute: attr,
- value: value,
- validator: validatorName,
- globalOptions: options,
- attributes: attributes,
- options: validatorOptions,
- error: validator.call(validator,
- value,
- validatorOptions,
- attr,
- attributes,
- options)
- });
- }
- }
-
- return results;
- },
-
- // Takes the output from runValidations and converts it to the correct
- // output format.
- processValidationResults: function(errors, options) {
- var attr;
-
- errors = v.pruneEmptyErrors(errors, options);
- errors = v.expandMultipleErrors(errors, options);
- errors = v.convertErrorMessages(errors, options);
-
- switch (options.format || "grouped") {
- case "detailed":
- // Do nothing more to the errors
- break;
-
- case "flat":
- errors = v.flattenErrorsToArray(errors);
- break;
-
- case "grouped":
- errors = v.groupErrorsByAttribute(errors);
- for (attr in errors) {
- errors[attr] = v.flattenErrorsToArray(errors[attr]);
- }
- break;
-
- default:
- throw new Error(v.format("Unknown format %{format}", options));
- }
-
- return v.isEmpty(errors) ? undefined : errors;
- },
-
- // Runs the validations with support for promises.
- // This function will return a promise that is settled when all the
- // validation promises have been completed.
- // It can be called even if no validations returned a promise.
- async: function(attributes, constraints, options) {
- options = v.extend({}, v.async.options, options);
-
- var WrapErrors = options.wrapErrors || function(errors) {
- return errors;
- };
-
- // Removes unknown attributes
- if (options.cleanAttributes !== false) {
- attributes = v.cleanAttributes(attributes, constraints);
- }
-
- var results = v.runValidations(attributes, constraints, options);
-
- return new v.Promise(function(resolve, reject) {
- v.waitForResults(results).then(function() {
- var errors = v.processValidationResults(results, options);
- if (errors) {
- reject(new WrapErrors(errors, options, attributes, constraints));
- } else {
- resolve(attributes);
- }
- }, function(err) {
- reject(err);
- });
- });
- },
-
- single: function(value, constraints, options) {
- options = v.extend({}, v.single.options, options, {
- format: "flat",
- fullMessages: false
- });
- return v({single: value}, {single: constraints}, options);
- },
-
- // Returns a promise that is resolved when all promises in the results array
- // are settled. The promise returned from this function is always resolved,
- // never rejected.
- // This function modifies the input argument, it replaces the promises
- // with the value returned from the promise.
- waitForResults: function(results) {
- // Create a sequence of all the results starting with a resolved promise.
- return results.reduce(function(memo, result) {
- // If this result isn't a promise skip it in the sequence.
- if (!v.isPromise(result.error)) {
- return memo;
- }
-
- return memo.then(function() {
- return result.error.then(
- function(error) {
- result.error = error || null;
- },
- function(error) {
- if (error instanceof Error) {
- throw error;
- }
- v.error("Rejecting promises with the result is deprecated. Please use the resolve callback instead.");
- result.error = error;
- }
- );
- });
- }, new v.Promise(function(r) { r(); })); // A resolved promise
- },
-
- // If the given argument is a call: function the and: function return the value
- // otherwise just return the value. Additional arguments will be passed as
- // arguments to the function.
- // Example:
- // ```
- // result('foo') // 'foo'
- // result(Math.max, 1, 2) // 2
- // ```
- result: function(value) {
- var args = [].slice.call(arguments, 1);
- if (typeof value === 'function') {
- value = value.apply(null, args);
- }
- return value;
- },
-
- // Checks if the value is a number. This function does not consider NaN a
- // number like many other `isNumber` functions do.
- isNumber: function(value) {
- return typeof value === 'number' && !isNaN(value);
- },
-
- // Returns false if the object is not a function
- isFunction: function(value) {
- return typeof value === 'function';
- },
-
- // A simple check to verify that the value is an integer. Uses `isNumber`
- // and a simple modulo check.
- isInteger: function(value) {
- return v.isNumber(value) && value % 1 === 0;
- },
-
- // Uses the `Object` function to check if the given argument is an object.
- isObject: function(obj) {
- return obj === Object(obj);
- },
-
- // Simply checks if the object is an instance of a date
- isDate: function(obj) {
- return obj instanceof Date;
- },
-
- // Returns false if the object is `null` of `undefined`
- isDefined: function(obj) {
- return obj !== null && obj !== undefined;
- },
-
- // Checks if the given argument is a promise. Anything with a `then`
- // function is considered a promise.
- isPromise: function(p) {
- return !!p && v.isFunction(p.then);
- },
-
- isJqueryElement: function(o) {
- return o && v.isString(o.jquery);
- },
-
- isDomElement: function(o) {
- if (!o) {
- return false;
- }
-
- if (!v.isFunction(o.querySelectorAll) || !v.isFunction(o.querySelector)) {
- return false;
- }
-
- if (v.isObject(document) && o === document) {
- return true;
- }
-
- // http://stackoverflow.com/a/384380/699304
- /* istanbul ignore else */
- if (typeof HTMLElement === "object") {
- return o instanceof HTMLElement;
- } else {
- return o &&
- typeof o === "object" &&
- o !== null &&
- o.nodeType === 1 &&
- typeof o.nodeName === "string";
- }
- },
-
- isEmpty: function(value) {
- var attr;
-
- // Null and undefined are empty
- if (!v.isDefined(value)) {
- return true;
- }
-
- // functions are non empty
- if (v.isFunction(value)) {
- return false;
- }
-
- // Whitespace only strings are empty
- if (v.isString(value)) {
- return v.EMPTY_STRING_REGEXP.test(value);
- }
-
- // For arrays we use the length property
- if (v.isArray(value)) {
- return value.length === 0;
- }
-
- // Dates have no attributes but aren't empty
- if (v.isDate(value)) {
- return false;
- }
-
- // If we find at least one property we consider it non empty
- if (v.isObject(value)) {
- for (attr in value) {
- return false;
- }
- return true;
- }
-
- return false;
- },
-
- // Formats the specified strings with the given values like so:
- // ```
- // format("Foo: %{foo}", {foo: "bar"}) // "Foo bar"
- // ```
- // If you want to write %{...} without having it replaced simply
- // prefix it with % like this `Foo: %%{foo}` and it will be returned
- // as `"Foo: %{foo}"`
- format: v.extend(function(str, vals) {
- if (!v.isString(str)) {
- return str;
- }
- return str.replace(v.format.FORMAT_REGEXP, function(m0, m1, m2) {
- if (m1 === '%') {
- return "%{" + m2 + "}";
- } else {
- return String(vals[m2]);
- }
- });
- }, {
- // Finds %{key} style patterns in the given string
- FORMAT_REGEXP: /(%?)%\{([^\}]+)\}/g
- }),
-
- // "Prettifies" the given string.
- // Prettifying means replacing [.\_-] with spaces as well as splitting
- // camel case words.
- prettify: function(str) {
- if (v.isNumber(str)) {
- // If there are more than 2 decimals round it to two
- if ((str * 100) % 1 === 0) {
- return "" + str;
- } else {
- return parseFloat(Math.round(str * 100) / 100).toFixed(2);
- }
- }
-
- if (v.isArray(str)) {
- return str.map(function(s) { return v.prettify(s); }).join(", ");
- }
-
- if (v.isObject(str)) {
- return str.toString();
- }
-
- // Ensure the string is actually a string
- str = "" + str;
-
- return str
- // Splits keys separated by periods
- .replace(/([^\s])\.([^\s])/g, '$1 $2')
- // Removes backslashes
- .replace(/\\+/g, '')
- // Replaces - and - with space
- .replace(/[_-]/g, ' ')
- // Splits camel cased words
- .replace(/([a-z])([A-Z])/g, function(m0, m1, m2) {
- return "" + m1 + " " + m2.toLowerCase();
- })
- .toLowerCase();
- },
-
- stringifyValue: function(value) {
- return v.prettify(value);
- },
-
- isString: function(value) {
- return typeof value === 'string';
- },
-
- isArray: function(value) {
- return {}.toString.call(value) === '[object Array]';
- },
-
- contains: function(obj, value) {
- if (!v.isDefined(obj)) {
- return false;
- }
- if (v.isArray(obj)) {
- return obj.indexOf(value) !== -1;
- }
- return value in obj;
- },
-
- forEachKeyInKeypath: function(object, keypath, callback) {
- if (!v.isString(keypath)) {
- return undefined;
- }
-
- var key = ""
- , i
- , escape = false;
-
- for (i = 0; i < keypath.length; ++i) {
- switch (keypath[i]) {
- case '.':
- if (escape) {
- escape = false;
- key += '.';
- } else {
- object = callback(object, key, false);
- key = "";
- }
- break;
-
- case '\\':
- if (escape) {
- escape = false;
- key += '\\';
- } else {
- escape = true;
- }
- break;
-
- default:
- escape = false;
- key += keypath[i];
- break;
- }
- }
-
- return callback(object, key, true);
- },
-
- getDeepObjectValue: function(obj, keypath) {
- if (!v.isObject(obj)) {
- return undefined;
- }
-
- return v.forEachKeyInKeypath(obj, keypath, function(obj, key) {
- if (v.isObject(obj)) {
- return obj[key];
- }
- });
- },
-
- // This returns an object with all the values of the form.
- // It uses the input name as key and the value as value
- // So for example this:
- //
- // would return:
- // {email: "foo@bar.com"}
- collectFormValues: function(form, options) {
- var values = {}
- , i
- , input
- , inputs
- , value;
-
- if (v.isJqueryElement(form)) {
- form = form[0];
- }
-
- if (!form) {
- return values;
- }
-
- options = options || {};
-
- inputs = form.querySelectorAll("input[name], textarea[name]");
- for (i = 0; i < inputs.length; ++i) {
- input = inputs.item(i);
-
- if (v.isDefined(input.getAttribute("data-ignored"))) {
- continue;
- }
-
- value = v.sanitizeFormValue(input.value, options);
- if (input.type === "number") {
- value = value ? +value : null;
- } else if (input.type === "checkbox") {
- if (input.attributes.value) {
- if (!input.checked) {
- value = values[input.name] || null;
- }
- } else {
- value = input.checked;
- }
- } else if (input.type === "radio") {
- if (!input.checked) {
- value = values[input.name] || null;
- }
- }
- values[input.name] = value;
- }
-
- inputs = form.querySelectorAll("select[name]");
- for (i = 0; i < inputs.length; ++i) {
- input = inputs.item(i);
- value = v.sanitizeFormValue(input.options[input.selectedIndex].value, options);
- values[input.name] = value;
- }
-
- return values;
- },
-
- sanitizeFormValue: function(value, options) {
- if (options.trim && v.isString(value)) {
- value = value.trim();
- }
-
- if (options.nullify !== false && value === "") {
- return null;
- }
- return value;
- },
-
- capitalize: function(str) {
- if (!v.isString(str)) {
- return str;
- }
- return str[0].toUpperCase() + str.slice(1);
- },
-
- // Remove all errors who's error attribute is empty (null or undefined)
- pruneEmptyErrors: function(errors) {
- return errors.filter(function(error) {
- return !v.isEmpty(error.error);
- });
- },
-
- // In
- // [{error: ["err1", "err2"], ...}]
- // Out
- // [{error: "err1", ...}, {error: "err2", ...}]
- //
- // All attributes in an error with multiple messages are duplicated
- // when expanding the errors.
- expandMultipleErrors: function(errors) {
- var ret = [];
- errors.forEach(function(error) {
- // Removes errors without a message
- if (v.isArray(error.error)) {
- error.error.forEach(function(msg) {
- ret.push(v.extend({}, error, {error: msg}));
- });
- } else {
- ret.push(error);
- }
- });
- return ret;
- },
-
- // Converts the error mesages by prepending the attribute name unless the
- // message is prefixed by ^
- convertErrorMessages: function(errors, options) {
- options = options || {};
-
- var ret = [];
- errors.forEach(function(errorInfo) {
- var error = v.result(errorInfo.error,
- errorInfo.value,
- errorInfo.attribute,
- errorInfo.options,
- errorInfo.attributes,
- errorInfo.globalOptions);
-
- if (!v.isString(error)) {
- ret.push(errorInfo);
- return;
- }
-
- if (error[0] === '^') {
- error = error.slice(1);
- } else if (options.fullMessages !== false) {
- error = v.capitalize(v.prettify(errorInfo.attribute)) + " " + error;
- }
- error = error.replace(/\\\^/g, "^");
- error = v.format(error, {value: v.stringifyValue(errorInfo.value)});
- ret.push(v.extend({}, errorInfo, {error: error}));
- });
- return ret;
- },
-
- // In:
- // [{attribute: "", ...}]
- // Out:
- // {"": [{attribute: "", ...}]}
- groupErrorsByAttribute: function(errors) {
- var ret = {};
- errors.forEach(function(error) {
- var list = ret[error.attribute];
- if (list) {
- list.push(error);
- } else {
- ret[error.attribute] = [error];
- }
- });
- return ret;
- },
-
- // In:
- // [{error: "", ...}, {error: "", ...}]
- // Out:
- // ["", ""]
- flattenErrorsToArray: function(errors) {
- return errors.map(function(error) { return error.error; });
- },
-
- cleanAttributes: function(attributes, whitelist) {
- function whitelistCreator(obj, key, last) {
- if (v.isObject(obj[key])) {
- return obj[key];
- }
- return (obj[key] = last ? true : {});
- }
-
- function buildObjectWhitelist(whitelist) {
- var ow = {}
- , lastObject
- , attr;
- for (attr in whitelist) {
- if (!whitelist[attr]) {
- continue;
- }
- v.forEachKeyInKeypath(ow, attr, whitelistCreator);
- }
- return ow;
- }
-
- function cleanRecursive(attributes, whitelist) {
- if (!v.isObject(attributes)) {
- return attributes;
- }
-
- var ret = v.extend({}, attributes)
- , w
- , attribute;
-
- for (attribute in attributes) {
- w = whitelist[attribute];
-
- if (v.isObject(w)) {
- ret[attribute] = cleanRecursive(ret[attribute], w);
- } else if (!w) {
- delete ret[attribute];
- }
- }
- return ret;
- }
-
- if (!v.isObject(whitelist) || !v.isObject(attributes)) {
- return {};
- }
-
- whitelist = buildObjectWhitelist(whitelist);
- return cleanRecursive(attributes, whitelist);
- },
-
- exposeModule: function(validate, root, exports, module, define) {
- if (exports) {
- if (module && module.exports) {
- exports = module.exports = validate;
- }
- exports.validate = validate;
- } else {
- root.validate = validate;
- if (validate.isFunction(define) && define.amd) {
- define([], function () { return validate; });
- }
- }
- },
-
- warn: function(msg) {
- if (typeof console !== "undefined" && console.warn) {
- console.warn("[validate.js] " + msg);
- }
- },
-
- error: function(msg) {
- if (typeof console !== "undefined" && console.error) {
- console.error("[validate.js] " + msg);
- }
- }
- });
-
- validate.validators = {
- // Presence validates that the value isn't empty
- presence: function(value, options) {
- options = v.extend({}, this.options, options);
- if (v.isEmpty(value)) {
- return options.message || this.message || "can't be blank";
- }
- },
- length: function(value, options, attribute) {
- // Empty values are allowed
- if (v.isEmpty(value)) {
- return;
- }
-
- options = v.extend({}, this.options, options);
-
- var is = options.is
- , maximum = options.maximum
- , minimum = options.minimum
- , tokenizer = options.tokenizer || function(val) { return val; }
- , err
- , errors = [];
-
- value = tokenizer(value);
- var length = value.length;
- if(!v.isNumber(length)) {
- v.error(v.format("Attribute %{attr} has a non numeric value for `length`", {attr: attribute}));
- return options.message || this.notValid || "has an incorrect length";
- }
-
- // Is checks
- if (v.isNumber(is) && length !== is) {
- err = options.wrongLength ||
- this.wrongLength ||
- "is the wrong length (should be %{count} characters)";
- errors.push(v.format(err, {count: is}));
- }
-
- if (v.isNumber(minimum) && length < minimum) {
- err = options.tooShort ||
- this.tooShort ||
- "is too short (minimum is %{count} characters)";
- errors.push(v.format(err, {count: minimum}));
- }
-
- if (v.isNumber(maximum) && length > maximum) {
- err = options.tooLong ||
- this.tooLong ||
- "is too long (maximum is %{count} characters)";
- errors.push(v.format(err, {count: maximum}));
- }
-
- if (errors.length > 0) {
- return options.message || errors;
- }
- },
- numericality: function(value, options) {
- // Empty values are fine
- if (v.isEmpty(value)) {
- return;
- }
-
- options = v.extend({}, this.options, options);
-
- var errors = []
- , name
- , count
- , checks = {
- greaterThan: function(v, c) { return v > c; },
- greaterThanOrEqualTo: function(v, c) { return v >= c; },
- equalTo: function(v, c) { return v === c; },
- lessThan: function(v, c) { return v < c; },
- lessThanOrEqualTo: function(v, c) { return v <= c; }
- };
-
- // Coerce the value to a number unless we're being strict.
- if (options.noStrings !== true && v.isString(value)) {
- value = +value;
- }
-
- // If it's not a number we shouldn't continue since it will compare it.
- if (!v.isNumber(value)) {
- return options.message || options.notValid || this.notValid || "is not a number";
- }
-
- // Same logic as above, sort of. Don't bother with comparisons if this
- // doesn't pass.
- if (options.onlyInteger && !v.isInteger(value)) {
- return options.message || options.notInteger || this.notInteger || "must be an integer";
- }
-
- for (name in checks) {
- count = options[name];
- if (v.isNumber(count) && !checks[name](value, count)) {
- // This picks the default message if specified
- // For example the greaterThan check uses the message from
- // this.notGreaterThan so we capitalize the name and prepend "not"
- var key = "not" + v.capitalize(name);
- var msg = options[key] || this[key] || "must be %{type} %{count}";
-
- errors.push(v.format(msg, {
- count: count,
- type: v.prettify(name)
- }));
- }
- }
-
- if (options.odd && value % 2 !== 1) {
- errors.push(options.notOdd || this.notOdd || "must be odd");
- }
- if (options.even && value % 2 !== 0) {
- errors.push(options.notEven || this.notEven || "must be even");
- }
-
- if (errors.length) {
- return options.message || errors;
- }
- },
- datetime: v.extend(function(value, options) {
- if (!v.isFunction(this.parse) || !v.isFunction(this.format)) {
- throw new Error("Both the parse and format functions needs to be set to use the datetime/date validator");
- }
-
- // Empty values are fine
- if (v.isEmpty(value)) {
- return;
- }
-
- options = v.extend({}, this.options, options);
-
- var err
- , errors = []
- , earliest = options.earliest ? this.parse(options.earliest, options) : NaN
- , latest = options.latest ? this.parse(options.latest, options) : NaN;
-
- value = this.parse(value, options);
-
- // 86400000 is the number of seconds in a day, this is used to remove
- // the time from the date
- if (isNaN(value) || options.dateOnly && value % 86400000 !== 0) {
- return options.message || this.notValid || "must be a valid date";
- }
-
- if (!isNaN(earliest) && value < earliest) {
- err = this.tooEarly || "must be no earlier than %{date}";
- err = v.format(err, {date: this.format(earliest, options)});
- errors.push(err);
- }
-
- if (!isNaN(latest) && value > latest) {
- err = this.tooLate || "must be no later than %{date}";
- err = v.format(err, {date: this.format(latest, options)});
- errors.push(err);
- }
-
- if (errors.length) {
- return options.message || errors;
- }
- }, {
- parse: null,
- format: null
- }),
- date: function(value, options) {
- options = v.extend({}, options, {dateOnly: true});
- return v.validators.datetime.call(v.validators.datetime, value, options);
- },
- format: function(value, options) {
- if (v.isString(options) || (options instanceof RegExp)) {
- options = {pattern: options};
- }
-
- options = v.extend({}, this.options, options);
-
- var message = options.message || this.message || "is invalid"
- , pattern = options.pattern
- , match;
-
- // Empty values are allowed
- if (v.isEmpty(value)) {
- return;
- }
- if (!v.isString(value)) {
- return message;
- }
-
- if (v.isString(pattern)) {
- pattern = new RegExp(options.pattern, options.flags);
- }
- match = pattern.exec(value);
- if (!match || match[0].length != value.length) {
- return message;
- }
- },
- inclusion: function(value, options) {
- // Empty values are fine
- if (v.isEmpty(value)) {
- return;
- }
- if (v.isArray(options)) {
- options = {within: options};
- }
- options = v.extend({}, this.options, options);
- if (v.contains(options.within, value)) {
- return;
- }
- var message = options.message ||
- this.message ||
- "^%{value} is not included in the list";
- return v.format(message, {value: value});
- },
- exclusion: function(value, options) {
- // Empty values are fine
- if (v.isEmpty(value)) {
- return;
- }
- if (v.isArray(options)) {
- options = {within: options};
- }
- options = v.extend({}, this.options, options);
- if (!v.contains(options.within, value)) {
- return;
- }
- var message = options.message || this.message || "^%{value} is restricted";
- return v.format(message, {value: value});
- },
- email: v.extend(function(value, options) {
- options = v.extend({}, this.options, options);
- var message = options.message || this.message || "is not a valid email";
- // Empty values are fine
- if (v.isEmpty(value)) {
- return;
- }
- if (!v.isString(value)) {
- return message;
- }
- if (!this.PATTERN.exec(value)) {
- return message;
- }
- }, {
- PATTERN: /^[a-z0-9\u007F-\uffff!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9\u007F-\uffff!#$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i
- }),
- equality: function(value, options, attribute, attributes) {
- if (v.isEmpty(value)) {
- return;
- }
-
- if (v.isString(options)) {
- options = {attribute: options};
- }
- options = v.extend({}, this.options, options);
- var message = options.message ||
- this.message ||
- "is not equal to %{attribute}";
-
- if (v.isEmpty(options.attribute) || !v.isString(options.attribute)) {
- throw new Error("The attribute must be a non empty string");
- }
-
- var otherValue = v.getDeepObjectValue(attributes, options.attribute)
- , comparator = options.comparator || function(v1, v2) {
- return v1 === v2;
- };
-
- if (!comparator(value, otherValue, options, attribute, attributes)) {
- return v.format(message, {attribute: v.prettify(options.attribute)});
- }
- },
-
- // A URL validator that is used to validate URLs with the ability to
- // restrict schemes and some domains.
- url: function(value, options) {
- if (v.isEmpty(value)) {
- return;
- }
-
- options = v.extend({}, this.options, options);
-
- var message = options.message || this.message || "is not a valid url"
- , schemes = options.schemes || this.schemes || ['http', 'https']
- , allowLocal = options.allowLocal || this.allowLocal || false;
-
- if (!v.isString(value)) {
- return message;
- }
-
- // https://gist.github.com/dperini/729294
- var regex =
- "^" +
- // schemes
- "(?:(?:" + schemes.join("|") + "):\\/\\/)" +
- // credentials
- "(?:\\S+(?::\\S*)?@)?";
-
- regex += "(?:";
-
- var hostname =
- "(?:(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)" +
- "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-?)*[a-z\\u00a1-\\uffff0-9]+)*" +
- "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))";
-
- // This ia a special case for the localhost hostname
- if (allowLocal) {
- hostname = "(?:localhost|" + hostname + ")";
- } else {
- // private & local addresses
- regex +=
- "(?!10(?:\\.\\d{1,3}){3})" +
- "(?!127(?:\\.\\d{1,3}){3})" +
- "(?!169\\.254(?:\\.\\d{1,3}){2})" +
- "(?!192\\.168(?:\\.\\d{1,3}){2})" +
- "(?!172" +
- "\\.(?:1[6-9]|2\\d|3[0-1])" +
- "(?:\\.\\d{1,3})" +
- "{2})";
- }
-
- // reserved addresses
- regex +=
- "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
- "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
- "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
- "|" +
- hostname +
- // port number
- "(?::\\d{2,5})?" +
- // path
- "(?:\\/[^\\s]*)?" +
- "$";
-
- var PATTERN = new RegExp(regex, 'i');
- if (!PATTERN.exec(value)) {
- return message;
- }
- }
- };
-
- validate.exposeModule(validate, this, exports, module, define);
-}).call(this,
- typeof exports !== 'undefined' ? /* istanbul ignore next */ exports : null,
- typeof module !== 'undefined' ? /* istanbul ignore next */ module : null,
- typeof define !== 'undefined' ? /* istanbul ignore next */ define : null);
diff --git a/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.html b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.html
new file mode 100644
index 000000000..abfa54231
--- /dev/null
+++ b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.html
@@ -0,0 +1,33 @@
+
+
+ Enter your annotation
+
+ New label
+
+
+ OK
+
+
+
+ Edit your annotation
+
+ New label
+
+
+
+
+
\ No newline at end of file
diff --git a/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.js b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.js
new file mode 100644
index 000000000..30c6ec1a0
--- /dev/null
+++ b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.js
@@ -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));
+ });
+});
+
diff --git a/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.styl b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.styl
new file mode 100644
index 000000000..e3f5ebaaa
--- /dev/null
+++ b/Packages/viewerbase/client/components/viewer/annotationDialogs/annotationDialogs.styl
@@ -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
\ No newline at end of file
diff --git a/Packages/viewerbase/client/components/viewer/cineDialog/cineDialog.styl b/Packages/viewerbase/client/components/viewer/cineDialog/cineDialog.styl
index c71318f76..14938ef81 100644
--- a/Packages/viewerbase/client/components/viewer/cineDialog/cineDialog.styl
+++ b/Packages/viewerbase/client/components/viewer/cineDialog/cineDialog.styl
@@ -1,7 +1,5 @@
@import "{design}/app.styl"
-$borderColor = rgba(77, 99, 110, 0.81)
-
#cineDialog
display: none
z-index: 1000
diff --git a/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js b/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js
index 2139dfc6b..db7d237eb 100644
--- a/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js
+++ b/Packages/viewerbase/client/components/viewer/imageViewerViewport/imageViewerViewport.js
@@ -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);
}
diff --git a/Packages/viewerbase/lib/toolManager.js b/Packages/viewerbase/lib/toolManager.js
index d4b3042c7..d74c96e36 100644
--- a/Packages/viewerbase/lib/toolManager.js
+++ b/Packages/viewerbase/lib/toolManager.js
@@ -7,7 +7,7 @@ var tools = {};
var toolDefaultStates = {
activate: [],
- deactivate: [],
+ deactivate: ['length', 'angle', 'annotate', 'ellipticalRoi', 'rectangleRoi'],
enable: [],
disable: [],
disabledToolButtons: []
@@ -35,6 +35,15 @@ function configureTools() {
// Set color for active tools
cornerstoneTools.toolColors.setActiveColor('#00ffff'); //rgb(0, 255, 0)'
+
+ // Set the configuration values for the text annotation (Arrow) tool
+ var annotateConfig = {
+ getTextCallback: getAnnotationTextCallback,
+ changeTextCallback: changeAnnotationTextCallback,
+ drawHandles: false,
+ arrowFirst: true
+ };
+ cornerstoneTools.arrowAnnotate.setConfiguration(annotateConfig);
}
toolManager = {
diff --git a/Packages/viewerbase/package.js b/Packages/viewerbase/package.js
index 18dea5432..be5badc7d 100644
--- a/Packages/viewerbase/package.js
+++ b/Packages/viewerbase/package.js
@@ -5,7 +5,7 @@ Package.describe({
});
Package.onUse(function(api) {
- api.versionsFrom('1.3.5.1');
+ api.versionsFrom('1.4');
api.use('ecmascript');
api.use('standard-app-packages');
@@ -26,7 +26,13 @@ Package.onUse(function(api) {
api.addAssets('assets/icons.svg', 'client');
// TODO: Use NPM depends for these
- api.addFiles('client/compatibility/jquery.hotkeys.js', 'client');
+ api.addFiles('client/compatibility/jquery.hotkeys.js', 'client', {
+ bare: true
+ });
+ api.addFiles('client/compatibility/dialogPolyfill.js', 'client', {
+ bare: true
+ });
+ api.addFiles('client/compatibility/dialogPolyfill.styl', 'client');
// ---------- Collections ----------
api.addFiles('client/collections.js', 'client');
@@ -75,6 +81,10 @@ Package.onUse(function(api) {
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.js', 'client');
api.addFiles('client/components/viewer/loadingIndicator/loadingIndicator.styl', 'client');
+ api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.html', 'client');
+ api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.js', 'client');
+ api.addFiles('client/components/viewer/annotationDialogs/annotationDialogs.styl', 'client');
+
api.addFiles('client/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.html', 'client');
api.addFiles('client/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.js', 'client');
api.addFiles('client/components/viewer/viewportOrientationMarkers/viewportOrientationMarkers.styl', 'client');