/* jshint -W098 */ define(function(require) { 'use strict'; /** * Dependencies */ var debug = require('debug')('view:preview-gallery'); var addPanAndZoomHandlers = require('lib/panzoom'); var orientation = require('lib/orientation'); var MediaFrame = require('MediaFrame'); var bind = require('lib/bind'); var attach = require('attach'); var View = require('view'); /** * `` used in template */ require('gaia-header'); /** * Constants */ var SWIPE_DISTANCE_THRESHOLD = window.innerWidth / 3; // pixels var SWIPE_VELOCITY_THRESHOLD = 1.0; // pixels/ms var SWIPE_DURATION = 150; // How long to animate the swipe var FADE_IN_DURATION = 450; // How long to animate the fade in after swipe /** * Locals */ return View.extend({ name: 'preview-gallery', className: 'offscreen', initialize: function() { debug('initialized'); }, render: function() { this.el.innerHTML = this.template(); this.els.frameContainer = this.find('.js-frame-container'); this.els.previewMenu = this.find('.js-preview-menu'); this.els.mediaFrame = this.find('.js-media-frame'); this.els.countText = this.find('.js-count-text'); this.els.options = this.find('.js-options'); this.els.header = this.find('.js-header'); this.els.share = this.find('.js-share'); // We're appending new elements to DOM so to make sure headers are // properly resized and centered, we emmit a lazyload event. // This will be removed when the gaia-header web component lands. window.dispatchEvent(new CustomEvent('lazyload', { detail: this.el })); // Configure the MediaFrame component this.configure(); // Clean up delete this.template; debug('rendered'); return this.bindEvents(); }, bindEvents: function() { bind(this.el, 'tap', this.onTap); bind(this.els.header, 'action', this.firer('click:back')); bind(this.els.options, 'click', this.onButtonClick); bind(this.els.share, 'click', this.onButtonClick); // Our screen reader synthesizes wheel events when the user swipes with two // fingers in either direction. Those events always have their deltaMode set // to DOM_DELTA_PAGE, and the delta value can either be -1, 0 or 1. bind(this.els.mediaFrame, 'wheel', this.onFrameWheel); return this; }, configure: function() { this.currentIndex = this.lastIndex = 0; this.frame = new MediaFrame(this.els.mediaFrame, true, this.maxPreviewSize); this.frame.video.onloading = this.onVideoLoading; this.frame.video.onplaying = this.onVideoPlaying; this.frame.video.onpaused = this.onVideoPaused; addPanAndZoomHandlers(this.frame, this.swipeCallback); }, template: function() { return '
' + '' + '

Preview

' + '' + '' + '
' + '
' + '
' + '
' + '
' + '
'; }, onTap: function() { if (this.videoPlaying) { return; } var isShown = this.els.previewMenu.classList.contains('visible'); if (isShown) { this.previewMenuFadeOut(); } else { this.previewMenuFadeIn(); } }, previewMenuFadeIn: function() { this.els.previewMenu.classList.add('visible'); }, previewMenuFadeOut: function() { this.els.previewMenu.classList.remove('visible'); }, onFrameWheel: function(event) { if (event.deltaMode !== event.DOM_DELTA_PAGE || event.deltaY) { return; } // If the video is playing, switch to preview. if (this.videoPlaying) { this.onVideoPaused(); } if (event.deltaX > 0) { this.emit('swipe', 'left'); } else if (event.deltaX < 0) { this.emit('swipe', 'right'); } }, swipeCallback: function(swipeAmount, swipeVelocity) { var self = this; if (swipeVelocity === undefined) { // If this is not the end of the gesture, then just move the media // frame left or right by the specified amount. this.els.frameContainer.style.transform = 'translate(' + swipeAmount + 'px, 0)'; return; } // If we were passed a velocity, then this is the end of the gesture // and we have to figure out whether we are going to go to the // next item, the previous item, or remain on the current item. var direction, translation; // Should we move to the previous item? if (swipeAmount > 0 && swipeVelocity > 0 && // Same sign and this.currentIndex > 1 && // has previous item and (swipeAmount > SWIPE_DISTANCE_THRESHOLD || // distance big enough or swipeVelocity > SWIPE_VELOCITY_THRESHOLD)) // speed fast enough { direction = 'right'; translation = '100%'; } // Should we move to the next item? else if (swipeAmount <= 0 && swipeVelocity <= 0 && this.currentIndex < this.lastIndex && (swipeAmount < -SWIPE_DISTANCE_THRESHOLD || swipeVelocity < -SWIPE_VELOCITY_THRESHOLD)) { direction = 'left'; translation = '-100%'; } // If we're not moving either left or right, just animate the // item to its starting point and we're done. if (!direction) { animate('transform', 'translate(0,0)', SWIPE_DURATION); return; } // If we get here, we are going to slide the current item off the screen // and display the next or previous item. // First, stop the video if there is one and it is playing. if (this.videoPlaying) { this.onVideoPaused(); } // Now animate the item off the screen animate('transform', 'translate(' + translation + ', 0)', SWIPE_DURATION, function() { // Once we're off screen ask the controller to // switch to the new image. self.emit('swipe', direction); // On the next redraw, put the frame back on the screen // but make it opaque window.requestAnimationFrame(function() { // Next, make the frame invisible // and put it back on the screen self.els.frameContainer.style.opacity = 0; self.els.frameContainer.style.transform = 'translate(0,0)'; // Then on the frame after that, animate the opacity to // make it visible again. window.requestAnimationFrame(function() { animate('opacity', 1, FADE_IN_DURATION); }); }); }); // A helper function to animate the specified CSS property to the // specified value for the specified duration. When the animation // is complete, reset everything and call the done callback function animate(property, value, duration, done) { var e = self.els.frameContainer; e.addEventListener('transitionend', onTransitionEnd); e.style.transitionProperty = property; e.style.transitionDuration = duration + 'ms'; e.style[property] = value; function onTransitionEnd() { e.removeEventListener('transitionend', onTransitionEnd); delete e.style.transitionProperty; delete e.style.transitionDuration; if (done) { done(); } } } }, onButtonClick: function(e, el) { if (this.videoPlaying) { return; } el = el || e.currentTarget; var name = el.getAttribute('name'); if (this.optionsMenuContainer) { this.hideOptionsMenu(); } this.emit('click:' + name, e); e.stopPropagation(); }, open: function() { window.addEventListener('resize', this.onResize); orientation.unlock(); this.previewMenuFadeIn(); this.el.classList.remove('offscreen'); }, close: function() { window.removeEventListener('resize', this.onResize); orientation.lock(); this.previewMenuFadeOut(); this.el.classList.add('offscreen'); this.frame.clear(); }, updateCountText: function(current, total) { this.currentIndex = current; this.lastIndex = total; this.els.countText.textContent = current + '/' + total; // Ensure that the information about the number of thumbnails is provided to // the screen reader. this.els.mediaFrame.setAttribute('data-l10n-id', 'media-frame'); this.els.mediaFrame.setAttribute('data-l10n-args', JSON.stringify( { total: total, current: current })); }, onResize: function() { // And we have to resize the frame (and its video player) this.frame.resize(); if (this.frame.displayingVideo) { this.frame.video.setPlayerSize(); } }, showImage: function(image) { this.frame.displayImage( image.blob, image.width, image.height, image.preview, image.rotation, image.mirrored); }, showVideo: function(video) { this.frame.displayVideo( video.blob, video.poster.blob, video.poster.width / video.poster.height, video.rotation); }, onVideoLoading: function() { this.emit('loadingvideo', 'loadingVideo'); }, onVideoPlaying: function() { if (this.videoPlaying) { return; } this.videoPlaying = true; this.previewMenuFadeOut(); this.emit('playingvideo'); }, onVideoPaused: function() { if (!this.videoPlaying) { return; } this.videoPlaying = false; this.previewMenuFadeIn(); }, showOptionsMenu: function() { this.optionsMenuContainer = document.createElement('div'); this.optionsMenuContainer.innerHTML = this.optionTemplate(); this.el.appendChild(this.optionsMenuContainer); this.el.classList.add('action-menu'); this.menu = this.find('.js-menu'); // We add the event listner for menu items and cancel buttons var cancelButton = this.find('.js-cancel'); bind(cancelButton, 'click', this.hideOptionsMenu); if (this.menu) { attach.on(this.menu, 'click', '.js-btn', this.onButtonClick); } }, hideOptionsMenu: function() { if (this.optionsMenuContainer) { this.el.classList.remove('action-menu'); this.optionsMenuContainer.parentElement.removeChild( this.optionsMenuContainer); this.optionsMenuContainer = null; } }, optionTemplate: function() { return '
' + '
Options
' + '' + '' + '' + '' + '' + '
'; } }); });