| // Copyright (c) 2013 The Chromium 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 cca = cca || {}; |
| |
| /** |
| * Namespace for utilities. |
| */ |
| cca.util = cca.util || {}; |
| |
| /** |
| * Gets the clockwise rotation and flip that can orient a photo to its upright |
| * position. |
| * @param {!Blob} blob JPEG blob that might contain EXIF orientation field. |
| * @return {Promise<Object<number, boolean>>} |
| */ |
| cca.util.getPhotoOrientation = function(blob) { |
| let getOrientation = new Promise((resolve, reject) => { |
| let reader = new FileReader(); |
| reader.onload = function(event) { |
| let view = new DataView(event.target.result); |
| if (view.getUint16(0, false) != 0xFFD8) { |
| resolve(1); |
| return; |
| } |
| let length = view.byteLength; |
| let offset = 2; |
| while (offset < length) { |
| if (view.getUint16(offset + 2, false) <= 8) { |
| break; |
| } |
| let marker = view.getUint16(offset, false); |
| offset += 2; |
| if (marker == 0xFFE1) { |
| if (view.getUint32(offset += 2, false) != 0x45786966) { |
| break; |
| } |
| |
| let little = view.getUint16(offset += 6, false) == 0x4949; |
| offset += view.getUint32(offset + 4, little); |
| let tags = view.getUint16(offset, little); |
| offset += 2; |
| for (let i = 0; i < tags; i++) { |
| if (view.getUint16(offset + (i * 12), little) == 0x0112) { |
| resolve(view.getUint16(offset + (i * 12) + 8, little)); |
| return; |
| } |
| } |
| } else if ((marker & 0xFF00) != 0xFF00) { |
| break; |
| } else { |
| offset += view.getUint16(offset, false); |
| } |
| } |
| resolve(1); |
| }; |
| reader.readAsArrayBuffer(blob); |
| }); |
| |
| return getOrientation.then((orientation) => { |
| switch (orientation) { |
| case 1: |
| return {rotation: 0, flip: false}; |
| case 2: |
| return {rotation: 0, flip: true}; |
| case 3: |
| return {rotation: 180, flip: false}; |
| case 4: |
| return {rotation: 180, flip: true}; |
| case 5: |
| return {rotation: 90, flip: true}; |
| case 6: |
| return {rotation: 90, flip: false}; |
| case 7: |
| return {rotation: 270, flip: true}; |
| case 8: |
| return {rotation: 270, flip: false}; |
| default: |
| return {rotation: 0, flip: false}; |
| } |
| }); |
| }; |
| |
| /** |
| * Orients a photo to the upright orientation. |
| * @param {!Blob} blob Photo as a blob. |
| * @param {function(Blob)} onSuccess Success callback with the result photo as |
| * a blob. |
| * @param {function()} onFailure Failure callback. |
| */ |
| cca.util.orientPhoto = function(blob, onSuccess, onFailure) { |
| // TODO(shenghao): Revise or remove this function if it's no longer |
| // applicable. |
| let drawPhoto = function(original, orientation, onSuccess, onFailure) { |
| let canvas = document.createElement('canvas'); |
| let context = canvas.getContext('2d'); |
| let canvasSquareLength = Math.max(original.width, original.height); |
| canvas.width = canvasSquareLength; |
| canvas.height = canvasSquareLength; |
| |
| let [centerX, centerY] = [canvas.width / 2, canvas.height / 2]; |
| context.translate(centerX, centerY); |
| context.rotate(orientation.rotation * Math.PI / 180); |
| if (orientation.flip) { |
| context.scale(-1, 1); |
| } |
| context.drawImage(original, -original.width / 2, -original.height / 2, |
| original.width, original.height); |
| if (orientation.flip) { |
| context.scale(-1, 1); |
| } |
| context.rotate(-orientation.rotation * Math.PI / 180); |
| context.translate(-centerX, -centerY); |
| |
| let outputCanvas = document.createElement('canvas'); |
| if (orientation.rotation == 90 || orientation.rotation == 270) { |
| outputCanvas.width = original.height; |
| outputCanvas.height = original.width; |
| } else { |
| outputCanvas.width = original.width; |
| outputCanvas.height = original.height; |
| } |
| let imageData = context.getImageData( |
| (canvasSquareLength - outputCanvas.width) / 2, |
| (canvasSquareLength - outputCanvas.height) / 2, |
| outputCanvas.width, outputCanvas.height); |
| let outputContext = outputCanvas.getContext('2d'); |
| outputContext.putImageData(imageData, 0, 0); |
| |
| outputCanvas.toBlob(function(blob) { |
| if (blob) { |
| onSuccess(blob); |
| } else { |
| onFailure(); |
| } |
| }, 'image/jpeg'); |
| }; |
| |
| cca.util.getPhotoOrientation(blob).then((orientation) => { |
| if (orientation.rotation == 0 && !orientation.flip) { |
| onSuccess(blob); |
| } else { |
| let original = document.createElement('img'); |
| original.onload = function() { |
| drawPhoto(original, orientation, onSuccess, onFailure); |
| }; |
| original.onerror = onFailure; |
| original.src = URL.createObjectURL(blob); |
| } |
| }); |
| }; |
| |
| /** |
| * Returns true if current installed Chrome version is larger than or equal to |
| * the given version. |
| * @param {number} minVersion the version to be compared with. |
| * @return {boolean} |
| */ |
| cca.util.isChromeVersionAbove = function(minVersion) { |
| var match = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./); |
| return (match ? parseInt(match[2], 10) : 0) >= minVersion; |
| }; |
| |
| /* |
| * Checks if the user is using a Chrome OS device. |
| * @return {boolean} Whether it is a Chrome OS device or not. |
| */ |
| cca.util.isChromeOS = function() { |
| return navigator.appVersion.indexOf('CrOS') !== -1; |
| }; |
| |
| /** |
| * Animates the element once by applying 'animate' class. |
| * @param {HTMLElement} element Element to be animated. |
| * @param {function()=} callback Callback called on completion. |
| */ |
| cca.util.animateOnce = function(element, callback) { |
| element.classList.remove('animate'); |
| /** @suppress {suspiciousCode} */ |
| element.offsetWidth; // Force calculation to re-apply animation. |
| element.classList.add('animate'); |
| cca.util.waitAnimationCompleted(element, () => { |
| element.classList.remove('animate'); |
| if (callback) { |
| callback(); |
| } |
| }); |
| }; |
| |
| /** |
| * Cancels animating the element by removing 'animate' class. |
| * @param {HTMLElement} element Element for canceling animation. |
| */ |
| cca.util.animateCancel = function(element) { |
| element.classList.remove('animate'); |
| }; |
| |
| /** |
| * Waits for animation completed and calls the callback. |
| * @param {HTMLElement} element Element to be animated. |
| * @param {function()} callback Callback called on completion. |
| */ |
| cca.util.waitAnimationCompleted = function(element, callback) { |
| var completed = false; |
| var onCompleted = (event) => { |
| if (completed || (event && event.target != element)) { |
| return; |
| } |
| completed = true; |
| element.removeEventListener('transitionend', onCompleted); |
| element.removeEventListener('animationend', onCompleted); |
| callback(); |
| }; |
| // Assume only either 'transition' or 'animation' is applied on the element. |
| // Listen to both end-events for its completion. |
| element.addEventListener('transitionend', onCompleted); |
| element.addEventListener('animationend', onCompleted); |
| }; |
| |
| /** |
| * Scrolls the parent of the element so the element is centered. |
| * @param {HTMLElement} element Element to be visible. |
| * @param {cca.util.SmoothScroller} scroller Scroller to be used. |
| * @param {cca.util.SmoothScroller.Mode} mode Scrolling mode. |
| */ |
| cca.util.scrollToCenter = function(element, scroller, 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, mode); |
| }; |
| |
| /** |
| * 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 |
| */ |
| cca.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 |
| */ |
| cca.util.SmoothScroller.DURATION = 500; |
| |
| /** |
| * Mode of scrolling. |
| * @enum {number} |
| */ |
| cca.util.SmoothScroller.Mode = { |
| SMOOTH: 0, |
| INSTANT: 1, |
| }; |
| |
| cca.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 |
| */ |
| cca.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 {cca.util.SmoothScroller.Mode} mode Scrolling mode. |
| */ |
| cca.util.SmoothScroller.prototype.scrollTo = function(x, y, mode) { |
| // 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 cca.util.SmoothScroller.Mode.INSTANT: |
| // Cancel any current animations. |
| if (this.animating_) { |
| this.flushScroll_(); |
| } |
| this.element_.scrollLeft = x; |
| this.element_.scrollTop = y; |
| break; |
| |
| case cca.util.SmoothScroller.Mode.SMOOTH: |
| // Calculate translating offset using the accelerated CSS3 transform. |
| var dx = Math.round(x - this.element_.scrollLeft); |
| var dy = Math.round(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 ' + |
| cca.util.SmoothScroller.DURATION + 'ms ease-out'; |
| this.padder_.style.webkitTransform = transformString; |
| |
| // Remove translation, and switch to scrollLeft/scrollTop when the |
| // animation is finished. |
| cca.util.waitAnimationCompleted(this.padder_, () => { |
| // Check if the animation got invalidated by a later scroll. |
| if (currentAnimationId == this.animationId_) { |
| this.flushScroll_(); |
| } |
| }); |
| break; |
| } |
| }; |
| |
| /** |
| * 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 |
| */ |
| cca.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 |
| */ |
| cca.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 |
| */ |
| cca.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 |
| */ |
| cca.util.PointerTracker.prototype.onTouchStart_ = function(event) { |
| this.callback_(event); |
| }; |
| |
| /** |
| * Handles the touch move event. |
| * @param {Event} event Touch move event. |
| * @private |
| */ |
| cca.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 {cca.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 |
| */ |
| cca.util.ScrollTracker = function(scroller, onScrollStarted, onScrollEnded) { |
| /** |
| * @type {cca.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)); |
| }; |
| |
| cca.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 |
| */ |
| cca.util.ScrollTracker.prototype.onMouseDown_ = function(event) { |
| this.mousePressed_ = true; |
| }; |
| |
| /** |
| * Handles releasing the mouse button. |
| * @param {Event} event Mouse up event. |
| * @private |
| */ |
| cca.util.ScrollTracker.prototype.onMouseUp_ = function(event) { |
| this.mousePressed_ = false; |
| }; |
| |
| /** |
| * Handles touching the screen. |
| * @param {Event} event Mouse down event. |
| * @private |
| */ |
| cca.util.ScrollTracker.prototype.onTouchStart_ = function(event) { |
| this.touchPressed_ = true; |
| }; |
| |
| /** |
| * Handles releasing touching of the screen. |
| * @param {Event} event Mouse up event. |
| * @private |
| */ |
| cca.util.ScrollTracker.prototype.onTouchEnd_ = function(event) { |
| this.touchPressed_ = false; |
| }; |
| |
| /** |
| * Starts monitoring. |
| */ |
| cca.util.ScrollTracker.prototype.start = function() { |
| if (this.timer_ === null) { |
| this.timer_ = setInterval(this.probe_.bind(this), 100); |
| } |
| }; |
| |
| /** |
| * Stops monitoring. |
| */ |
| cca.util.ScrollTracker.prototype.stop = function() { |
| if (this.timer_ !== null) { |
| clearTimeout(this.timer_); |
| this.timer_ = null; |
| } |
| }; |
| |
| /** |
| * Probes for scrolling changes. |
| * @private |
| */ |
| cca.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 {cca.util.SmoothScroller} scroller SmoothScroller for the element. |
| * @constructor |
| */ |
| cca.util.MouseScroller = function(scroller) { |
| /** |
| * @type {cca.util.SmoothScroller} |
| * @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 |
| */ |
| cca.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 |
| */ |
| cca.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, cca.util.SmoothScroller.Mode.INSTANT); |
| }; |
| |
| /** |
| * Handles the mouse up event on the tracked element. |
| * @param {Event} event Mouse down event. |
| * @private |
| */ |
| cca.util.MouseScroller.prototype.onMouseUp_ = function(event) { |
| this.startPosition_ = null; |
| this.startScrollPosition_ = null; |
| }; |
| |
| /** |
| * Returns a shortcut string, such as Ctrl-Alt-A. |
| * @param {Event} event Keyboard event. |
| * @return {string} Shortcut identifier. |
| */ |
| cca.util.getShortcutIdentifier = function(event) { |
| var identifier = (event.ctrlKey ? 'Ctrl-' : '') + |
| (event.altKey ? 'Alt-' : '') + |
| (event.shiftKey ? 'Shift-' : '') + |
| (event.metaKey ? 'Meta-' : ''); |
| 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 'a': |
| case 'p': |
| case 's': |
| case 'v': |
| case 'r': |
| identifier += event.key.toUpperCase(); |
| break; |
| default: |
| identifier += event.key; |
| } |
| } |
| return identifier; |
| }; |
| |
| /** |
| * Makes the element unfocusable by mouse. |
| * @param {HTMLElement} element Element to be unfocusable. |
| */ |
| cca.util.makeUnfocusableByMouse = function(element) { |
| element.addEventListener('mousedown', (event) => event.preventDefault()); |
| }; |
| |
| /** |
| * Updates the wrapped element size according to the given bounds. The wrapped |
| * content (either img or video child element) should keep the aspect ratio and |
| * is either filled up or letterboxed inside the wrapper element. |
| * @param {HTMLElement} wrapper Element whose wrapped child to be updated. |
| * @param {number} boundWidth Bound width in pixels. |
| * @param {number} boundHeight Bound height in pixels. |
| * @param {boolean} fill True to fill up and crop the content to the bounds, |
| * false to letterbox the content within the bounds. |
| */ |
| cca.util.updateElementSize = function( |
| wrapper, boundWidth, boundHeight, fill) { |
| // Assume the wrapped child is either img or video element. |
| var child = wrapper.firstElementChild; |
| var srcWidth = child.naturalWidth || child.videoWidth; |
| var srcHeight = child.naturalHeight || child.videoHeight; |
| var f = fill ? Math.max : Math.min; |
| var scale = f(boundHeight / srcHeight, boundWidth / srcWidth); |
| |
| // Corresponding CSS should handle the adjusted sizes for proper display. |
| child.width = Math.round(scale * srcWidth); |
| child.height = Math.round(scale * srcHeight); |
| }; |
| |
| /** |
| * Checks if the window is maximized or fullscreen. |
| * @return {boolean} True if maximized or fullscreen, false otherwise. |
| */ |
| cca.util.isWindowFullSize = function() { |
| // App-window's isFullscreen, isMaximized state and window's outer-size may |
| // not be updated immediately during resizing. Use if app-window's outerBounds |
| // width matches screen width here as workarounds. |
| return chrome.app.window.current().outerBounds.width >= screen.width || |
| chrome.app.window.current().outerBounds.height >= screen.height; |
| }; |
| |
| /** |
| * Opens help. |
| */ |
| cca.util.openHelp = function() { |
| window.open( |
| 'https://support.google.com/chromebook/?p=camera_usage_on_chromebook'); |
| }; |
| |
| /** |
| * Sets up i18n messages on DOM subtree by i18n attributes. |
| * @param {HTMLElement} rootElement Root of DOM subtree to be set up with. |
| */ |
| cca.util.setupI18nElements = function(rootElement) { |
| var getElements = (attr) => rootElement.querySelectorAll('[' + attr + ']'); |
| var getMessage = (element, attr) => |
| chrome.i18n.getMessage(element.getAttribute(attr)); |
| var setAriaLabel = (element, attr) => |
| element.setAttribute('aria-label', getMessage(element, attr)); |
| |
| getElements('i18n-content') |
| .forEach( |
| (element) => element.textContent = |
| getMessage(element, 'i18n-content')); |
| getElements('i18n-tooltip-true') |
| .forEach( |
| (element) => element.setAttribute( |
| 'tooltip-true', getMessage(element, 'i18n-tooltip-true'))); |
| getElements('i18n-tooltip-false') |
| .forEach( |
| (element) => element.setAttribute( |
| 'tooltip-false', getMessage(element, 'i18n-tooltip-false'))); |
| getElements('i18n-aria') |
| .forEach((element) => setAriaLabel(element, 'i18n-aria')); |
| cca.tooltip.setup(getElements('i18n-label')) |
| .forEach((element) => setAriaLabel(element, 'i18n-label')); |
| }; |
| |
| /** |
| * Reads blob into Image. |
| * @param {!Blob} blob |
| * @return {!Promise<!HTMLImageElement>} |
| * @throws {Error} |
| */ |
| cca.util.blobToImage = function(blob) { |
| return new Promise((resolve, reject) => { |
| const img = new Image(); |
| img.onload = () => resolve(img); |
| img.onerror = () => reject(new Error('Failed to load unprocessed image')); |
| img.src = URL.createObjectURL(blob); |
| }); |
| }; |
| |
| /** |
| * Gets default facing according to device mode. |
| * @return {!Promise<string>} |
| */ |
| cca.util.getDefaultFacing = async function() { |
| return await cca.mojo.ChromeHelper.getInstance().isTabletMode() ? |
| 'environment' : |
| 'user'; |
| }; |