blob: d25cf1fe394f0d455b53ae867ebceff219d69a73 [file] [log] [blame] [edit]
// Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Namespace for the Camera app.
*/
var camera = camera || {};
/**
* Namespace for utilities.
*/
camera.util = camera.util || {};
/**
* Creates a tooltip manager for the entire document.
* @constructor
*/
camera.util.TooltipManager = function() {
/**
* @type {camera.util.StyleEffect}
* @private
*/
this.effect_ = new camera.util.StyleEffect(
function(args, callback) {
this.setTooltipVisibility_(args.element, args.visibility, callback);
}.bind(this));
// No more properties. Freeze the object.
Object.freeze(this);
};
/**
* Minimal distance from the tooltip to the closest edge in pixels.
* @type {number}
* @const
*/
camera.util.TooltipManager.EDGE_MARGIN = 10;
camera.util.TooltipManager.prototype = {
get animating() {
return this.effect_.animating;
}
};
/**
* Initializes the manager by adding tooltip handlers to every element which
* has the i18n-label attribute.
*/
camera.util.TooltipManager.prototype.initialize = function() {
var selectors = document.querySelectorAll('*[i18n-label]');
for (var index = 0; index < selectors.length; index++) {
selectors[index].addEventListener(
'mouseover', this.showTooltip_.bind(this, selectors[index]));
}
};
/**
* Positions the tooltip on the screen and toggles its visibility.
*
* @param {HTMLElement} element Element to be the tooltip positioned to.
* @param {boolean} visible True for visible, false for hidden.
* @param {function()} callback Completion callback for changing visibility.
* @private
*/
camera.util.TooltipManager.prototype.setTooltipVisibility_ = function(
element, visible, callback) {
var tooltip = document.querySelector('#tooltip');
// Hide the tooltip.
if (!visible) {
tooltip.classList.remove('visible');
callback(); // No animation, finish immediately.
return;
}
// Show the tooltip.
// TODO(mtomasz): Support showing near the top edge.
var tooltipMsg = tooltip.querySelector('#tooltip-msg');
var tooltipArrow = tooltip.querySelector('#tooltip-arrow');
var elementRect = element.getBoundingClientRect();
var elementCenter = elementRect.left + element.offsetWidth / 2;
tooltip.style.top = elementRect.top - tooltip.offsetHeight + 'px';
// Center over the element, but avoid touching edges.
var left = Math.min(
Math.max(elementCenter - tooltip.clientWidth / 2,
camera.util.TooltipManager.EDGE_MARGIN),
document.body.offsetWidth - tooltip.offsetWidth -
camera.util.TooltipManager.EDGE_MARGIN);
tooltip.style.left = Math.round(left) + 'px';
// Align the arrow to point to the element.
tooltipArrow.style.left = Math.round(elementCenter - left) + 'px';
// Show the tooltip element.
tooltip.classList.add('visible');
camera.util.waitForTransitionCompletion(tooltip, 1000, callback);
};
/**
* Shows a tooltip over the element.
* @param {HTMLElement} element Element to be shown.
* @private
*/
camera.util.TooltipManager.prototype.showTooltip_ = function(element) {
var tooltip = document.querySelector('#tooltip');
var tooltipMsg = tooltip.querySelector('#tooltip-msg');
var tooltipArrow = tooltip.querySelector('#tooltip-arrow');
this.effect_.invoke(false, function() {});
tooltipMsg.textContent = chrome.i18n.getMessage(
element.getAttribute('i18n-label'));
var hideTooltip = function() {
this.effect_.invoke({
element: element,
visibility: false
}, function() {});
element.removeEventListener('mouseout', hideTooltip);
element.removeEventListener('click', hideTooltip);
}.bind(this);
element.addEventListener('mouseout', hideTooltip);
element.addEventListener('click', hideTooltip);
// Show the tooltip after 500ms.
this.effect_.invoke({
element: element,
visibility: true
}, function() {}, 500);
};
/**
* Checks the board name if the user is using a chromebook.
* @param {string} name Board name.
* @param {function(boolean)} callback Result callback.
*/
camera.util.isBoard = function(name, callback) {
if (chrome.chromeosInfoPrivate) {
chrome.chromeosInfoPrivate.get(['board'], function(values) {
var board = values['board'];
callback(board && board.indexOf(name) == 0);
});
} else {
callback(false);
}
};
/**
* Sets localized aria attributes for TTS on the entire document. Uses the
* dedicated i18n-aria-label attribute as a strings identifier. If it is not
* found, then i18n-label is used as a fallback.
*/
camera.util.setAriaAttributes = function() {
var elements = document.querySelectorAll('*[i18n-aria-label], *[i18n-label]');
for (var index = 0; index < elements.length; index++) {
var label = elements[index].hasAttribute('i18n-aria-label') ?
elements[index].getAttribute('i18n-aria-label') :
elements[index].getAttribute('i18n-label'); // Fallback.
elements[index].setAttribute('aria-label', chrome.i18n.getMessage(label));
}
};
/**
* Sets a class which invokes an animation and calls the callback when the
* animation is done. The class is released once the animation is finished.
* If the class name is already set, then calls onCompletion immediately.
*
* @param {HTMLElement} classElement Element to be applied the class on.
* @param {HTMLElement} animationElement Element to be animated.
* @param {string} className Class name to be added.
* @param {number} timeout Animation timeout in milliseconds.
* @param {function()=} opt_onCompletion Completion callback.
*/
camera.util.setAnimationClass = function(
classElement, animationElement, className, timeout, opt_onCompletion) {
if (classElement.classList.contains(className)) {
if (opt_onCompletion)
opt_onCompletion();
return;
}
classElement.classList.add(className);
var onAnimationCompleted = function() {
classElement.classList.remove(className);
if (opt_onCompletion)
opt_onCompletion();
};
camera.util.waitForAnimationCompletion(
animationElement, timeout, onAnimationCompleted);
};
/**
* Waits for animation completion and calls the callback.
*
* @param {HTMLElement} animationElement Element to be animated.
* @param {number} timeout Timeout for completion. 0 for no timeout.
* @param {function()} onCompletion Completion callback.
*/
camera.util.waitForAnimationCompletion = function(
animationElement, timeout, onCompletion) {
var completed = false;
var onAnimationCompleted = function(opt_event) {
if (completed || (opt_event && opt_event.target != animationElement))
return;
completed = true;
animationElement.removeEventListener(
'webkitAnimationEnd', onAnimationCompleted);
onCompletion();
};
if (timeout)
setTimeout(onAnimationCompleted, timeout);
animationElement.addEventListener('webkitAnimationEnd', onAnimationCompleted);
};
/**
* Waits for transition completion and calls the callback.
*
* @param {HTMLElement} transitionElement Element to be transitioned.
* @param {number} timeout Timeout for completion. 0 for no timeout.
* @param {function()} onCompletion Completion callback.
*/
camera.util.waitForTransitionCompletion = function(
transitionElement, timeout, onCompletion) {
var completed = false;
var onTransitionCompleted = function(opt_event) {
if (completed || (opt_event && opt_event.target != transitionElement))
return;
completed = true;
transitionElement.removeEventListener(
'webkitTransitionEnd', onTransitionCompleted);
onCompletion();
};
if (timeout)
setTimeout(onTransitionCompleted, timeout);
transitionElement.addEventListener(
'webkitTransitionEnd', onTransitionCompleted);
};
/**
* Scrolls the parent of the element so the element is visible.
*
* @param {HTMLElement} element Element to be visible.
* @param {camera.util.SmoothScroller} scroller Scroller to be used.
* @param {camera.util.SmoothScroller.Mode=} opt_mode Scrolling mode. Default:
* SMOOTH.
*/
camera.util.ensureVisible = function(element, scroller, opt_mode) {
var scrollLeft = scroller.scrollLeft;
var scrollTop = scroller.scrollTop;
if (element.offsetTop < scroller.scrollTop)
scrollTop = Math.round(element.offsetTop - element.offsetHeight * 0.5);
if (element.offsetTop + element.offsetHeight >
scrollTop + scroller.clientHeight) {
scrollTop = Math.round(element.offsetTop + element.offsetHeight * 1.5 -
scroller.clientHeight);
}
if (element.offsetLeft < scroller.scrollLeft)
scrollLeft = Math.round(element.offsetLeft - element.offsetWidth * 0.5);
if (element.offsetLeft + element.offsetWidth >
scrollLeft + scroller.clientWidth) {
scrollLeft = Math.round(element.offsetLeft + element.offsetWidth * 1.5 -
scroller.clientWidth);
}
scroller.scrollTo(scrollLeft, scrollTop, opt_mode);
};
/**
* Scrolls the parent of the element so the element is centered.
*
* @param {HTMLElement} element Element to be visible.
* @param {camera.util.SmoothScroller} scroller Scroller to be used.
* @param {camera.util.SmoothScroller.Mode=} opt_mode Scrolling mode. Default:
* SMOOTH.
*/
camera.util.scrollToCenter = function(element, scroller, opt_mode) {
var scrollLeft = Math.round(element.offsetLeft + element.offsetWidth / 2 -
scroller.clientWidth / 2);
var scrollTop = Math.round(element.offsetTop + element.offsetHeight / 2 -
scroller.clientHeight / 2);
scroller.scrollTo(scrollLeft, scrollTop, opt_mode);
};
/**
* Wraps an effect in style implemented as either CSS3 animation or CSS3
* transition. The passed closure should invoke the effect.
* Only the last callback, passed to the latest invoke() call will be called
* on the transition or the animation end.
*
* @param {function(*, function())} closure Closure for invoking the effect.
* @constructor
*/
camera.util.StyleEffect = function(closure) {
/**
* @type {function(*, function()}
* @private
*/
this.closure_ = closure;
/**
* Callback to be called for the latest invokation.
* @type {?function()}
* @private
*/
this.callback_ = null;
/**
* @type {?number{
* @private
*/
this.invocationTimer_ = null;
// End of properties. Seal the object.
Object.seal(this);
};
camera.util.StyleEffect.prototype = {
get animating() {
return !!this.callback_;
}
};
/**
* Invokes the animation and calls the callback on completion. Note, that
* the callback will not be called if there is another invocation called after.
*
* @param {*} state State of the effect to be set
* @param {function()} callback Completion callback.
* @param {number=} opt_delay Timeout in milliseconds before invoking.
*/
camera.util.StyleEffect.prototype.invoke = function(
state, callback, opt_delay) {
if (this.invocationTimer_) {
clearTimeout(this.invocationTimer_);
this.invocationTimer_ = null;
}
var invokeClosure = function() {
this.callback_ = callback;
this.closure_(state, function() {
if (!this.callback_)
return;
var callback = this.callback_;
this.callback_ = null;
// Let the animation neatly finish.
setTimeout(callback, 0);
}.bind(this));
}.bind(this);
if (opt_delay !== undefined)
this.invocationTimer_ = setTimeout(invokeClosure, opt_delay);
else
invokeClosure();
};
/**
* Performs smooth scrolling of a scrollable DOM element using a accelerated
* CSS3 transform and transition for smooth animation.
*
* @param {HTMLElement} element Element to be scrolled.
* @param {HTMLElement} padder Element holding contents within the scrollable
* element.
* @constructor
*/
camera.util.SmoothScroller = function(element, padder) {
/**
* @type {HTMLElement}
* @private
*/
this.element_ = element;
/**
* @type {HTMLElement}
* @private
*/
this.padder_ = padder;
/**
* @type {boolean}
* @private
*/
this.animating_ = false;
/**
* @type {number}
* @private
*/
this.animationId_ = 0;
// End of properties. Seal the object.
Object.seal(this);
};
/**
* Smooth scrolling animation duration in milliseconds.
* @type {number}
* @const
*/
camera.util.SmoothScroller.DURATION = 500;
/**
* Mode of scrolling.
* @enum {number}
*/
camera.util.SmoothScroller.Mode = {
SMOOTH: 0,
INSTANT: 1
};
camera.util.SmoothScroller.prototype = {
get element() {
return this.element_;
},
get animating() {
return this.animating_;
},
get scrollLeft() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return -this.padder_.getBoundingClientRect().left;
},
get scrollTop() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return -this.padder_.getBoundingClientRect().top;
},
get scrollWidth() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return this.padder_.scrollWidth;
},
get scrollHeight() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return this.padder_.scrollHeight;
},
get clientWidth() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return this.element_.clientWidth;
},
get clientHeight() {
// TODO(mtomasz): This does not reflect paddings nor margins.
return this.element_.clientHeight;
}
};
/**
* Flushes the CSS3 transition scroll to real scrollLeft/scrollTop attributes.
* @private
*/
camera.util.SmoothScroller.prototype.flushScroll_ = function() {
var scrollLeft = this.scrollLeft;
var scrollTop = this.scrollTop;
this.padder_.style.transition = '';
this.padder_.style.webkitTransform = '';
this.element_.scrollLeft = scrollLeft;
this.element_.scrollTop = scrollTop;
this.animationId_++; // Invalidate the animation by increasing the id.
this.animating_ = false;
};
/**
* Scrolls smoothly to specified position.
*
* @param {number} x X Target scrollLeft value.
* @param {number} y Y Target scrollTop value.
* @param {camera.util.SmoothScroller.Mode=} opt_mode Scrolling mode. Default:
* SMOOTH.
*/
camera.util.SmoothScroller.prototype.scrollTo = function(x, y, opt_mode) {
var mode = opt_mode || camera.util.SmoothScroller.Mode.SMOOTH;
// Limit to the allowed values.
var x = Math.max(0, Math.min(x, this.scrollWidth - this.clientWidth));
var y = Math.max(0, Math.min(y, this.scrollHeight - this.clientHeight));
switch (mode) {
case camera.util.SmoothScroller.Mode.INSTANT:
// Cancel any current animations.
if (this.animating_)
this.flushScroll_();
this.element_.scrollLeft = x;
this.element_.scrollTop = y;
break;
case camera.util.SmoothScroller.Mode.SMOOTH:
// Calculate translating offset using the accelerated CSS3 transform.
var dx = x - this.element_.scrollLeft;
var dy = y - this.element_.scrollTop;
var transformString =
'translate(' + -dx + 'px, ' + -dy + 'px)';
// If nothing to change, then return.
if (this.padder_.style.webkitTransform == transformString ||
(dx == 0 && dy == 0 && !this.padder_.style.webkitTransform)) {
return;
}
// Invalidate previous invocations.
var currentAnimationId = ++this.animationId_;
// Start the accelerated animation.
this.animating_ = true;
this.padder_.style.transition = '-webkit-transform ' +
camera.util.SmoothScroller.DURATION + 'ms ease-out';
this.padder_.style.webkitTransform = transformString;
// Remove translation, and switch to scrollLeft/scrollTop when the
// animation is finished.
camera.util.waitForTransitionCompletion(
this.padder_,
0,
function() {
// Check if the animation got invalidated by a later scroll.
if (currentAnimationId == this.animationId_)
this.flushScroll_();
}.bind(this));
break;
}
};
/**
* Runs asynchronous closures in a queue.
* @constructor
*/
camera.util.Queue = function() {
/**
* @type {Array.<function(function())>}
* @private
*/
this.closures_ = [];
/**
* @type {boolean}
* @private
*/
this.running_ = false;
// End of properties. Seal the object.
Object.seal(this);
};
/**
* Runs a task within the queue.
* @param {function(function())} closure Closure to be run with a completion
* callback.
*/
camera.util.Queue.prototype.run = function(closure) {
this.closures_.push(closure);
if (!this.running_)
this.continue_();
};
/**
* Continues executing further enqueued closures, or stops the queue if nothing
* pending.
* @private
*/
camera.util.Queue.prototype.continue_ = function() {
if (!this.closures_.length) {
this.running_ = false;
return;
}
this.running_ = true;
var closure = this.closures_.shift();
closure(this.continue_.bind(this));
};
/**
* Tracks the mouse for click and move, and the touch screen for touches. If
* any of these are detected, then the callback is called.
*
* @param {HTMLElement} element Element to be monitored.
* @param {function(Event)} callback Callback triggered on events detected.
* @constructor
*/
camera.util.PointerTracker = function(element, callback) {
/**
* @type {HTMLElement}
* @private
*/
this.element_ = element;
/**
* @type {function(Event)}
* @private
*/
this.callback_ = callback;
/**
* @type {Array.<number>}
* @private
*/
this.lastMousePosition_ = null;
// End of properties. Seal the object.
Object.seal(this);
// Add the event listeners.
this.element_.addEventListener('mousedown', this.onMouseDown_.bind(this));
this.element_.addEventListener('mousemove', this.onMouseMove_.bind(this));
this.element_.addEventListener('touchstart', this.onTouchStart_.bind(this));
this.element_.addEventListener('touchmove', this.onTouchMove_.bind(this));
};
/**
* Handles the mouse down event.
*
* @param {Event} event Mouse down event.
* @private
*/
camera.util.PointerTracker.prototype.onMouseDown_ = function(event) {
this.callback_(event);
this.lastMousePosition_ = [event.screenX, event.screenY];
};
/**
* Handles the mouse move event.
*
* @param {Event} event Mouse move event.
* @private
*/
camera.util.PointerTracker.prototype.onMouseMove_ = function(event) {
// Ignore mouse events, which are invoked on the same position, but with
// changed client coordinates. This will happen eg. when scrolling. We should
// ignore them, since they are not invoked by an actual mouse move.
if (this.lastMousePosition_ && this.lastMousePosition_[0] == event.screenX &&
this.lastMousePosition_[1] == event.screenY) {
return;
}
this.callback_(event);
this.lastMousePosition_ = [event.screenX, event.screenY];
};
/**
* Handles the touch start event.
*
* @param {Event} event Touch start event.
* @private
*/
camera.util.PointerTracker.prototype.onTouchStart_ = function(event) {
this.callback_(event);
};
/**
* Handles the touch move event.
*
* @param {Event} event Touch move event.
* @private
*/
camera.util.PointerTracker.prototype.onTouchMove_ = function(event) {
this.callback_(event);
};
/**
* Tracks scrolling and calls a callback, when scrolling is started and ended
* by either the scroller or the user.
*
* @param {camera.util.SmoothScroller} scroller Scroller object to be tracked.
* @param {function()} onScrollStarted Callback called when scrolling is
* started.
* @param {function()} onScrollEnded Callback called when scrolling is ended.
* @constructor
*/
camera.util.ScrollTracker = function(scroller, onScrollStarted, onScrollEnded) {
/**
* @type {camera.util.SmoothScroller}
* @private
*/
this.scroller_ = scroller;
/**
* @type {function()}
* @private
*/
this.onScrollStarted_ = onScrollStarted;
/**
* @type {function()}
* @private
*/
this.onScrollEnded_ = onScrollEnded;
/**
* Timer to probe for scroll changes, every 100 ms.
* @type {?number}
* @private
*/
this.timer_ = null;
/**
* Workaround for: crbug.com/135780.
* @type {?number}
* @private
*/
this.noChangeTimer_ = null;
/**
* @type {boolean}
* @private
*/
this.scrolling_ = false;
/**
* @type {Array.<number>}
* @private
*/
this.startScrollPosition_ = [0, 0];
/**
* @type {Array.<number>}
* @private
*/
this.lastScrollPosition_ = [0, 0];
/**
* Whether the touch screen is currently touched.
* @type {boolean}
* @private
*/
this.touchPressed_ = false;
/**
* Whether the touch screen is currently touched.
* @type {boolean}
* @private
*/
this.mousePressed_ = false;
// End of properties. Seal the object.
Object.seal(this);
// Register event handlers.
this.scroller_.element.addEventListener(
'mousedown', this.onMouseDown_.bind(this));
this.scroller_.element.addEventListener(
'touchstart', this.onTouchStart_.bind(this));
window.addEventListener('mouseup', this.onMouseUp_.bind(this));
window.addEventListener('touchend', this.onTouchEnd_.bind(this));
};
camera.util.ScrollTracker.prototype = {
/**
* @return {boolean} Whether scrolling is being performed or not.
*/
get scrolling() {
return this.scrolling_;
},
/**
* @return {Array.<number>} Returns distance of the last detected scroll.
*/
get delta() {
return [
this.startScrollPosition_[0] - this.lastScrollPosition_[0],
this.startScrollPosition_[1] - this.lastScrollPosition_[1]
];
}
};
/**
* Handles pressing the mouse button.
* @param {Event} event Mouse down event.
* @private
*/
camera.util.ScrollTracker.prototype.onMouseDown_ = function(event) {
this.mousePressed_ = true;
};
/**
* Handles releasing the mouse button.
* @param {Event} event Mouse up event.
* @private
*/
camera.util.ScrollTracker.prototype.onMouseUp_ = function(event) {
this.mousePressed_ = false;
};
/**
* Handles touching the screen.
* @param {Event} event Mouse down event.
* @private
*/
camera.util.ScrollTracker.prototype.onTouchStart_ = function(event) {
this.touchPressed_ = true;
};
/**
* Handles releasing touching of the screen.
* @param {Event} event Mouse up event.
* @private
*/
camera.util.ScrollTracker.prototype.onTouchEnd_ = function(event) {
this.touchPressed_ = false;
};
/**
* Starts monitoring.
*/
camera.util.ScrollTracker.prototype.start = function() {
if (this.timer_ !== null)
return;
this.timer_ = setInterval(this.probe_.bind(this), 100);
};
/**
* Stops monitoring.
*/
camera.util.ScrollTracker.prototype.stop = function() {
if (this.timer_ === null)
return;
clearTimeout(this.timer_);
this.timer_ = null;
};
/**
* Probes for scrolling changes.
* @private
*/
camera.util.ScrollTracker.prototype.probe_ = function() {
var scrollLeft = this.scroller_.scrollLeft;
var scrollTop = this.scroller_.scrollTop;
var scrollChanged =
scrollLeft != this.lastScrollPosition_[0] ||
scrollTop != this.lastScrollPosition_[1] ||
this.scroller_.animating;
if (scrollChanged) {
if (!this.scrolling_) {
this.startScrollPosition_ = [scrollLeft, scrollTop];
this.onScrollStarted_();
}
this.scrolling_ = true;
} else {
if (!this.mousePressed_ && !this.touchPressed_ && this.scrolling_) {
this.onScrollEnded_();
this.scrolling_ = false;
}
}
// Workaround for: crbug.com/135780.
// When scrolling by touch screen, the touchend event is not emitted. So, a
// timer has to be used as a fallback to detect the end of scrolling.
if (this.touchPressed_) {
if (scrollChanged) {
// Scrolling changed, cancel the timer.
if (this.noChangeTimer_) {
clearTimeout(this.noChangeTimer_);
this.noChangeTimer_ = null;
}
} else {
// Scrolling previously, but now no change is detected, so set a timer.
if (this.scrolling_ && !this.noChangeTimer_) {
this.noChangeTimer_ = setTimeout(function() {
this.onScrollEnded_();
this.scrolling_ = false;
this.touchPressed_ = false;
this.noChangeTimer_ = null;
}.bind(this), 200);
}
}
}
this.lastScrollPosition_ = [scrollLeft, scrollTop];
};
/**
* Makes an element scrollable by dragging with a mouse.
*
* @param {camera.util.Scroller} scroller Scroller for the element.
* @constructor
*/
camera.util.MouseScroller = function(scroller) {
/**
* @type {camera.util.Scroller}
* @private
*/
this.scroller_ = scroller;
/**
* @type {Array.<number>}
* @private
*/
this.startPosition_ = null;
/**
* @type {Array.<number>}
* @private
*/
this.startScrollPosition_ = null;
// End of properties. Seal the object.
Object.seal(this);
// Register mouse handlers.
this.scroller_.element.addEventListener(
'mousedown', this.onMouseDown_.bind(this));
window.addEventListener('mousemove', this.onMouseMove_.bind(this));
window.addEventListener('mouseup', this.onMouseUp_.bind(this));
};
/**
* Handles the mouse down event on the tracked element.
* @param {Event} event Mouse down event.
* @private
*/
camera.util.MouseScroller.prototype.onMouseDown_ = function(event) {
if (event.which != 1)
return;
this.startPosition_ = [event.screenX, event.screenY];
this.startScrollPosition_ = [
this.scroller_.scrollLeft,
this.scroller_.scrollTop
];
};
/**
* Handles moving a mouse over the tracker element.
* @param {Event} event Mouse move event.
* @private
*/
camera.util.MouseScroller.prototype.onMouseMove_ = function(event) {
if (!this.startPosition_)
return;
// It may happen that we won't receive the mouseup event, when clicking on
// the -webkit-app-region: drag area.
if (event.which != 1) {
this.startPosition_ = null;
this.startScrollPosition_ = null;
return;
}
var scrollLeft =
this.startScrollPosition_[0] - (event.screenX - this.startPosition_[0]);
var scrollTop =
this.startScrollPosition_[1] - (event.screenY - this.startPosition_[1]);
this.scroller_.scrollTo(
scrollLeft, scrollTop, camera.util.SmoothScroller.Mode.INSTANT);
};
/**
* Handles the mouse up event on the tracked element.
* @param {Event} event Mouse down event.
* @private
*/
camera.util.MouseScroller.prototype.onMouseUp_ = function(event) {
this.startPosition_ = null;
this.startScrollPosition_ = null;
};
/**
* Monitors performance by calculating FPS.
* @constructor
*/
camera.util.PerformanceMonitor = function() {
/**
* Stores an array of probes, as an array of pair (timestamp, duration) of
* measurements.
*
* @type {Array.<number, number>}
* @private
*/
this.probes_ = [];
/**
* @type {number}
* @private
*/
this.tailStartTime_ = performance.now();
// No more properties, seal the object.
Object.seal(this);
};
/**
* Length of history tail in milliseconds. Older probes will be discarded.
* @type {number}
* @const
*/
camera.util.PerformanceMonitor.HISTORY_LENGTH = 3 * 1000;
camera.util.PerformanceMonitor.prototype = {
/**
* @return {number} Number of measurements per second.
*/
get fps() {
return this.tailStartTime_ ? this.probes_.length /
(performance.now() - this.tailStartTime_) * 1000 : 0;
},
/**
* @return {number} Average measurment duration in ms.
*/
get average() {
var result = 0;
if (!this.probes_.length)
return 0;
for (var i = 0; i < this.probes_.length; i++) {
result += this.probes_[i][1];
}
return result / this.probes_.length;
}
};
/**
* Resets the monitor.
*/
camera.util.PerformanceMonitor.prototype.reset = function() {
this.tailStartTime_ = performance.now();
this.probes_ = [];
};
/**
* Stars measuring a task execution time.
* @return {function()} Callback to be called, when the task is finished.
*/
camera.util.PerformanceMonitor.prototype.startMeasuring = function() {
var startTime = performance.now();
return this.finishMeasuring_.bind(this, startTime);
};
/**
* Finishes measuring.
* @param {number} startTime Start time in milliseconds.
* @private
*/
camera.util.PerformanceMonitor.prototype.finishMeasuring_ = function(
startTime) {
this.probes_.push([performance.now(), performance.now() - startTime]);
// Discard old probes.
var threshold =
performance.now() - camera.util.PerformanceMonitor.HISTORY_LENGTH;
var i = 0;
while (i < this.probes_.length && this.probes_[i][0] < threshold) {
i++;
}
if (i > 0) {
this.tailStartTime_ = this.probes_[i][0];
this.probes_.splice(0, i);
}
};
/**
* Manages multiple monitors in a name-keyed map.
* @constructor
*/
camera.util.NamedPerformanceMonitors = function() {
/**
* @type {Object.<camera.util.PerformanceMonitor}
* @private
*/
this.monitors_ = {};
// No more properties, seal the object.
Object.seal(this);
};
/**
* Gets a named monitor. If doesn't exist, then creates it.
* @param {string} name Identifier.
* @return {camera.util.PerformanceMonitor}
* @private
*/
camera.util.NamedPerformanceMonitors.prototype.get_ = function(name) {
if (!this.monitors_[name])
this.monitors_[name] = new camera.util.PerformanceMonitor();
return this.monitors_[name];
};
/**
* Starts measuring a task execution time for the specific monitor.
* @param {string} name Identifier.
* @return {function()} Callback to be called, when the task is finished.
*/
camera.util.NamedPerformanceMonitors.prototype.startMeasuring = function(name) {
return this.get_(name).startMeasuring();
};
/**
* Resets all monitors.
*/
camera.util.NamedPerformanceMonitors.prototype.reset = function() {
Object.keys(this.monitors_).forEach(function(identifier) {
this.monitors_[identifier].reset();
}.bind(this));
};
/**
* Returns a debug string.
* @return {string} Debug string.
*/
camera.util.NamedPerformanceMonitors.prototype.toDebugString = function() {
var result = '';
Object.keys(this.monitors_).forEach(function(identifier) {
result += identifier + ': ' + this.average(identifier) +
' ms @ ' + this.fps(identifier).toPrecision(2) + ' fps\n';
}.bind(this));
return result;
};
/**
* Returns a fps value for the named monitor.
* @param {string} Identifier.
* @return {number} Number of frames per second.
*/
camera.util.NamedPerformanceMonitors.prototype.fps = function(name) {
return this.get_(name).fps;
};
/**
* Returns an average measurement duration value for the named monitor.
* @param {string} Identifier.
* @return {number} Average measurement duration in ms
*/
camera.util.NamedPerformanceMonitors.prototype.average = function(name) {
return this.get_(name).average;
};
/**
* Returns a shortcut string, such as Ctrl-Alt-A.
* @param {Event} event Keyboard event.
* @return {string} Shortcut identifier.
*/
camera.util.getShortcutIdentifier = function(event) {
var identifier = (event.ctrlKey ? 'Ctrl-' : '') +
(event.altKey ? 'Alt-' : '') +
(event.shiftKey ? 'Shift-' : '') +
(event.metaKey ? 'Meta-' : '');
// Handle both KeyboardEvent keyIdentifier and key as keyIdentifier is
// deprecated since Chrome M54 and key is not supported prior Chrome M51.
if (event.keyIdentifier && !event.key) {
switch (event.keyIdentifier) {
case 'U+001B':
identifier += 'Escape';
break;
case 'U+007F':
identifier += 'Delete';
break;
case 'U+0020':
identifier += 'Space';
break;
case 'U+0041':
identifier += 'A';
break;
case 'U+0050':
identifier += 'P';
break;
case 'U+0053':
identifier += 'S';
break;
case 'U+0047':
identifier += 'G';
break;
default:
identifier += event.keyIdentifier;
}
}
if (event.key) {
switch (event.key) {
case 'ArrowLeft':
identifier += 'Left';
break;
case 'ArrowRight':
identifier += 'Right';
break;
case 'ArrowDown':
identifier += 'Down';
break;
case 'ArrowUp':
identifier += 'Up';
break;
case ' ':
identifier += 'Space';
break;
case 'a':
identifier += 'A';
break;
case 'p':
identifier += 'P';
break;
case 's':
identifier += 'S';
break;
case 'g':
identifier += 'G';
break;
default:
identifier += event.key;
}
}
return identifier;
};
/**
* Makes all elements with a tabindex attribute unfocusable by mouse.
*/
camera.util.makeElementsUnfocusableByMouse = function() {
var elements = document.querySelectorAll('[tabindex]');
for (var index = 0; index < elements.length; index++) {
elements[index].addEventListener('mousedown', function(event) {
event.preventDefault();
});
}
};
/**
* Makes the elements pullable via touch and mouse. Only vertical orientation is
* currently supported.
*
* @param {HTMLElement} wrapper Wrapper of the element to be used for
* positioning while pulling.
* @param {HTMLElement} element Element to be made pullable.
* @param {function(number)} onPullReleased Callback with the pulling distance
* in percent points.
* @constructor
*/
camera.util.Puller = function(wrapper, element, onPullReleased) {
/**
* @type {HTMLElement} element
* @private
*/
this.wrapper_ = wrapper;
/**
* @type {HTMLElement} element
* @private
*/
this.element_ = element;
/**
* @type {function(number)}
* @private
*/
this.onPullReleased_ = onPullReleased;
/**
* @type {Array.<number>}
* @private
*/
this.pullStartPoint_ = null;
/**
* @type {Array.<number>}
* @private
*/
this.pullLastPoint_ = null;
// End of properties, seal the object.
Object.seal(this);
// Register handlers for both touch and mouse.
this.element_.addEventListener('touchstart', this.onTouchStart_.bind(this));
window.addEventListener('touchmove', this.onTouchMove_.bind(this));
window.addEventListener('touchend', this.onTouchEnd_.bind(this));
this.element_.addEventListener('mousedown', this.onMouseDown_.bind(this));
window.addEventListener('mousemove', this.onMouseMove_.bind(this), true);
window.addEventListener('mouseup', this.onMouseUp_.bind(this));
};
/**
* Handles start of pulling at passed coordinates.
*
* @param {number} x Horizontal coordinate in pixels.
* @param {number} y Vertical coordinate in pixels.
* @private
*/
camera.util.Puller.prototype.startPulling_ = function(x, y) {
this.pullStartPoint_ = [x, y];
this.pullLastPoint_ = [x, y];
this.wrapper_.classList.remove('puller-reset');
};
/**
* Handles update of pulling at passed coordinates.
*
* @param {number} x Horizontal coordinate in pixels.
* @param {number} y Vertical coordinate in pixels.
* @return {boolean} True if the event got handled, false otherwide.
* @private
*/
camera.util.Puller.prototype.updatePulling_ = function(x, y) {
if (!this.pullStartPoint_)
return false;
var distance = (y - this.pullStartPoint_[1]);
this.wrapper_.style.webkitTransform = 'translateY(' + distance / 3 + 'px)';
this.pullLastPoint_ = [x, y];
return true;
};
/**
* Handles end of pulling at passed coordinates.
* @private
*/
camera.util.Puller.prototype.endPulling_ = function() {
if (!this.pullStartPoint_)
return;
var distance = (this.pullLastPoint_[1] - this.pullStartPoint_[1]);
this.onPullReleased_(distance);
this.wrapper_.classList.add('puller-reset');
this.wrapper_.style.webkitTransform = '';
this.pullStartPoint_ = null;
this.pullLastPoint_ = null;
};
/**
* Handles the touch start event.
* @param {Event} event Touch event.
* @private
*/
camera.util.Puller.prototype.onTouchStart_ = function(event) {
this.startPulling_(
event.targetTouches[0].screenX, event.targetTouches[0].screenY);
};
/**
* Handles the touch move event.
* @param {Event} event Touch event.
* @private
*/
camera.util.Puller.prototype.onTouchMove_ = function(event) {
if (this.updatePulling_(
event.targetTouches[0].screenX, event.targetTouches[0].screenY)) {
event.preventDefault(); // Prevent native touch scrolling.
}
};
/**
* Handles the touch end event.
* @param {Event} event Touch event.
* @private
*/
camera.util.Puller.prototype.onTouchEnd_ = function(event) {
this.endPulling_();
};
/**
* Handles the mouse down event.
* @param {Event} event Mount event.
* @private
*/
camera.util.Puller.prototype.onMouseDown_ = function(event) {
this.startPulling_(event.screenX, event.screenY);
};
/**
* Handles the mouse move event.
* @param {Event} event Mount event.
* @private
*/
camera.util.Puller.prototype.onMouseMove_ = function(event) {
this.updatePulling_(event.screenX, event.screenY);
};
/**
* Handles the mouse up event.
* @param {Event} event Mount event.
* @private
*/
camera.util.Puller.prototype.onMouseUp_ = function(event) {
this.endPulling_();
};