/** * ModuloBox PACKAGED v1.3.0 * Touch & responsive multimedia Lightbox * * @author Themeone [https://theme-one.com/] * Copyright © 2016 All Rights Reserved. */ /* global navigator, location, window, document, screen, history, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame, XMLHttpRequest, Image */ /* global define, module, require, jQuery, MediaElementPlayer */ /** * requestAnimationFrame polyfill * Modified version of Paul Irish (https://gist.github.com/paulirish/1579671) * v1.0.0 */ ( function() { var win = window, lastTime = 0; // get rAF, prefixed, if present win.requestAnimationFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame; // fallback to setTimeout if ( !win.requestAnimationFrame ) { win.requestAnimationFrame = function( callback ) { var currTime = new Date().getTime(), timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ), id = setTimeout( callback, timeToCall ); lastTime = currTime + timeToCall; return id; }; } // fallback to setTimeout if ( !win.cancelAnimationFrame ) { win.cancelAnimationFrame = function( id ) { clearTimeout( id ); }; } }()); /** * Themeone Utils * Utilities * v1.0.0 */ ( function( root, factory ) { if ( typeof define === 'function' && define.amd ) { // AMD define( 'themeone-utils/utils', factory ); } else if ( typeof module === 'object' && module.exports ) { // Node, CommonJS-like module.exports = factory(); } else { // Browser globals (root is window) root.ThemeoneUtils = factory(); } }( this, function() { "use strict"; var utils = {}; var Console = window.console; /** * Output console error * @param {string} message */ utils.error = function( message ) { if ( typeof Console !== 'undefined' ) { Console.error( message ); } }; /** * Extend an Object * @param {Object} options * @param {Object} defaults * @return {Object} defaults */ utils.extend = function( options, defaults ) { if ( options ) { if ( typeof options !== 'object' ) { this.error( 'Custom options must be an object' ); } else { for ( var prop in defaults ) { if ( defaults.hasOwnProperty( prop ) && options.hasOwnProperty( prop ) ) { defaults[prop] = options[prop]; } } } } return defaults; }; /** * Find style property (with vendor prefix) * @param {string} prop * @return {string} prefixedProp */ utils.prop = function( prop ) { var el = this.createEl(), prefixes = ['', 'Webkit', 'Moz', 'ms', 'O']; for ( var p = 0, pl = prefixes.length; p < pl; p++ ) { var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt( 0 ).toUpperCase() + prop.slice( 1 ) : prop; if ( el.style[prefixedProp] !== undefined ) { return prefixedProp; } } return ''; }; /** * Clone object properties * @param {Object} obj * @return {Object} copy */ utils.cloneObject = function( obj ) { var copy = {}; for ( var attr in obj ) { if ( obj.hasOwnProperty( attr ) ) { copy[attr] = obj[attr]; } } return copy; }; /** * Create DOM element * @param {string} tag * @param {string} classes * @return {Object} el */ utils.createEl = function( tag, classes ) { var el = document.createElement( tag || 'div' ); if ( classes ) { el.className = classes; } return el; }; /** * Camel case string * @param {string} string * @return {string} string */ utils.camelize = function( string ) { return string.replace( /-([a-z])/g, function( g ) { return g[1].toUpperCase(); }); }; /** * Add/remove event listeners * @param {Object} _this * @param {Object} el * @param {Array} event * @param {string} fn * @param {boolean} isBind */ utils.handleEvents = function( _this, el, event, fn, isBind ) { if ( typeof this.event_handlers !== 'object' ) { this.event_handlers = {}; } // register handlers for later (to remove) if ( !this.event_handlers[fn] ) { this.event_handlers[fn] = _this[fn].bind( _this ); } // set bind method isBind = isBind === undefined ? true : !!isBind; var bindMethod = isBind ? 'addEventListener' : 'removeEventListener'; // loop through each event event.forEach( function( ev ) { el[bindMethod]( ev, this.event_handlers[fn], false ); }.bind( this ) ); }; /** * Emits events via EvEmitter and jQuery events * @param {Object} _this * @param {string} namespace - namespace of event * @param {string} type - name of event * @param {Event} event - original event * @param {Array} args - extra arguments */ utils.dispatchEvent = function( _this, namespace, type, event, args ) { // add Namespace for the event type += namespace ? '.' + namespace : ''; // add original event to arguments var emitArgs = event ? [event].concat( args ) : [args]; // trigger vanilla JS event _this.emitEvent( type, emitArgs ); }; /** * Function for applying a debounce effect to a function call. * @function * @param {Function} func - Function to be called at end of timeout. * @param {number} delay - Time in ms to delay the call of `func`. * @returns function */ utils.throttle = function( func, delay ) { var timestamp = null, limit = delay; return function() { var self = this, args = arguments, now = Date.now(); if ( !timestamp || now - timestamp >= limit ) { timestamp = now; func.apply( self, args ); } }; }; /** * Modulo calculation * @param {number} length * @param {number} index * @return {number} */ utils.modulo = function( length, index ) { return ( length + ( index % length ) ) % length; }; /** * Regex classname from string * @param {string} className * @return {RegExp} */ utils.classReg = function( className ) { return new RegExp( '(^|\\s+)' + className + '(\\s+|$)' ); }; /** * Check if element has class name * @param {Object} el * @param {string} className * @return {boolean} */ utils.hasClass = function( el, className ) { return !!el.className.match( this.classReg( className ) ); }; /** * Add a class name to an element * @param {Object} el * @param {string} className */ utils.addClass = function( el, className ) { if ( !this.hasClass( el, className ) ) { el.className += ( el.className ? ' ' : '' ) + className; } }; /** * Remove a class name to an element * @param {Object} el * @param {string} className */ utils.removeClass = function( el, className ) { if ( this.hasClass( el, className ) ) { el.className = el.className.replace( this.classReg( className ), ' ' ).replace( /\s+$/, '' ); } }; /** * Translate an element * @param {Object} el * @param {number} x * @param {number} y * @param {number} s */ utils.translate = function( el, x, y, s ) { var scale = s ? ' scale(' + s + ',' + s + ')' : ''; el.style[this.browser.trans] = ( this.browser.gpu ) ? 'translate3d(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px, 0)' + scale : 'translate(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px)' + scale; }; /* * Browser features detection */ utils.browser = { trans : utils.prop( 'transform' ), gpu : utils.prop( 'perspective' ) ? true : false }; return utils; })); /** * EvEmitter (Emit event) * Modified version of David Desandro (https://github.com/desandro/EventEmitter) * v1.0.0 */ ( function( root, factory ) { if ( typeof define === 'function' && define.amd ) { // AMD define( 'themeone-event/event', factory ); } else if ( typeof module === 'object' && module.exports ) { // Node, CommonJS-like module.exports = factory(); } else { // Browser globals (root is window) root.ThemeoneEvent = factory(); } }( typeof window !== 'undefined' ? window : this, function() { "use strict"; var EvEmitter = function() {}, proto = EvEmitter.prototype; /** * Bind on event * @param {string} eventName * @param {Object} listener * @return {Object} this */ proto.on = function( eventName, listener ) { if ( !eventName || !listener ) { return null; } // set events hash var events = this._events = this._events || {}; // set listeners array var listeners = events[eventName] = events[eventName] || []; // only add once if ( listeners.indexOf( listener ) === -1 ) { listeners.push( listener ); } return this; }; /** * Unbind event event * @param {string} eventName * @param {Object} listener * @return {Object} this */ proto.off = function( eventName, listener ) { var listeners = this._events && this._events[eventName]; if ( !listeners || !listeners.length ) { return null; } var index = listeners.indexOf( listener ); if ( index !== -1 ) { listeners.splice( index, 1 ); } return this; }; /** * Emit an event * @param {string} eventName * @param {Object} args * @return {Object} this */ proto.emitEvent = function( eventName, args ) { var listeners = this._events && this._events[eventName]; if ( !listeners || !listeners.length ) { return null; } var i = 0, listener = listeners[i]; args = args || []; // once stuff var onceListeners = this._onceEvents && this._onceEvents[eventName]; while ( listener ) { var isOnce = onceListeners && onceListeners[listener]; if ( isOnce ) { // remove before trigger to prevent recursion this.off( eventName, listener ); // unset once flag delete onceListeners[listener]; } // trigger listener listener.apply( this, args ); // get next listener i += isOnce ? 0 : 1; listener = listeners[i]; } return this; }; return EvEmitter; })); /** * Animate * RAF animations * v1.0.0 */ ( function( root, factory ) { if ( typeof define === 'function' && define.amd ) { // AMD define( 'themeone-animate/animate', ['themeone-utils/utils', 'themeone-event/event'], factory ); } else if ( typeof module === 'object' && module.exports ) { // Node, CommonJS-like module.exports = factory( require( 'themeone-utils' ), require( 'themeone-event' ) ); } else { // Browser globals (root is window) root.ThemeoneAnimate = factory( root.ThemeoneUtils, root.ThemeoneEvent ); } }( this, function( utils, EvEmitter ) { 'use strict'; /** * Animate * @param {Object} element * @param {Object} positions * @param {Object} friction * @param {Object} attraction */ var Animate = function( element, positions, friction, attraction ) { this.element = element; this.defaults = positions; this.forces = { friction : friction || 0.28, attraction : attraction || 0.028 }; this.resetAnimate(); }; var proto = Animate.prototype = Object.create( EvEmitter.prototype ); /** * Update animation on drag * @param {Object} obj */ proto.updateDrag = function( obj ) { this.move = true; this.drag = obj; }; /** * Release drag */ proto.releaseDrag = function() { this.move = false; }; /** * Animate to a specific position * @param {Object} obj */ proto.animateTo = function( obj ) { this.attraction = obj; }; /** * Start render animation */ proto.startAnimate = function() { this.move = true; this.settle = false; this.restingFrames = 0; if ( !this.RAF ) { this.animate(); } }; /** * Stop animation */ proto.stopAnimate = function() { this.move = false; this.restingFrames = 0; if ( this.RAF ) { cancelAnimationFrame( this.RAF ); this.RAF = false; } this.start = utils.cloneObject( this.position ); this.velocity = { x : 0, y : 0, s : 0 }; }; /** * Reset animation */ proto.resetAnimate = function() { this.stopAnimate(); this.settle = true; this.drag = utils.cloneObject( this.defaults ); this.start = utils.cloneObject( this.defaults ); this.resting = utils.cloneObject( this.defaults ); this.position = utils.cloneObject( this.defaults ); this.attraction = utils.cloneObject( this.defaults ); }; /** * Handle animation rendering */ proto.animate = function() { // animation loop function var loop = ( function() { if ( typeof this.position !== 'undefined' ) { // get clone previous values var previous = utils.cloneObject( this.position ); // set main forces this.applyDragForce(); this.applyAttractionForce(); // dispatch render event (before physic otherwise calculation will be wrong) utils.dispatchEvent( this, 'toanimate', 'render', this ); // apply physics this.integratePhysics(); this.getRestingPosition(); // render positions this.render( 100 ); // loop this.RAF = requestAnimationFrame( loop ); // cancel RAF if no animations this.checkSettle( previous ); } }).bind( this ); // start animation loop this.RAF = requestAnimationFrame( loop ); }; /** * Simulation physic velocity */ proto.integratePhysics = function() { for ( var k in this.position ) { if ( typeof this.position[k] !== 'undefined' ) { this.position[k] += this.velocity[k]; this.position[k] = ( k === 's' ) ? Math.max( 0.1, this.position[k] ) : this.position[k]; this.velocity[k] *= this.getFrictionFactor(); } } }; /** * Simulation physic friction */ proto.applyDragForce = function() { if ( this.move ) { for ( var k in this.drag ) { if ( typeof this.drag[k] !== 'undefined' ) { var dragVelocity = this.drag[k] - this.position[k]; var dragForce = dragVelocity - this.velocity[k]; this.applyForce( k, dragForce ); } } } }; /** * Simulation physic attraction */ proto.applyAttractionForce = function() { if ( !this.move ) { for ( var k in this.attraction ) { if ( typeof this.attraction[k] !== 'undefined' ) { var distance = this.attraction[k] - this.position[k]; var force = distance * this.forces.attraction; this.applyForce( k, force ); } } } }; /** * Calculate estimated resting position from physic */ proto.getRestingPosition = function() { for ( var k in this.position ) { if ( typeof this.position[k] !== 'undefined' ) { this.resting[k] = this.position[k] + this.velocity[k] / ( 1 - this.getFrictionFactor() ); } } }; /** * Apply an attraction force * @param {string} direction * @param {number} force */ proto.applyForce = function( direction, force ) { this.velocity[direction] += force; }; /** * Apply a friction factor * @return {number} */ proto.getFrictionFactor = function() { return 1 - this.forces.friction; }; /** * Round value to correctly calculate if animate is settle or not * @param {number} values * @param {number} round */ proto.roundValues = function( values, round ) { for ( var k in values ) { if ( typeof values[k] !== 'undefined' ) { round = k === 's' ? round * 100 : round; values[k] = Math.round( values[k] * round ) / round; } } }; /** * Check if the current animated object is settled * @param {Object} previous */ proto.checkSettle = function( previous ) { // keep track of frames where x hasn't moved if ( !this.move ) { var count = 0; for ( var k in this.position ) { if ( typeof this.position[k] !== 'undefined' ) { var round = k === 's' ? 10000 : 100; if ( Math.round( this.position[k] * round ) === Math.round( previous[k] * round ) ) { count++; if ( count === Object.keys( this.position ).length ) { this.restingFrames++; } } } } } // stop RAF animation if position didn't change during 3 frames (60fps) if ( this.restingFrames > 2 ) { this.stopAnimate(); this.render( this.position.s > 1 ? 10 : 1 ); this.settle = true; // dispatch settle only if moved if ( JSON.stringify( this.start ) !== JSON.stringify( this.position ) ) { utils.dispatchEvent( this, 'toanimate', 'settle', this ); } } }; /** * Render animation * @param {number} round */ proto.render = function( round ) { // round new position values this.roundValues( this.position, round ); // translate utils.translate( this.element, this.position.x, this.position.y, this.position.s ); }; return Animate; })); /** * ModuloBox * Core Plugin * v1.3.0 */ ( function( root, factory ) { if ( typeof define === 'function' && define.amd ) { // AMD define( ['themeone-utils/utils', 'themeone-event/event', 'themeone-animate/animate'], factory ); } else if ( typeof exports === 'object' && module.exports ) { // Node, CommonJS-like module.exports = factory( require( 'themeone-utils' ), require( 'themeone-event' ), require( 'themeone-animate' ) ); } else { // Browser globals (root is window) root.ModuloBox = factory( root.ThemeoneUtils, root.ThemeoneEvent, root.ThemeoneAnimate ); } }( this, function( utils, EvEmitter, Animate ) { 'use strict'; // Modulobox version var version = '1.3.0'; // Globally unique identifiers var GUID = 0; // Internal store of all plugin intances var instances = {}; // Internal cache var expando = 'mobx' + ( version + Math.random() ).replace( /\D/g, '' ); var cache = { uid : 0 }; // Default options var defaults = { // Setup mediaSelector : '.mobx', // Media class selector used to fetch media in document // Behaviour threshold : 5, // Dragging doesn't start until 5px moved (not taken into account when pinched with 2 fingers) attraction : { // Attracts the position of the slider to the selected cell. Higher value makes the slider move faster. Lower value makes it move slower. slider : 0.055, // From 0 to 1 slide : 0.018, // From 0 to 1 thumbs : 0.016 // From 0 to 1 }, friction : { // Friction slows the movement of slider. Higher value makes the slider feel stickier & less bouncy. Lower value makes the slider feel looser & more wobbly slider : 0.62, // From 0 to 1 slide : 0.18, // From 0 to 1 thumbs : 0.22 // From 0 to 1 }, rightToLeft : false, // Enable right to left layout loop : 3, // From how much items infinite loop start (0: no loop, 1: always loop, 2: loop if 2 items at least, etc...) preload : 1, // Number of media to preload. If 1 set, it will load the currently media opened and once loaded it'll preload the 2 closest media. value can be 1, 3 or 5 unload : false, // Allows to unload media which are out of the visible viewport to improve performance (EXPERIMENTAL - not work in Safari, can create issue on other browsers) timeToIdle : 4000, // Hide controls when an idle state exceed 4000ms (0 - always keep controls visible) history : false, // Enable/disable history in browser (deeplink) when opening/navigating/closing a gallery mouseWheel : true, // Enable/disable mouseWheel (up/down/space keys) to scroll page contextMenu : true, // Enable/disable contextmenu on right click (on image) scrollBar : true, // Show/hide browser scrollbar fadeIfSettle : false, // Show media only if the slider is settled (improves performance especially for iframe/html) // User Interface controls : ['close'], // 'zoom', 'play', 'fullScreen', 'download', 'share', 'close' prevNext : true, // Show/hide prev/next navigation buttons prevNextTouch : false, // Show/hide prev/next navigation buttons on small touch capable devices counterMessage : '[index] / [total]', // Message used in the item counter. If empty, no counter will be displayed caption : true, // Show/hide caption under media (globally) autoCaption : false, // Generate captions from alt and/or title attributes if data-title and/or data-desc missing captionSmallDevice : true, // Show/hide caption under media on small browser width like mobile devices thumbnails : true, // Enable/disable thumbnail thumbnailsNav : 'basic', // Navigation type for the thumbnail (basic or centered) thumbnailSizes : { // Set thumbnails size for different browser widths (unlimited) 1920 : { // browser width (px) width : 110, // thumbnail width (px) - 0 in width will hide thumbnails height : 80, // thumbnail height(px) - 0 in height will hide thumbnails gutter : 10 // gutter width (px) between thumbnails }, 1280 : { width : 90, height : 65, gutter : 10 }, 680 : { width : 70, height : 50, gutter : 8 }, 480 : { width : 60, height : 44, gutter : 5 } }, spacing : 0.1, // Space in percent between each slide. For example, 0.1 will render as a 10% of sliding viewport width smartResize : true, // Allow images to overflow on top bar and/or caption on small devices only if image can fill the full screen height overflow : false, // Allow images to overflow on top bar and/or caption (if enable, smart resize will be ignored) loadError : 'Sorry, an error occured while loading the content...', noContent : 'Sorry, no content was found!', // functions prevNextKey : true, // Press keyboard left/right to navigate scrollToNav : false, // Scroll with a mousewheel to navigate next/prev (disabled if scrollToZoom set to true) scrollSensitivity : 15, // Threshold in 'px' from when a methods is called (Useful to fine tune touchpad and free mousewheel sensitivity) zoomTo : 'auto', // Zoom factor applied on button click and double tap ('auto' will scale to the natural image size, accept number value (e.g.: 2, 3, 4, etc...) minZoom : 1.2, // Min zoom factor required to enable Zoom feature on click or pinch maxZoom : 4, // Max zoom factor allowed when pinched/scrolled or if the zoomTo auto value is superior to maxZoom doubleTapToZoom : true, // double Tap/click to zoom on image scrollToZoom : false, // Scroll with a mousewheel to zoom in/out pinchToZoom : true, // Pinch in with 2 fingers to zoom on image escapeToClose : true, // Esc keyboard key to close the lightbox scrollToClose : false, // Scroll with a mousewheel to close the lightbox (disabled if scrollToNav and/or scrollToZoom set to true) pinchToClose : true, // Pinch out with 2 fingers to close the lightbox dragToClose : true, // Drag vertically to close the lightbox tapToClose : true, // Tap/click outside the image/media to close the lightbox shareButtons : ['facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit'], // 'facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit', 'stumbleupon', 'tumblr', 'blogger', 'buffer', 'digg', 'evernote' shareText : 'Share on', // text displayed above social sharing buttons sharedUrl : 'deeplink', // url used to share the media ('page', deeplink', 'media') slideShowInterval : 4000, // Time interval, in milliseconds, between slide changes in slideshow mode slideShowAutoPlay : false, // Automatically start slideShow mode on opening slideShowAutoStop : false, // Stop slideshow when the last item is reached (only if slider loop) countTimer : true, // Show a circular countdown timer next to the counter message when autoplay (slideshow) countTimerBg : 'rgba(255,255,255,0.25)', // Background color of the circular timer stroke countTimerColor : 'rgba(255,255,255,0.75)', // Color of the circular timer stroke mediaelement : false, // Play HTML5 videos with mediaelement (jQuery library required & mediaelement.js script) videoRatio : 16 / 9, // Video aspect ratio videoMaxWidth : 1180, // Video max width allowed in a slide videoAutoPlay : false, // Autoplay video on opening videoThumbnail : false // Automatically fetches poster/thumbnails of iframe videos if missing }; /** * ModuloBox * @constructor * @param {Object} options */ var ModuloBox = function( options ) { // extend defaults with user-set options this.options = utils.extend( options, defaults ); // set main plugin variables this.setVar(); }; // Set plugin prototype (and add EvEmitter proto) var proto = ModuloBox.prototype = Object.create( EvEmitter.prototype ); /** * Initialize ModuloBox * Create main instance & DOM */ proto.init = function() { // prevent to initialize twice if ( this.GUID ) { return instances[this.GUID]; } // set instance this.GUID = ++GUID; instances[this.GUID] = this; this.createDOM(); this.setAnimation(); this.getGalleries(); this.openFromQuery(); }; /** * Set main plugin variable */ proto.setVar = function() { var win = window, doc = document, nav = navigator; // prefix used for class names this.pre = 'mobx'; // set main var this.gesture = {}; this.buttons = {}; this.slider = {}; this.slides = {}; this.cells = {}; this.states = {}; this.pointers = []; // set internal cache this.expando = expando; this.cache = cache; // drag events this.dragEvents = this.detectPointerEvents(); // browser detection this.browser = { touchDevice : ( 'ontouchstart' in win ) || ( nav.maxTouchPoints > 0 ) || ( nav.msMaxTouchPoints > 0 ), pushState : 'history' in win && 'pushState' in history, fullScreen : this.detectFullScreen(), mouseWheel : 'onwheel' in doc.createElement( 'div' ) ? 'wheel' : doc.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll' }; // add social & video stream support this.iframeVideo = this.iframeVideo(); this.socialMedia = this.socialMedia(); }; /** * Detect pointer type browser support * @return {Object} */ proto.detectPointerEvents = function() { var nav = navigator; // listen for W3C Pointer Events (IE11) if ( nav.pointerEnabled ) { return { start : ['pointerdown'], move : ['pointermove'], end : ['pointerup', 'pointercancel'] }; } // listen for IE10 Pointer Events if ( nav.msPointerEnabled ) { return { start : ['MSPointerDown'], move : ['MSPointerMove'], end : ['MSPointerUp', 'MSPointerCancel'] }; } // listen for both Mouse & Touch Events return { start : ['mousedown', 'touchstart'], move : ['mousemove', 'touchmove'], end : ['mouseup', 'mouseleave', 'touchend', 'touchcancel'] }; }; /** * Detect fullScreen browser support * Remove fullScreen button if not supported * @return {Object} */ proto.detectFullScreen = function() { var fullScreen = ['fullscreenEnabled', 'webkitFullscreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled']; for ( var i = 0, l = fullScreen.length; i < l; i++ ) { if ( document[fullScreen[i]] ) { return { element : ['fullscreenElement', 'webkitFullscreenElement', 'mozFullScreenElement', 'msFullscreenElement'][i], request : ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen'][i], change : ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'][i], exit : ['exitFullscreen', 'webkitExitFullscreen', 'mozCancelFullScreen', 'msExitFullscreen'][i] }; } } var controls = this.options.controls, index = controls.indexOf( 'fullScreen' ); // if no fullscreen support remove fullscreen button if exists if ( index > -1 ) { controls.splice( index, 1 ); } return null; }; /** * Iframe stream video * HTML5 window post message API * [ID] => iframe video ID * @return {Object} */ proto.iframeVideo = function() { return { youtube : { reg : /(?:www\.)?youtu\.?be(?:\.com)?\/?.*?(?:watch|embed)?(?:.*v=|v\/|watch%3Fv%3D|\/)([\w\-_]+)&?/i, url : 'https://www.youtube.com/embed/[ID]?enablejsapi=1&rel=0&autoplay=1', share : 'https://www.youtube.com/watch?v=[ID]', poster : 'https://img.youtube.com/vi/[ID]/maxresdefault.jpg', thumb : 'https://img.youtube.com/vi/[ID]/default.jpg', play : { event : 'command', func : 'playVideo' }, pause : { event : 'command', func : 'pauseVideo' } }, vimeo : { reg : /(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/)?(\d+)(?:[a-zA-Z0-9_\-]+)?/i, url : 'https://player.vimeo.com/video/[ID]?autoplay=1&api=1', share : 'https://vimeo.com/[ID]', poster : 'https://vimeo.com/api/v2/video/[ID].json', play : { event : 'command', method : 'play' }, pause : { event : 'command', method : 'pause' } }, dailymotion : { reg : /(?:www\.)?(?:dailymotion\.com(?:\/embed)(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+#video=(?:[a-zA-Z0-9_\-]+))?/i, url : 'https://dailymotion.com/embed/video/[ID]?autoplay=1&api=postMessage', share : 'https://www.dailymotion.com/video/[ID]', poster : 'https://www.dailymotion.com/thumbnail/video/[ID]', thumb : 'https://www.dailymotion.com/thumbnail/video/[ID]', play : 'play', pause : 'pause' }, wistia : { reg : /(?:www\.)?(?:wistia\.(?:com|net)|wi\.st)\/(?:(?:m|medias|projects)|embed\/(?:iframe|playlists))\/([a-zA-Z0-9_\-]+)/i, url : 'https://fast.wistia.net/embed/iframe/[ID]?version=3&enablejsapi=1&html5=1&autoplay=1', share : 'https://fast.wistia.net/embed/iframe/[ID]', poster : 'https://fast.wistia.com/oembed?url=https://home.wistia.com/medias/[ID].json', play : { event : 'cmd', method : 'play' }, pause : { event : 'cmd', method : 'pause' } } }; }; /** * Social media url * [url] => location deeplink or media url * [text] => text from title or null * [image] => image or video poster or null * @return {Object} */ proto.socialMedia = function() { return { facebook : 'https://www.facebook.com/sharer/sharer.php?u=[url]', googleplus : 'https://plus.google.com/share?url=[url]', twitter : 'https://twitter.com/intent/tweet?text=[text]&url=[url]', pinterest : 'https://www.pinterest.com/pin/create/button/?url=[url]&media=[image]&description=[text]', linkedin : 'https://www.linkedin.com/shareArticle?url=[url]&mini=true&title=[text]', reddit : 'https://www.reddit.com/submit?url=[url]&title=[text]', stumbleupon : 'https://www.stumbleupon.com/badge?url=[url]&title=[text]', tumblr : 'https://www.tumblr.com/share?v=3&u=[url]&t=[text]', blogger : 'https://www.blogger.com/blog_this.pyra?t&u=[url]&n=[text]', buffer : 'https://bufferapp.com/add?url=[url]title=[text]', digg : 'https://digg.com/submit?url=[url]&title=[text]', evernote : 'https://www.evernote.com/clip.action?url=[url]&title=[text]' }; }; // ----- Create & append Lightbox in document ----- // /** * Create DOM for the lightbox */ proto.createDOM = function() { this.DOM = {}; var elements = [ 'holder', 'overlay', 'slider', 'item', 'item-inner', 'ui', 'top-bar', 'bottom-bar', 'share-tooltip', 'counter', 'caption', 'caption-inner', 'thumbs-holder', 'thumbs-inner' ]; for ( var i = 0; i < elements.length; i++ ) { this.DOM[utils.camelize( elements[i] )] = utils.createEl( 'div', this.pre + '-' + elements[i] ); } this.appendDOM( this.DOM ); }; /** * Append lightbox DOM to body * @param {{holder, item, itemInner, overlay, slider, ui, topBar}} dom */ proto.appendDOM = function( dom ) { var opt = this.options; // append main containers dom.holder.appendChild( dom.overlay ); dom.holder.appendChild( dom.slider ); dom.holder.appendChild( dom.ui ); // append slides & cells to slider for ( var i = 0; i < 5; i++ ) { var slide = dom.item.cloneNode( true ); slide.appendChild( dom.itemInner.cloneNode( true ) ); dom.slider.appendChild( slide ); // manually assign dom to slides object this.slides[i] = slide; } // set slides length this.slides.length = dom.slider.children.length; // create UI elements this.createUI( dom, opt ); // add lightbox attribute dom.holder.setAttribute( 'tabindex', -1 ); dom.holder.setAttribute( 'aria-hidden', true ); // add plugin version in comment this.DOM.comment = document.createComment( ' ModuloBox (v' + version + ') by Themeone ' ); document.body.appendChild( this.DOM.comment ); // Dispatch event before appending lightbox DOM (to add custom markup) utils.dispatchEvent( this, 'modulobox', 'beforeAppendDOM', dom ); // append lightbox to body document.body.appendChild( dom.holder ); // store top bar height for later dom.topBar.height = dom.topBar.clientHeight; }; /** * Create UI elements * @param {{holder, ui, topBar, bottomBar, thumbsHolder, thumbsInner, counter, shareTooltip, caption, captionInner}} dom * @param {Object} opt */ proto.createUI = function( dom, opt ) { var shareIndex = opt.controls.indexOf( 'share' ); // append share tooltip if ( shareIndex > -1 ) { var buttons = opt.shareButtons, i = buttons.length; // check if each social button exists while ( i-- ) { if ( !this.socialMedia.hasOwnProperty( buttons[i] ) ) { buttons.splice( i, 1 ); } } // if there are social button(s) registered if ( buttons.length ) { dom.ui.appendChild( dom.shareTooltip ); // add share text if ( opt.shareText ) { dom.shareTooltip.appendChild( utils.createEl( 'span' ) ).textContent = opt.shareText; } this.createButtons( buttons, dom.shareTooltip, 'shareOn' ); // remove share button if no social media } else { opt.controls.splice( shareIndex, 1 ); } } // append top bar & buttons if ( opt.controls.length || opt.counterMessage ) { var slideShow = opt.controls.indexOf( 'play' ); dom.ui.appendChild( dom.topBar ); // append counter message if ( opt.counterMessage ) { dom.topBar.appendChild( dom.counter ); } // remove play button if slideShow interval <= 0 if ( opt.slideShowInterval < 1 && slideShow > -1 ) { opt.controls.splice( slideShow, 1 ); } // append countdown timer if ( opt.countTimer && slideShow > -1 ) { var timer = this.DOM.timer = utils.createEl( 'canvas', this.pre + '-timer' ); timer.setAttribute( 'width', 48 ); timer.setAttribute( 'height', 48 ); dom.topBar.appendChild( timer ); } // append crontrol buttons if ( opt.controls.length ) { // clone array (prevent options to be reversed if destroyed and initialized again) var controls = opt.controls.slice(); this.createButtons( controls.reverse(), dom.topBar ); } } // append bottom bar if ( opt.caption || opt.thumbnails ) { dom.ui.appendChild( dom.bottomBar ); // append caption if ( opt.caption ) { dom.bottomBar.appendChild( dom.caption ) .appendChild( dom.captionInner ); } // append thumbnails if ( opt.thumbnails ) { dom.bottomBar.appendChild( dom.thumbsHolder ) .appendChild( dom.thumbsInner ); } } // append prev/next buttons if ( opt.prevNext ) { this.createButtons( ['prev', 'next'], dom.ui ); } }; /** * Create Buttons elements * @param {Array} buttons * @param {Object} dom * @param {string} event (event name attached to a button) */ proto.createButtons = function( buttons, dom, event ) { var length = buttons.length; for ( var i = 0; i < length; i++ ) { var type = buttons[i]; // create and append button this.buttons[type] = utils.createEl( 'BUTTON', this.pre + '-' + type.toLowerCase() ); dom.appendChild( this.buttons[type] ); // attach event if button have a corresponding prototype event if ( ( type && typeof this[type] === 'function' ) || event ) { this.buttons[type].event = event ? event : type; this.buttons[type].action = type; if ( event === 'shareOn' ) { this.buttons[type].setAttribute( 'title', type.charAt( 0 ).toUpperCase() + type.slice( 1 ) ); } } } }; /** * Get media galleries (img, video, iframe, HTML) */ proto.getGalleries = function() { // prepare galleries // reset if executed again this.galleries = {}; // prepare querySelector var selectors = this.options.mediaSelector, sources = ''; // if no selector set (prevent to trigger unnecessary errors) if ( !selectors ) { return false; } // setup sources try { sources = document.querySelectorAll( selectors ); } catch (error) { utils.error( 'Your current mediaSelector is not a valid selector: "' + selectors + '"' ); } for ( var i = 0, l = sources.length; i < l; i++ ) { var source = sources[i], media = {}; // get original image url/src depending of the tagName media.src = source.tagName === 'A' ? source.getAttribute( 'href' ) : null; media.src = source.tagName === 'IMG' ? source.currentSrc || source.src : media.src; // if mediaSelector have data-src attr, take this one instead of src or href attributes media.src = source.getAttribute( 'data-src' ) || media.src; if ( media.src ) { this.getMediaAtts( source, media ); this.setMediaType( media ); // If the media have a known type if ( media.type ) { this.getMediaThumb( source, media ); this.getVideoThumb( media ); this.getMediaCaption( source, media ); // format caption this.setMediaCaption( media ); // get the gallery name & features var gallery = this.setGalleryName( source ); this.setGalleryFeatures( gallery, media ); // assign media index media.index = gallery.length; // push media data in gallery gallery.push( media ); // attach click event to media this.setMediaEvent( source, gallery.name, media.index ); } } } // dispatch updateGalleries event utils.dispatchEvent( this, 'modulobox', 'updateGalleries', this.galleries ); }; /** * Add media in a gallery * @param {number|string} name (gallery name) * @param {Object} media (media collection) */ proto.addMedia = function( name, media ) { if ( !media || typeof media !== 'object' ) { utils.error( 'No media was found to addMedia() in a gallery' ); return false; } // set gallery name = name === '' ? 1 : name; var gallery = this.galleries[name]; gallery = !gallery ? ( this.galleries[name] = [] ) : gallery; gallery.name = name; var length = media.length; for ( var i = 0; i < length; i++ ) { var item = utils.cloneObject( media[i] ); if ( item.src ) { this.setMediaType( item ); this.getVideoThumb( item ); this.setMediaCaption( item ); this.setGalleryFeatures( gallery, item ); // assign media index item.index = gallery.length; // push media data in gallery gallery.push( item ); } } }; /** * Set media type depending of its content * Image/video types only, iframe & HTML must be set manually * @param {Object} media */ proto.setMediaType = function( media ) { // if the media type is already set and valid if ( ['image', 'video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) { return; } // reset media type in case not valid media.type = null; // get source var source = media.src ? media.src : null; /* jslint bitwise: true */ // get extension without query string var extension = ( source.split( /[?#]/ )[0] || source ).substr( ( ~-source.lastIndexOf( '.' ) >>> 0 ) + 2 ); /* jslint bitwise: false */ // detect image if ( /(jpg|jpeg|png|bmp|gif|tif|tiff|jfi|jfif|exif|svg)/i.test( extension ) || ['external.xx.fbcdn', 'drscdn.500px.org'].indexOf( source ) > -1 ) { media.type = 'image'; media.src = this.getSrc( source ); // detect HTML5 video } else if ( /(mp4|webm|ogv)/i.test( extension ) ) { media.type = 'video'; media.format = 'html5'; // detect iframe video } else { var stream = this.iframeVideo; for ( var type in stream ) { if ( stream.hasOwnProperty( type ) ) { var regs = source.match( stream[type].reg ); if ( regs && regs[1] ) { var object = stream[type]; media.type = 'video'; media.format = type; media.share = object.share.replace( '[ID]', regs[1] ); media.src = object.url.replace( '[ID]', regs[1] ); media.pause = object.pause; media.play = object.play; if ( this.options.videoThumbnail ) { media.poster = !media.poster && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.poster; media.thumb = !media.thumb && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.thumb; } break; } } } } }; /** * Get image srcset custom * @param {string} source * @return {string} image url */ proto.getSrc = function( source ) { var srcset = ( source || '' ).split( /,/ ), length = srcset.length, width = 0; if ( length <= 1 ) { return source; } for ( var i = 0; i < length; i++ ) { var parts = srcset[i].replace(/\s+/g, ' ').trim().split( / / ), value = parseFloat( parts[1] ) || 0, unit = parts[1] ? parts[1].slice(-1) : null; if ( ( unit === 'w' && screen.width >= value && value > width ) || !value || i === 0 ) { width = value; source = parts[0]; } } return source; }; /** * Get media attributes * @param {Object} source * @param {Object} media */ proto.getMediaAtts = function( source, media ) { var auto = this.options.autoCaption, data = this.getAttr( source ), img = source.firstElementChild; img = source.tagName !== 'IMG' && img && img.tagName === 'IMG' ? img : source; media.type = !media.type ? data.type || source.getAttribute( 'data-type' ) : media.type; media.title = data.title || source.getAttribute( 'data-title' ) || ( auto ? img.title : null ); media.desc = data.desc || source.getAttribute( 'data-desc' ) || ( auto ? img.alt : null ); media.thumb = data.thumb || source.getAttribute( 'data-thumb' ); media.poster = this.getSrc( data.poster || source.getAttribute( 'data-poster' ) ); media.width = data.width || source.getAttribute( 'data-width' ); media.height = data.height || source.getAttribute( 'data-height' ); // prevent duplicate content if autoCaption enabled if ( media.title === media.desc ) { media.desc = null; } }; /** * Get media thumbnail * @param {Object} source * @param {Object} media */ proto.getMediaThumb = function( source, media ) { var thumbnail = source.getElementsByTagName( 'img' ); if ( !media.thumb && thumbnail[0] ) { media.thumb = thumbnail[0].src; } }; /** * Get iframe video thumbnail * @param {Object} media */ proto.getVideoThumb = function( media ) { if ( !this.options.videoThumbnail || media.type !== 'video' || media.format === 'html5' ) { return; } var hasPoster = media.poster && media.poster.indexOf( '.json' ) > -1, hasThumb = media.thumb && media.thumb.indexOf( '.json' ) > -1; if ( hasPoster || hasThumb ) { var uri = hasPoster ? media.poster : media.thumb, xhr = new XMLHttpRequest(); xhr.onload = function( event ) { /** @type {?|{thumbnail_large: string, thumbnail_url: string, thumbnail_small: string}} */ var response = event.target.responseText; response = JSON.parse( response ); response = response.hasOwnProperty( 0 ) ? response[0] : response; if ( response ) { media.poster = response.thumbnail_large || response.thumbnail_url; // replace background image if image DOM already built if ( media.dom ) { media.dom.style.backgroundImage = 'url("' + media.poster + '")'; } if ( hasThumb ) { var thumb = response.thumbnail_small || response.thumbnail_url; // replace background image if thumbnail already built if ( typeof media.thumb === 'object' ) { media.thumb.style.backgroundImage = 'url("' + thumb + '")'; } else { media.thumb = thumb; } } } }.bind( this ); xhr.open( 'GET', encodeURI( uri ), true ); setTimeout( function() { xhr.send(); }, 0 ); } }; /** * Get media caption * @param {Object} source * @param {Object} media */ proto.getMediaCaption = function( source, media ) { var next = source.nextElementSibling; // if gallery follow schema.org markup if ( next && next.tagName === 'FIGCAPTION' ) { var caption = next.innerHTML; if ( ! media.title ) { media.title = caption; } else if ( ! media.desc ) { media.desc = caption; } } }; /** * Build media caption * @param {Object} media */ proto.setMediaCaption = function( media ) { media.title = media.title ? '
' + media.title.trim() + '
' : ''; media.desc = media.desc ? '
' + media.desc.trim() + '
' : ''; media.caption = media.title + media.desc; }; /** * Get gallery name from DOM * @param {Object} source */ proto.getGalleryName = function( source ) { var parent = source, node = 0; while ( parent && node < 2 ) { parent = parent.parentNode; if ( parent && parent.tagName === 'FIGURE' && parent.parentNode ) { return parent.parentNode.getAttribute( 'id' ); } node++; } }; /** * Set gallery name * @param {Object} source */ proto.setGalleryName = function( source ) { var data = this.getAttr( source ); // get name var name = data.rel || source.getAttribute( 'data-rel' ); name = !name ? this.getGalleryName( source ) : name; name = !name ? Object.keys( this.galleries ).length + 1 : name; // set gallery var gallery = this.galleries[name]; gallery = !gallery ? ( this.galleries[name] = [] ) : gallery; gallery.name = name; return gallery; }; /** * Set gallery features * @param {Object} gallery * @param {Object} media */ proto.setGalleryFeatures = function( gallery, media ) { if ( !gallery.zoom && media.type === 'image' ) { gallery.zoom = true; } if ( !gallery.download && ( media.type === 'image' || media.format === 'html5' ) ) { gallery.download = true; } }; /** * Attach click event to Media DOM * @param {Object} source * @param {number} name (of gallery) * @param {number} index */ proto.setMediaEvent = function( source, name, index ) { // if media already have a click event attached if ( source.mobxListener ) { source.removeEventListener( 'click', source.mobxListener, false ); } // set listener function to remove later if gallery updated source.mobxListener = this.open.bind( this, name, index ); source.addEventListener( 'click', source.mobxListener, false ); }; /** * Open lightbox from a given Gallery name and media index * @param {string} name * @param {number} index * @param {Object} event */ proto.open = function( name, index, event ) { // prevent default action and to bubbling up the DOM tree if ( event ) { event.preventDefault(); event.stopPropagation(); } // if the instance was destroyed or does not exist if ( !this.GUID ) { return false; } // check if gallery exists if ( !this.galleries.hasOwnProperty( name ) ) { utils.error( 'This gallery name : "' + name + '", does not exist!' ); return false; } // check if gallery have media if ( !this.galleries[name].length ) { utils.error( 'Sorry, no media was found for the current gallery.' ); return false; } // check if current media index exists (mainly to open from query param) if ( !this.galleries[name][index] ) { utils.error( 'Sorry, no media was found for the current media index: ' + index ); return false; } // dispatch beforeOpen Event utils.dispatchEvent( this, 'modulobox', 'beforeOpen', name, index ); // reset slide index this.slides.index = index; // set gallery this.gallery = this.galleries[name]; this.gallery.name = name; this.gallery.index = index; this.gallery.loaded = false; // prepare lightbox this.removeContent(); this.wrapAround(); this.hideScrollBar(); this.setSlider(); this.setThumbs(); this.setCaption(); this.setMedia( this.options.preload ); this.updateMediaInfo(); this.replaceState(); this.setControls(); this.bindEvents( true ); this.show(); // autoplay video on opening if ( this.options.videoAutoPlay ) { this.appendVideo(); } // autoplay slideshow on opening (only if video autoplay not set or not a video item opened) if ( this.options.slideShowAutoPlay && this.options.controls.indexOf( 'play' ) > -1 && ( ! this.options.videoAutoPlay || this.galleries[name][index].type !== 'video' ) ) { this.startSlideShow(); } // set lightbox states this.states.zoom = false; this.states.open = true; }; /** * Open lightbox from query string in url */ proto.openFromQuery = function() { var query = this.getQueryString( window.location.search ); if ( query.hasOwnProperty( 'guid' ) && query.hasOwnProperty( 'mid' ) ) { var open = this.open( decodeURIComponent( query.guid ), decodeURIComponent( query.mid ) - 1 ); // remove query string if failed to open if ( open === false ) { this.replaceState( true ); } } }; /** * Reveal lightbox */ proto.show = function() { var holder = this.DOM.holder, method = this.options.rightToLeft ? 'add' : 'remove'; holder.setAttribute( 'aria-hidden', false ); utils.removeClass( holder, this.pre + '-idle' ); utils.removeClass( holder, this.pre + '-panzoom' ); utils.removeClass( holder, this.pre + '-will-close' ); utils[method + 'Class']( holder, this.pre + '-rtl' ); utils.addClass( holder, this.pre + '-open' ); }; /** * Close lightbox */ proto.close = function( event ) { // Prevent click/touch propagation if ( event ) { event.preventDefault(); } var holder = this.DOM.holder, gallery = this.gallery, index = gallery ? gallery.index : 'undefined', name = gallery ? gallery.name : 'undefined'; // dispatch beforeClose Event utils.dispatchEvent( this, 'modulobox', 'beforeClose', name, index ); // exist from fullscreen if ( this.states.fullScreen ) { this.exitFullScreen(); utils.removeClass( holder, this.pre + '-fullscreen' ); } // stop all actions this.share(); this.stopSlideShow(); this.pauseVideo(); this.bindEvents( false ); this.replaceState( true ); this.hideScrollBar(); // hide lightbox holder.setAttribute( 'aria-hidden', true ); utils.removeClass( holder, this.pre + '-open' ); this.states.open = false; }; /** * Set controls and buttons states */ proto.setControls = function() { // handle play/prev/next buttons var gallery = this.gallery, options = this.options, buttons = this.buttons; // hide counter message if one media if ( this.DOM.counter ) { this.DOM.counter.style.display = ( gallery.initialLength > 1 ) ? '' : 'none'; } // hide play button if one media if ( options.controls.indexOf( 'play' ) > -1 ) { buttons.play.style.display = ( gallery.initialLength > 1 ) ? '' : 'none'; } // hide zoom button if one media and not zoomable if ( options.controls.indexOf( 'zoom' ) > -1 ) { buttons.zoom.style.display = !gallery.zoom ? 'none' : ''; } // hide download button if one media and not Downloadable if ( options.controls.indexOf( 'download' ) > -1 ) { buttons.download.style.display = !gallery.download ? 'none' : ''; } // hide/show prev & next buttons this.setPrevNextButtons(); }; /** * Set responsive Prev/Next buttons */ proto.setPrevNextButtons = function() { if ( this.options.prevNext ) { var hide = this.slider.width < 680 && this.browser.touchDevice && !this.options.prevNextTouch; this.buttons.prev.style.display = this.buttons.next.style.display = ( this.gallery.length > 1 && !hide ) ? '' : 'none'; } }; /** * Set caption display depending of the screen size */ proto.setCaption = function() { this.states.caption = !( !this.options.captionSmallDevice && ( this.slider.width <= 480 || this.slider.height <= 480 ) ); this.DOM.caption.style.display = this.states.caption ? '' : 'none'; }; /** * Set buttons states */ proto.hideScrollBar = function() { if ( !this.options.scrollBar ) { var open = this.states.open, scrollBar = open === 'undefined' || !open ? true : false; document.body.style.overflow = document.documentElement.style.overflow = scrollBar ? 'hidden' : ''; } }; // ----- Handle events ----- // /** * Bind/unbind all events attached to the lightbox * @param {boolean} bind */ proto.bindEvents = function( bind ) { var win = window, doc = document, opt = this.options, holder = this.DOM.holder, buttons = this.buttons, scrollMethod; // handle buttons for ( var type in buttons ) { if ( buttons.hasOwnProperty( type ) ) { var DOM = ( type !== 'share' ) ? buttons[type] : win; utils.handleEvents( this, DOM, ['click', 'touchend'], buttons[type].event, bind ); } } // touch events utils.handleEvents( this, holder, this.dragEvents.start, 'touchStart', bind ); // keyboard event utils.handleEvents( this, win, ['keydown'], 'keyDown', bind ); // window resize event utils.handleEvents( this, win, ['resize', 'orientationchange'], 'resize', bind ); // transition end event utils.handleEvents( this, holder, ['transitionend', 'webkitTransitionEnd', 'oTransitionEnd', 'otransitionend', 'MSTransitionEnd'], 'opened' ); // disable double tap to zoom utils.handleEvents( this, holder, ['touchend'], 'disableZoom', bind ); // handle fullscreen if ( this.browser.fullScreen ) { utils.handleEvents( this, doc, [this.browser.fullScreen.change], 'toggleFullScreen', bind ); } // handle history navigation (replaceState) if ( opt.history ) { utils.handleEvents( this, win, ['mouseout'], 'mouseOut', bind ); } // handle idle state if ( opt.timeToIdle > 0 ) { utils.handleEvents( this, holder, ['mousemove'], 'mouseMove', bind ); } // prevent right click if ( !opt.contextMenu ) { utils.handleEvents( this, holder, ['contextmenu'], 'contextMenu', bind ); } // disable scroll if ( !opt.mouseWheel ) { this.disableScroll( bind ); } // scroll to zoom if ( opt.scrollToZoom ) { scrollMethod = 'scrollToZoom'; } // scroll to navigate else if ( opt.scrollToNav ) { scrollMethod = 'scrollToNav'; } // scroll to close else if ( opt.scrollToClose ) { scrollMethod = 'scrollToClose'; } // handle scroll method if ( scrollMethod ) { utils.handleEvents( this, holder, [this.browser.mouseWheel], scrollMethod, bind ); } }; /** * Detect animation transition end * Set open/close state * Unload/remove media only when the lightbox is closed and hidden (transitionend) * @param {Object} event */ proto.opened = function( event ) { if ( event.propertyName === 'visibility' && event.target === this.DOM.holder ) { var name = this.gallery.name, index = this.gallery.index; if ( !this.states.open ) { // remove media in slide and thumbnails this.removeContent(); // dispatch afterClose Event utils.dispatchEvent( this, 'modulobox', 'afterClose', name, index ); } else { // dispatch afterOpen Event utils.dispatchEvent( this, 'modulobox', 'afterOpen', name, index ); } } }; /** * Detect when mouse leave browser window * @param {Object} event */ proto.mouseOut = function( event ) { var e = event ? event : window.event, from = e.relatedTarget || e.toElement; if ( !from || from.nodeName === 'HTML' ) { this.replaceState(); } }; /** * Detect idle state from mousemove */ proto.mouseMove = function() { var holder = this.DOM.holder, idleClass = this.pre + '-idle'; clearTimeout( this.states.idle ); this.states.idle = setTimeout( function() { // prevent to hide top bar if tooltip opened if ( !utils.hasClass( holder, this.pre + '-open-tooltip' ) ) { utils.addClass( holder, idleClass ); } }.bind( this ), this.options.timeToIdle ); utils.removeClass( holder, idleClass ); }; /** * Disable contextmenu (right click) on image * @param {Object} event */ proto.contextMenu = function( event ) { var target = event.target, tagName = target.tagName, className = target.className; if ( tagName === 'IMG' || tagName === 'VIDEO' || className.indexOf( this.pre + '-video' ) > -1 || className.indexOf( this.pre + '-thumb-bg' ) > -1 || className === this.pre + '-thumb' ) { event.preventDefault(); } }; /** * Disable scroll * @param {boolean} bind */ proto.disableScroll = function( bind ) { var doc = document, win = window; var prevent = function( e ) { if ( !this.isEl( e ) ) { return; } e = e || win.event; if ( e.preventDefault ) { e.preventDefault(); e.returnValue = false; return false; } }; win.onwheel = win.ontouchmove = win.onmousewheel = doc.onmousewheel = doc.onmousewheel = bind ? prevent.bind( this ) : null; }; /** * Scroll to zoom * @param {Object} event */ proto.scrollToZoom = function( event ) { if ( !this.isEl( event ) ) { return; } var data = this.normalizeWheel( event ); if ( data && data.deltaY ) { var cell = this.getCell(), scale = cell.attraction.s || cell.position.s; scale = Math.min( this.options.maxZoom, Math.max( 1, scale - Math.abs( data.deltaY ) / data.deltaY ) ); this.stopSlideShow(); this.zoomTo( event.clientX, event.clientY, Math.round( scale * 10 ) / 10 ); } }; /** * Scroll to navigate (next/prev slide) * @param {Object} event */ proto.scrollToNav = function( event ) { if ( !this.isEl( event ) ) { return; } var data = this.normalizeWheel( event ); if ( data && data.delta ) { this[data.delta * this.isRTL() < 0 ? 'prev' : 'next'](); } }; /** * Scroll to close * @param {Object} event */ proto.scrollToClose = function( event ) { if ( !this.isEl( event ) ) { return; } event.preventDefault(); this.close(); }; /** * Disable double tap to zoom on touch devices * @param {Object} event */ proto.disableZoom = function( event ) { var node = event.target; while ( node ) { // Prevent issue on Android with native HTML5 video controls // If preventDefault on Android devices, video controls will never be shown on tap // Keep click on link/input for HTML content if ( ['VIDEO', 'INPUT', 'A'].indexOf( node.tagName ) > -1 ) { return; } node = node.parentElement; } event.preventDefault(); }; /** * Resize lightbox */ proto.resize = function( event ) { // Set new top bar height (in case icons are replaced) this.DOM.topBar.height = this.DOM.topBar.clientHeight; this.share(); this.setSlider(); this.setThumbsPosition(); this.setCaption(); this.resizeMedia(); this.updateMediaInfo(); this.setPrevNextButtons(); // reset current slide zoom this.states.zoom = false; utils.removeClass( this.DOM.holder, this.pre + '-panzoom' ); // dispatch resize Event utils.dispatchEvent( this, 'modulobox', 'resize', event ); }; /** * Resize media on window resize */ proto.resizeMedia = function() { var slides = this.slides; for ( var i = 0; i < slides.length; i++ ) { if ( !this.gallery ) { break; } var media = this.gallery[slides[i].media]; if ( media && ( ( media.dom && media.dom.loaded ) || ( media.dom && ['video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) ) ) { this.setMediaSize( media, slides[i] ); } } }; // ----- main plugin helper functions ----- // /** * Check if current element is part of Modulobox UI * @param {Object} event * @return {boolean} */ proto.isEl = function( event ) { var name = event.target.className; name = typeof name === 'string' ? name : name.baseVal; return name.indexOf( this.pre ) > -1; }; /** * Check if current media is zoomable * @return {boolean} */ proto.isZoomable = function() { var media = this.getMedia(), zoom = false; if ( media.type === 'image' && media.dom && media.dom.size && media.dom.size.scale > 1 ) { zoom = true; } this.DOM.holder.setAttribute( 'data-zoom', zoom ); return zoom; }; /** * Check if current media is downloadable * @return {boolean} */ proto.isDownloadable = function() { var media = this.getMedia(), download = true; if ( media.type !== 'image' && media.format !== 'html5' ) { download = false; } this.DOM.holder.setAttribute( 'data-download', download ); return download; }; /** * Check if RTL layout is enabled * @return {number} */ proto.isRTL = function() { return this.options.rightToLeft ? - 1 : 1; }; /** * Add attribute(s) in cache * @param {Object} el * @param {Object} attrs */ proto.addAttr = function( el, attrs ) { var cacheID; if ( typeof el[this.expando] === 'undefined' ) { cacheID = this.cache.uid++; el[this.expando] = cacheID; this.cache[cacheID] = {}; } else { cacheID = el[this.expando]; } for ( var attr in attrs ) { if ( attrs.hasOwnProperty( attr ) ) { this.cache[cacheID][attr] = attrs[attr]; } } }; /** * Get attribute(s) in cache * @param {Object} el * @return {Object} */ proto.getAttr = function( el ) { return this.cache[el[this.expando]] || {}; }; /** * Get thumbnail height * @return {number} */ proto.getThumbHeight = function() { var thumb = this.thumbs; return thumb.height > 0 && thumb.width > 0 ? thumb.height + Math.min( 10, thumb.gutter ) * 2 : 0; }; /** * Get current media in opened gallery * @return {Object} */ proto.getMedia = function() { var gallery = this.gallery; return gallery ? gallery[gallery.index] : null; }; /** * Get current cell in slide * @return {Object} */ proto.getCell = function() { var slides = this.slides, index = utils.modulo( slides.length, slides.index ); return this.cells[index]; }; /** * Remove/reset media in slide and remove thumbnails */ proto.removeContent = function() { // remove media in each slide for ( var i = 0; i < this.slides.length; i++ ) { var slide = this.slides[i]; // clean slide content this.unloadMedia( slide ); this.removeMedia( slide ); // reset index and media index previously set slide.index = slide.media = null; } // remove all thumbnails this.removeMedia( this.DOM.thumbsHolder ); }; /** * Get query string paramaters from url * @param {string} search * @return {Object} */ proto.getQueryString = function( search ) { var params = {}; search.substr( 1 ).split( '&' ).forEach( function( param ) { param = param.split( '=' ); params[decodeURIComponent( param[0] )] = param.length > 1 ? decodeURIComponent( param[1] ) : ''; }); return params; }; /** * Replace query parameter in url * @param {Object} key * @return {string} */ proto.setQueryString = function( key ) { var search = window.location.search, query = this.getQueryString( search ); // decode URI part search = decodeURI( search ); for ( var prop in key ) { if ( key.hasOwnProperty( prop ) ) { // encode string component // value to replace in URI var replace = encodeURIComponent( key[prop] ); if ( query.hasOwnProperty( prop ) ) { // already decoded from getQueryString // value to search from decoded search URI var value = query[prop]; if ( !replace ) { search = search.replace( '&' + prop + '=' + value, '' ); search = search.replace( prop + '=' + value, '' ); } else { search = search.replace( prop + '=' + value, prop + '=' + replace ); } } else { if ( replace ) { search = search + ( !search ? '?' : '&' ) + prop + '=' + replace; } else { search = search.replace( prop + '=', '' ); } } } } // create base url var base = [location.protocol, '//', location.host, location.pathname].join( '' ); // check if search query is empty or not search = !search.substr( 1 ) ? search.substr( 1 ) : search; // encode complete URI return encodeURI( base + search ); }; /** * Replace url state with HTML5 API replaceState method * @param {boolean} remove */ proto.replaceState = function( remove ) { if ( ( this.options.history || remove ) && this.browser.pushState && !this.states.push ) { var prevData = window.history.state, data = { guid : !remove ? this.gallery.name : '', mid : !remove ? utils.modulo( this.gallery.initialLength, this.gallery.index ) + 1 : '' }; if ( !prevData || prevData.mid !== data.mid ) { var search = this.setQueryString( data ); try { window.history.replaceState( data, '', search ); } catch (error) { // disable history this.options.history = false; // display error message in console utils.error( 'SecurityError: A history state object with origin \'null\' cannot be created. Please run the script on a server.' ); } } } this.states.push = false; }; /** * Normalize mousewheel event accross browsers * @param {Object} event */ proto.normalizeWheel = function( event ) { var ev = event || window.event, data = null, deltaX, deltaY, delta; event.preventDefault(); // old scrollwheel events if ( 'detail' in ev ) { deltaY = ev.detail * -1; } if ( 'wheelDelta' in ev ) { deltaY = ev.wheelDelta * -1; } if ( 'wheelDeltaY' in ev ) { deltaY = ev.wheelDeltaY * -1; } if ( 'wheelDeltaX' in ev ) { deltaX = ev.wheelDeltaX * -1; } // W3C wheel event if ( 'deltaY' in ev ) { deltaY = ev.deltaY; } if ( 'deltaX' in ev ) { deltaX = ev.deltaX * -1; } // deltaX/Y values are specified in lines if ( ev.deltaMode === 1 ) { deltaX *= 40; deltaY *= 40; // deltaX/Y values are specified in pages } else if ( ev.deltaMode === 2 ) { deltaX *= 100; deltaY *= 100; } // normalize deltaX & deltaY delta = Math.abs( deltaX ) > Math.abs( deltaY ) ? deltaX : deltaY; delta = Math.min( 100, Math.max( -100, delta ) ); // if no scroll happened if ( Math.abs( delta ) < this.options.scrollSensitivity ) { this.states.prevDelta = delta; return data; } var time = +new Date(); // if mouse wheel velocity increases (touchpad or free wheel) // or elapsed time between mouse wheel events is superior to 60ms (user touchend touchpad or free wheel stopped) if ( Math.abs( delta ) > Math.abs( this.states.prevDelta ) || time - this.states.prevScroll > 60 ) { // set data data = { 'deltaX' : deltaX, 'deltaY' : deltaY, 'delta' : delta }; } // store last scroll data this.states.prevDelta = delta; this.states.prevScroll = time; return data; }; // ----- Main function / events ----- // /** * Reveal/hide share tooltip holder on click * @param {Object} event */ proto.share = function( event ) { // prevent issue on Android with native HTML5 video controls if ( event && event.target.tagName === 'VIDEO' ) { return; } var holder = this.DOM.holder, className = this.pre + '-open-tooltip', element = event ? event.target.className : null, method = utils.hasClass( holder, className ) ? 'remove' : 'add'; if ( ( method === 'remove' && ( element !== this.pre + '-share' || !event ) ) || element === this.pre + '-share' ) { if ( method === 'add' ) { this.setShareTooltip(); } utils[method + 'Class']( holder, className ); } }; /** * Share lightbox media from query url on social icon click * @param {Object} event */ proto.shareOn = function( event ) { var type = event.target.action, gallery = this.gallery, media = this.getMedia(), image = media.type === 'image' ? media.src : media.poster, url = this.socialMedia[type], uri; if ( url ) { // prepare uri from base url if ( this.options.sharedUrl === 'page' ) { uri = [location.protocol, '//', location.host, location.pathname].join( '' ); // prepare uri from deeplink } else if ( this.options.sharedUrl === 'deeplink' || ['iframe', 'HTML'].indexOf( media.type ) > -1 ) { uri = this.setQueryString({ guid : gallery.name, mid : gallery.index + 1 }); // prepare uri from media file } else { uri = media.src.replace( /\s/g, '' ).split( ',' )[0]; if ( media.type === 'video' && media.format !== 'html5' ) { uri = media.share; } } // convert to absolute path if necessary var link = utils.createEl( 'a' ); link.href = image; image = link.href; link.href = uri; uri = link.href; var tmp = utils.createEl( 'div' ); tmp.innerHTML = media.caption; var text = ( tmp.textContent || tmp.innerText ).replace( /\s+/g, ' ' ).trim() || ''; url = url.replace( '[url]', encodeURIComponent( uri ) ) .replace( '[image]', encodeURIComponent( image ) ) .replace( '[text]', encodeURIComponent( text || document.title ) ); if ( url ) { var left = Math.round( window.screenX + ( window.outerWidth - 626 ) / 2 ), top = Math.round( window.screenY + ( window.outerHeight - 436 ) / 2 ); window.open( url, this.pre + '_share', 'status=0,resizable=1,location=1,toolbar=0,width=626,height=436,top=' + top + ',left=' + left ); } } else { utils.error( 'This social share media does not exist' ); } return false; }; /** * Set share tooltip position */ proto.setShareTooltip = function() { if ( this.options.controls.indexOf( 'share' ) > -1 ) { var attribute = 'right', tooltip = this.DOM.shareTooltip, width = tooltip.clientWidth, button = this.buttons.share.getBoundingClientRect(), position = button.left - width + button.width / 2 + 20; if ( position < 0 ) { attribute = 'left'; position = button.left + button.width / 2 - 20; } tooltip.setAttribute( 'data-position', attribute ); tooltip.style.top = this.DOM.topBar.height + 6 + 'px'; tooltip.style.left = position + 'px'; } }; /** * Download a media, generate downloadable link */ proto.download = function() { if ( !this.isDownloadable() ) { return false; } var media = this.getMedia(), url = media.src.replace( /\s/g, '' ).split( ',' )[0], link = document.createElement( 'a' ); link.href = url; link.download = new Date().getTime(); link.setAttribute( 'target', '_blank' ); document.body.appendChild( link ); link.click(); document.body.removeChild( link ); }; /** * Handle click on fullScreen button */ proto.fullScreen = function() { var fullScreenElement = this.browser.fullScreen.element; if ( !document[fullScreenElement] ) { this.requestFullScreen(); } else { this.exitFullScreen(); } }; /** * Switch fullscreen state on fullScreen change */ proto.toggleFullScreen = function() { var holder = this.DOM.holder, fullScreenElement = document[this.browser.fullScreen.element]; if ( !fullScreenElement ) { this.share(); this.states.fullScreen = false; utils.removeClass( holder, this.pre + '-fullscreen' ); } else if ( fullScreenElement === holder ) { // set share tooltip position (fix issue on Safari) this.setShareTooltip(); this.states.fullScreen = true; utils.addClass( holder, this.pre + '-fullscreen' ); } this.videoFullScreen(); }; /** * RequestFullScreen lightbox holder */ proto.requestFullScreen = function() { var request = this.browser.fullScreen.request; if ( document.documentElement[request] ) { this.DOM.holder[request](); } }; /** * ExitFullScreen lightbox holder */ proto.exitFullScreen = function() { var exit = this.browser.fullScreen.exit; if ( document[exit] ) { document[exit](); } }; // ----- Slideshow & count down timer ----- // /** * Play slideshow event on click */ proto.play = function() { if ( this.states.play ) { this.stopSlideShow(); } else { this.startSlideShow(); } }; /** * Start play and countdown timer */ proto.startSlideShow = function() { var start = 0, gallery = this.gallery, options = this.options, loop = this.states.loop, autoStop = options.slideShowAutoStop, cycle = Math.max( 120, options.slideShowInterval ), count = options.countTimer, canvas = count && this.DOM.timer ? this.DOM.timer.getContext( '2d' ) : null; var countDown = ( function( now ) { now = !now ? +new Date() : now; start = !start ? now : start; if ( !loop || autoStop ) { if ( gallery.index === gallery.initialLength - 1 ) { this.stopSlideShow(); return; } } if ( count && canvas ) { var percent = Math.min( 1, ( now - start + cycle ) / cycle - 1 ), radians = percent * 360 * ( Math.PI / 180 ); canvas.clearRect( 0, 0, 48, 48 ); this.timerProgress( canvas, options.countTimerBg, 100 ); this.timerProgress( canvas, options.countTimerColor, radians ); } if ( now >= start + cycle ) { start = now; this.slideTo( this.slides.index + 1, true ); } this.timer = requestAnimationFrame( countDown ); }).bind( this ); // reveal canvas timer utils.addClass( this.DOM.holder, this.pre + '-autoplay' ); // play state this.states.play = true; // start timer this.timer = requestAnimationFrame( countDown ); }; /** * Stop slideshow */ proto.stopSlideShow = function() { cancelAnimationFrame( this.timer ); utils.removeClass( this.DOM.holder, this.pre + '-autoplay' ); this.states.play = false; }; /** * Canvas arc countdown timer progress * @param {Object} canvas * @param {string} color * @param {number} end */ proto.timerProgress = function( canvas, color, end ) { var start = 1.5 * Math.PI; canvas.strokeStyle = color; canvas.lineWidth = 5; canvas.beginPath(); canvas.arc( 24, 24, 18, start, start + end, false ); canvas.stroke(); }; // ----- Handle videos ----- // /** * Append video DOM in slide on click */ proto.appendVideo = function() { var media = this.getMedia(); if ( media.type !== 'video' ) { return; } utils.addClass( media.dom, this.pre + '-loading' ); utils.removeClass( media.dom, this.pre + '-playing' ); if ( !media.video ) { // append HTML5 video if ( media.format === 'html5' ) { // keep in memory video dom (for progression) media.video = utils.createEl( 'video' ); media.video.setAttribute( 'controls', '' ); media.video.setAttribute( 'autoplay', '' ); var urls = media.src.replace( /\s/g, '' ).split( ',' ); // loop through each video source for ( var i = 0; i < urls.length; i++ ) { var frag = document.createDocumentFragment(), source = utils.createEl( 'source' ), type = ( /^.+\.([^.]+)$/ ).exec( urls[i] ); // check if video type is HTML5 valid if ( type && ['mp4', 'webm', 'ogv'].indexOf( type[1] ) > -1 ) { source.src = urls[i]; source.setAttribute( 'type', 'video/' + ( type[1] === 'ogv' ? 'ogg' : type[1] ) ); frag.appendChild( source ); } media.video.appendChild( frag ); } } // append iFrame video else if ( media.format ) { // create iframe media.video = utils.createEl( 'iframe' ); media.video.src = media.src; media.video.setAttribute( 'frameborder', 0 ); media.video.setAttribute( 'allowfullscreen', '' ); } // set width/height media.video.setAttribute( 'width', '100%' ); media.video.setAttribute( 'height', '100%' ); } // append video if not exists if ( !media.dom.firstChild ) { // append video if not exists media.dom.appendChild( media.video ); // reset loaded for iframe to preload before reveal if ( media.format !== 'html5' ) { media.video.loaded = false; } } // play video this.playVideo( media ); }; /** * Play video * @param {Object} media */ proto.onVideoLoaded = function( media ) { media.video.loaded = true; utils.removeClass( media.dom, this.pre + '-loading' ); utils.addClass( media.dom, this.pre + '-playing' ); this.cloneVideo( media ); }; /** * Clone video * allows to preserve video state if wrapped around (loop) * @param {Object} media */ proto.cloneVideo = function( media ) { if ( this.states.loop && media.format === 'html5' ) { var gallery = this.gallery, length = gallery.length, initial = gallery.initialLength, current = utils.modulo( initial, media.index ); for ( var i = 0; i < length; i++ ) { var index = utils.modulo( initial, gallery[i].index ); if ( index === current && gallery[i].index !== media.index ) { gallery[i].video = media.video; } } } }; /** * Enable/disable iframe video fullscreen * Disable fullscreen iframe if lightbox in fullscreen * Prevent issue on webkit browsers */ proto.videoFullScreen = function() { var media = this.getMedia(), fullScreen = this.states.fullScreen; if ( media.type === 'video' && media.format !== 'html5' && media.video ) { media.video[fullScreen ? 'removeAttribute' : 'setAttribute']( 'allowfullscreen', '' ); } }; /** * Play video * @param {{player}} media */ proto.playVideo = function( media ) { // if video already loaded if ( media.video.loaded ) { // trigger reflow to apply css transition media.video.getClientRects(); utils.removeClass( media.dom, this.pre + '-loading' ); utils.addClass( media.dom, this.pre + '-playing' ); if ( media.format !== 'html5' ) { if ( media.play ) { var message = typeof media.play === 'object' ? JSON.stringify( media.play ) : String( media.play ); media.video.contentWindow.postMessage( message, '*' ); } } else if ( !media.video.error ) { if ( typeof MediaElementPlayer === 'function' && typeof jQuery !== 'undefined' && this.options.mediaelement ) { // because on IOS/Android the native HTML5 player is preserved var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0]; if ( mejs.player ) { mejs.player.setControlsSize(); } mejs.play(); } else { media.video.play(); } } } else { var _this = this; // if mediaelement player if ( typeof jQuery !== 'undefined' && typeof MediaElementPlayer === 'function' && !media.play && this.options.mediaelement && !media.video.player ) { MediaElementPlayer( media.video, { features : ['playpause', 'stop', 'current', 'progress', 'duration', 'volume', 'fullscreen'], videoVolume : 'horizontal', startVolume : 0.8, keyActions : false, enableKeyboard : false, iPadUseNativeControls : true, iPhoneUseNativeControls : true, AndroidUseNativeControls : true, success : function( mejs ) { mejs.addEventListener( 'loadeddata', function() { // get right DOM (because on mobile devices preserve original HTML5 player) // (iPadUseNativeControls, iPhoneUseNativeControls, AndroidUseNativeControls) media.video = media.dom.lastChild; // if video is still in a slide if ( media.video ) { // remove offscreen div to prevent display issue var offScreen = media.video.previousSibling; if ( offScreen && offScreen.parentNode ) { offScreen.parentNode.removeChild( offScreen ); } _this.onVideoLoaded( media ); } }, media, false ); }, error : function() { _this.onVideoLoaded( media ); } }); } else if ( !media.video.onload ) { media.video.onload = media.video.onerror = media.video.onloadedmetadata = function() { // if video is still in a slide if ( media.dom.firstChild ) { _this.onVideoLoaded( media ); _this.videoFullScreen(); } }; media.video.src = media.src.replace( /\s/g, '' ).split( ',' )[0]; } } }; /** * Pause video */ proto.pauseVideo = function() { var media = this.getMedia(); if ( media && media.type === 'video' && media.video ) { utils.removeClass( media.dom, this.pre + '-playing' ); // prevent iframe to autoplay if pause while loading if ( !media.video.loaded ) { media.dom.innerHTML = ''; utils.removeClass( media.dom, this.pre + '-loading' ); return; } if ( media.format === 'html5' ) { if ( typeof MediaElementPlayer === 'function' && typeof jQuery !== 'undefined' && this.options.mediaelement ) { // because on IOS/Android the native HTML5 player is preserved var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0]; mejs.pause(); } else { media.video.pause(); } } else { if ( media.pause && media.format !== 'dailymotion' ) { var message = typeof media.pause === 'object' ? JSON.stringify( media.pause ) : String( media.pause ); media.video.contentWindow.postMessage( message, '*' ); // to handle embed videos with unknown postMessage methods } else { media.dom.innerHTML = ''; media.video = null; } } } }; // ----- Handle Media loading/sizing ----- // /** * Append media in slider * @param {number} media_index * @param {number} slide_index */ proto.insertMedia = function( media_index, slide_index ) { // get media from current gallery var media = this.gallery[media_index]; if ( !media ) { return; } // assign an index if not exists // necessary if manually pushed from the API if ( typeof media.index === 'undefined' ) { media.index = this.gallery.indexOf( media ); } this.buildMedia( media ); this.appendMedia( media, slide_index ); this.loadMedia( media, slide_index ); }; /** * Build media DOM * @param {Object} media */ proto.buildMedia = function( media ) { // if the media is not defined if ( typeof media.dom === 'undefined' ) { // detect media type switch ( media.type ) { case 'image': media.dom = utils.createEl( 'img', this.pre + '-img' ); media.dom.src = media.src; break; case 'video': media.dom = utils.createEl( 'div', this.pre + '-video' ); if ( media.poster ) { media.dom.style.backgroundImage = 'url("' + media.poster + '")'; } else { media.dom.loaded = true; } break; case 'iframe': media.dom = utils.createEl( 'iframe', this.pre + '-iframe' ); media.dom.setAttribute( 'allowfullscreen', '' ); media.dom.setAttribute( 'frameborder', 0 ); media.dom.src = media.src; break; case 'HTML': var element = media.src; var content = document.querySelector( element ); media.dom = utils.createEl( 'div', this.pre + '-html' ); media.dom.appendChild( utils.createEl( 'div', this.pre + '-html-inner' ) ); media.dom.firstChild.innerHTML = content ? content.innerHTML : null; media.src = content ? content : ''; media.dom.loaded = true; break; } // handle no media type & not content found (HMTL src empty for example) if ( !media.type || !media.src ) { // build no content message media.dom = utils.createEl( 'div', this.pre + '-error' ); media.dom.textContent = this.options.noContent; media.dom.loaded = true; media.dom.error = true; // dispatch load error event utils.dispatchEvent( this, 'modulobox', 'noContent', this.gallery.name, parseInt( media.index, 10 ) ); } } }; /** * Append in a slide media DOM * @param {Object} media * @param {number} slide_index */ proto.appendMedia = function( media, slide_index ) { // get media slide var slide = this.slides[slide_index], holder = slide.firstChild, loader; // if slide empty if ( !holder.childElementCount ) { var fragment = document.createDocumentFragment(); loader = utils.createEl( 'div', this.pre + '-loader' ); fragment.appendChild( loader ); fragment.appendChild( media.dom ); holder.appendChild( fragment ); // if a media is already present in the slide } else { var oldMedia = holder.lastChild; loader = holder.firstChild; loader.style.visibility = ''; // prevent unnecessary DOM manipulations if media already exists in slide if ( media.dom !== oldMedia ) { // prevent to remove and duplicate media from another slide (because of replaceChild) // small hack for low performance (like IE9 or too much long key frames) // slideTo() method already includes a throttle (120ms) which will also fix it more naturally var method = holder.childElementCount === 1 ? 'appendChild' : 'replaceChild'; holder[method]( media.dom, oldMedia ); } } // Assign media index to slide slide.media = media.index; }; /** * Load image/iframe * @param {Object} media * @param {number} slide_index */ proto.loadMedia = function( media, slide_index ) { // show immediatly media if already loaded if ( media.dom.loaded ) { this.showMedia( media, slide_index ); return; } var _this = this, dom = media.type === 'iframe' ? media.dom : media.dom.img = new Image(); // on media load complete var onComplete = function() { // dispatch loaded event if ( !media.dom.error ) { utils.dispatchEvent( _this, 'modulobox', 'loadComplete', _this.gallery.name, parseInt( media.index, 10 ) ); } // need to reload iframe because loaded DOM can't be stored due to cross-origin policy media.dom.loaded = media.type !== 'iframe' ? true : false; // reveal media _this.showMedia( media, slide_index ); }; // handle onload events dom.onload = onComplete; // handle onerror event dom.onerror = function(e) { // set error message // not for video because it only concerns a poster and not the media itself if ( media.type !== 'video' ) { // build load error message media.dom = utils.createEl( 'p', _this.pre + '-error' ); media.dom.textContent = _this.options.loadError; media.dom.error = true; // append error message _this.appendMedia( media, slide_index ); } // dispatch loaded error event utils.dispatchEvent( _this, 'modulobox', 'loadError', _this.gallery.name, parseInt( media.index, 10 ) ); // trigger complete to reveal error onComplete(); }; // set src dom.src = ( media.type === 'video' ) ? media.poster : media.src; }; /** * Unload media src * @param {Object} slide */ proto.unloadMedia = function( slide ) { if ( !this.gallery ) { return; } var index = slide.media, media = this.gallery[index]; if ( !media || !media.dom ) { return; } // cancel old media if exists (and option enabled) if ( this.options.unload && media.type === 'image' && !media.dom.loaded && !media.dom.complete && !media.dom.naturalWidth ) { // unset events and src media.dom.onload = null; media.dom.onerror = null; media.dom.src = ''; if ( media.dom.img ) { // unset events and src for img object media.dom.img.onload = null; media.dom.img.onerror = null; media.dom.img.src = ''; delete media.dom.img; } // unset dom to reset src from buildMedia method delete media.dom; } // remove iframe video (prevent autoplay if loop enable) else if ( media.type === 'video' && media.format !== 'html5' && media.dom.firstChild ) { media.video = null; media.dom.removeChild( media.dom.firstChild ); } }; /** * Remove all content present in slide * @param {Object} holder */ proto.removeMedia = function( holder ) { var content = holder.firstChild; if ( !content ) { return; } while ( content.firstChild ) { content.removeChild( content.firstChild ); } }; /** * Reveal media content in slide * @param {Object} media * @param {number} slide_index */ proto.showMedia = function( media, slide_index ) { // get slider states var slider = this.slider; // if option fade when settled, check if slider settled to reveal if ( this.options.fadeIfSettle && !slider.settle && !media.dom.revealed ) { return; } // get slide DOM var slide = this.slides[slide_index], gallery = this.gallery, holder = slide.firstChild, loader = holder.firstChild, preload = this.options.preload; // set media size this.setMediaSize( media, slide ); // check if media is zoomable if ( media.index === gallery.index ) { this.isZoomable(); } // reveal media object // even if media is not in a slide, it will be revealed in media object (stored in gallery obj) utils.addClass( media.dom, this.pre + '-media-loaded' ); media.dom.revealed = true; // if current media match slide if ( slide.media === media.index ) { // hide loader loader.style.visibility = 'hidden'; // increment number of media loaded in the gallery gallery.loaded += 1; // set closest media if ( gallery.loaded === preload && preload < 4 ) { this.setMedia( preload + 2 ); } } // if iframe than reset loaded // because we can't store content due to cross-origin policy if ( media.type === 'iframe' ) { media.dom.loaded = false; } }; /** * Calculate and set media size styles * @param {Object} media * @param {Object} slide */ proto.setMediaSize = function( media, slide ) { var object = media.dom, slider = this.slider, viewport = object.viewport, thumbs = this.getThumbHeight(); // if error no size to calculate if ( object.error ) { return; } // if the media was not already sized for the current slider viewport // prevent unnecessary calculations and reflows from caption calculations (clientHeight) if ( !viewport || viewport.width !== slider.width || viewport.height !== slider.height - thumbs ) { this.getCaptionHeight( media, slide ); this.getMediaSize( media, slide ); this.fitMediaSize( media, slide ); this.setMediaOffset( media, slide ); } var style = object.style; // media is not displayed (no reflow) style.width = object.size.width + 'px'; style.height = object.size.height + 'px'; style.left = object.offset.left + 'px'; style.top = object.offset.top + 'px'; }; /** * Calculate caption height & assign slide size * @param {Object} media * @param {Object} slide */ proto.getCaptionHeight = function( media, slide ) { var caption = this.DOM.captionInner, topBar = this.DOM.topBar.height, content = caption.innerHTML, thumbs = this.getThumbHeight(); if ( this.options.caption && this.states.caption && media.caption ) { // set media caption caption.innerHTML = media.caption; // get caption height caption.height = Math.max( topBar, parseInt( caption.clientHeight, 10 ) ) || topBar; // restore current caption caption.innerHTML = content; // if caption is hidden } else { // set caption height to top bar // allow to center image in the viewport // if thumbnails fit to the top of the thumbs slider caption.height = thumbs ? 0 : topBar; } // set slide size (not real size, just an helper) slide.width = this.slider.width; slide.height = this.slider.height - topBar - caption.height - thumbs; }; /** * Calculate natural media size * @param {Object} media * @param {Object} slide */ proto.getMediaSize = function( media, slide ) { var size = media.dom.size = {}; switch ( media.type ) { case 'image': size.width = media.dom.naturalWidth; size.height = media.dom.naturalHeight; break; case 'video': size.width = this.options.videoMaxWidth; size.height = size.width / this.options.videoRatio; break; case 'iframe': size.width = media.width ? media.width : slide.width > 680 ? slide.width * 0.8 : slide.width; size.height = media.height ? media.height : slide.height; break; case 'HTML': size.width = media.width ? media.width : slide.width; size.height = media.height ? media.height : slide.height; break; } }; /** * Fit media size to slide * @param {Object} media * @param {Object} slide */ proto.fitMediaSize = function( media, slide ) { var slider = this.slider, options = this.options, zoom = options.zoomTo, size = media.dom.size, ratio = size.width / size.height, thumbs = this.getThumbHeight(), smallDevice = slider.width <= 480 || slider.height <= 680, canOverflow = ['video', 'iframe', 'HTML'].indexOf( media.type ) < 0, smartResize = options.smartResize && smallDevice, width, height; // add slide height viewport var viewports = [slide.height]; // add slider height viewport if ( ( smartResize || options.overflow ) && canOverflow ) { viewports.unshift( slider.height - thumbs ); } // check if media size fit to viewports (viewport) viewports.forEach( function( viewport ) { if ( !height || height < slider.height - thumbs ) { width = Math.min( size.width, ratio * viewport ); width = width > slide.width ? slide.width : Math.round( width ); height = Math.ceil( 1 / ratio * width ); height = height % viewport < 2 ? viewport : height; } }); // Calculate from how much the image can be zoomed var scale = Number( ( size.width / width ).toFixed( 3 ) ); zoom = zoom === 'auto' ? scale : zoom; media.dom.size = { width : width, height : height, scale : scale >= options.minZoom ? Math.min( options.maxZoom, zoom ) : 1 }; }; /** * Calculate media offset * @param {Object} media * @param {Object} slide */ proto.setMediaOffset = function( media, slide ) { var size = media.dom.size, slider = this.slider, topBar = this.DOM.topBar.height, thumbs = this.getThumbHeight(), fromTop = 0; // if the media can be centered if ( size.height <= slide.height ) { // center from the available slide height fromTop = topBar + ( slide.height - size.height ) * 0.5; } // set media offset media.dom.offset = { top : fromTop < 0 ? 0 : Math.round( fromTop ), left : Math.round( ( slide.width - size.width ) * 0.5 ) }; // set media viewport media.dom.viewport = { width : slider.width, height : slider.height - thumbs }; }; /** * Calculate media viewport * @param {number} scale * @return {Object} * hGap : difference between slider height and image size * vGap : difference image offset and image overflow * cGap : distance between image center and slider center * sGap : cGap scaled * these values allows to take into account the image center gap regarding the slide center */ proto.mediaViewport = function( scale ) { var media = this.getMedia(); // if loading or not a media (error) if ( !media.dom || !media.dom.size ) { return { top : 0, bottom : 0, left : 0, right : 0 }; } // calculate media viewport var size = media.dom.size, offset = media.dom.offset, height = this.slider.height, width = this.slider.width, hGap = ( height - size.height ) * 0.5, vGap = offset.top * 2 - hGap, cGap = ( hGap - vGap ) * 0.5, sGap = cGap * scale - cGap * 2 - vGap, left = size.width / 2 * ( scale - 1 ) - offset.left, top = size.height * scale <= height ? cGap * scale : -size.height / 2 * ( scale - 1 ) + height - size.height + sGap, bottom = size.height * scale <= height ? cGap * scale : size.height / 2 * ( scale - 1 ) + sGap; // set panZoom viewport return { top : scale <= 1 ? 0 : Math.round( top ), bottom : scale <= 1 ? 0 : Math.round( bottom ), left : size.width * scale < width ? 0 : Math.round( left ), right : size.width * scale < width ? 0 : Math.round( -left ) }; }; /** * Set/add media in slides * @param {number} number */ proto.setMedia = function( number ) { var gallery = this.gallery, slides = this.slides, loop = this.states.loop, RTL = this.isRTL(), index = Math.round( - RTL * this.slider.position.x / slides.width ), length = gallery.initialLength - 1, adjust = 0, toLoad = [], i; // find number of media to load if ( !number && !gallery.loaded ) { number = 0; for ( i = 0; i < slides.length; i++ ) { if ( slides[i].firstChild.childElementCount ) { number++; } } number += 2; gallery.loaded = this.options.preload; } // prepare positions switch ( number ) { case 0: case 1: toLoad = [0]; break; case 2: case 3: toLoad = [-1, 0, 1]; break; default: number = 5; toLoad = [-2, -1, 0, 1, 2]; } // adjust positions to ends if no loop if ( !loop ) { var maxMedia = index + toLoad[number - 1], minMedia = index + toLoad[0]; adjust = ( minMedia < 0 ) ? -minMedia : 0; adjust = ( maxMedia > length ) ? length - maxMedia : adjust; } // convert positions to media index toLoad = toLoad.map( function( i ) { return utils.modulo( gallery.length, i + adjust + index ); }); // insert media in corresponding slides for ( i = 0; i < slides.length; i++ ) { var slide = slides[i], media_index = utils.modulo( gallery.length, slide.index ); // if no loop && media index bigger than slide index if ( !loop && slide.index > media_index ) { continue; } // if the media corresponds to a slide and is not already assigned if ( toLoad.indexOf( media_index ) > -1 && slide.media !== media_index ) { // unload previous media this.unloadMedia( slide ); // insert media in slide this.insertMedia( media_index, i ); } } }; /** * Update media info based on current slide index */ proto.updateMediaInfo = function() { var slides = this.slides, gallery = this.gallery; // update gallery index gallery.index = utils.modulo( gallery.length, slides.index ); this.isZoomable(); this.isDownloadable(); this.updateCounter(); this.updateCaption(); this.updateThumbs(); // dispatch update event utils.dispatchEvent( this, 'modulobox', 'updateMedia', this.getMedia() ); }; /** * Set thumbnails */ proto.setThumbs = function() { var gallery = this.gallery, thumbs = this.thumbs, length = gallery.initialLength, thumbnails = this.options.thumbnails, thumbsHolder = this.DOM.thumbsHolder; // return if no thumb opt or if only one thumbnail to display if ( !thumbnails || length < 2 ) { // hide thumbs and reset caption position this.DOM.caption.style.bottom = 0; thumbsHolder.style.visibility = 'hidden'; thumbsHolder.style.height = 0; // unset thumbs thumbs.height = thumbs.gutter = 0; return; } // get max screen width available on the current device var sizes = this.options.thumbnailSizes, screenW = Math.max( window.innerWidth, Math.max( screen.width, screen.height ) ), thumbNb = 0, i; // sort thumbnail browser widths var widths = Object.keys( sizes ).sort( function( a, b ) { return a - b; }); // find number of thumbnails needed to build the thumbnails slider for ( i = 0; i < widths.length; i++ ) { var size = widths[i], width = i === widths.length - 1 ? screenW : Math.min( screenW, size ), number = Math.ceil( width / ( sizes[size].width + sizes[size].gutter ) * 2 ); if ( isFinite( number ) && number > thumbNb ) { thumbNb = number; } if ( size >= screenW ) { break; } } // set the number of thumbnail requiered for the current gallery var fragment = document.createDocumentFragment(); length = length > 50 ? Math.min( thumbNb, length ) : length; // prepare thumbnails for ( i = 0; i < length; i++ ) { var thumb = utils.createEl( 'div', this.pre + '-thumb' ); fragment.appendChild( thumb ); } // append thumbnails this.DOM.thumbsInner.appendChild( fragment ); // set thumbnails position this.setThumbsPosition(); }; /** * Set thumbnails * @param {Object} event */ proto.thumbClick = function( event ) { var target = event.target; if ( !utils.hasClass( target, this.pre + '-thumb' ) ) { target = target.parentNode; } if ( parseInt( target.index, 10 ) >= 0 ) { this.slideTo( target.index ); } }; /** * Set & init animation (slider & cells) * @param {Object} thumb * @param {number} index */ proto.loadThumb = function( thumb, index ) { // get media var media = this.gallery[index], src; // if the thumbnail was not loaded (is not DOM object) if ( !media.thumb || typeof media.thumb !== 'object' ) { src = media.thumb; media.thumb = utils.createEl( 'div', this.pre + '-thumb-bg' ); media.thumb.style.backgroundImage = src && src.indexOf( '.json' ) < 0 ? 'url(' + src + ')' : null; // add video icon if ( media.type === 'video' ) { utils.addClass( media.thumb, this.pre + '-thumb-video' ); utils.addClass( media.thumb, this.pre + '-thumb-loaded' ); } } // append/replace thumbnail var method = thumb.firstChild ? 'replaceChild' : 'appendChild'; thumb[method]( media.thumb, thumb.firstChild ); // save media index to thumb thumb.media = index; // load thumbnail if ( src ) { var dom = new Image(); // handle onload events (no need to handle error/abort) dom.onload = function() { // reveal thumbnail utils.addClass( media.thumb, this.pre + '-thumb-loaded' ); }.bind( this ); // set image src dom.src = src; } }; /** * Update thumbnail active */ proto.updateThumbs = function() { var gallery = this.gallery; if ( !this.options.thumbnails || gallery.initialLength < 2 ) { return; } var thumbs = this.thumbs, position = this.getThumbPosition( thumbs ); // stop current animation thumbs.stopAnimate(); // if thumb slider already at the right position if ( position === thumbs.position.x ) { this.shiftThumbs( thumbs ); return; } // move thumbnails without animation if ( Math.abs( position - thumbs.position.x ) > 50 * thumbs.size ) { this.DOM.thumbsHolder.style.visibility = 'hidden'; thumbs.position.x = position; utils.translate( this.DOM.thumbsInner, position, 0 ); this.renderThumbs( thumbs ); this.DOM.thumbsHolder.style.visibility = ''; // move thumbnails } else { thumbs.startAnimate(); thumbs.releaseDrag(); thumbs.animateTo({ x : position }); } }; /** * Set caption height */ proto.updateCaption = function() { if ( this.options.caption ) { // get caption content var media = this.getMedia(), content = media.caption ? media.caption : '', caption = this.DOM.captionInner; // set the caption content if not already present if ( caption.innerHTML !== content ) { caption.innerHTML = content; } } }; /** * Set counter index */ proto.updateCounter = function() { if ( this.options.counterMessage ) { var gallery = this.gallery, length = gallery.initialLength, index = utils.modulo( length, gallery.index ), message = this.options.counterMessage, content = message.replace( '[index]', index + 1 ).replace( '[total]', length ), counter = this.DOM.counter; // set the counter if not already set if ( counter.textContent !== content ) { counter.textContent = content; } } }; // ----- Set position and animations ----- // /** * Wrap media around for infinite loop */ proto.wrapAround = function() { var loop = this.options.loop, gallery = this.gallery, length = gallery.length; // store initial length if ( !gallery.initialLength ) { gallery.initialLength = length; } // set loop state this.states.loop = loop && loop <= length ? true : false; // add fake media if infinite loop and media length inferior to slides length if ( this.states.loop && length < this.slides.length ) { var add = Math.ceil( this.slides.length / length ) * length - length; for ( var i = 0; i < add; i++ ) { var index = length + i; gallery[index] = utils.cloneObject( gallery[utils.modulo( length, i )] ); gallery[index].index = index; } } }; /** * Set slider */ proto.setSlider = function() { var slider = this.slider, slides = this.slides; this.setSizes( slider, slides ); this.setSliderPosition( slider, slides ); this.setSlidesPositions( slides ); // show overlay this.DOM.overlay.style.opacity = 1; }; /** * Set slider and slide sizes * @param {Object} slider * @param {Object} slides */ proto.setSizes = function( slider, slides ) { // prevent issue on Google Chrome and mobile devices on orientation change // this.DOM.holder.clientWidth wrong on mobile devices with Chrome because fixed position slider.width = document.body.clientWidth; slider.height = window.innerHeight; // always right value on IOS devices (prevent Safari top bar issue on resize) slides.width = slider.width + Math.round( slider.width * this.options.spacing ); }; /** * Set slides/cells positions * @param {Object} slides */ proto.setSlidesPositions = function( slides ) { for ( var i = 0; i < slides.length; i++ ) { slides[i].position = null; this.setCellPosition( i ); } this.shiftSlides(); }; /** * Set thumbnails positions */ proto.setThumbsPosition = function() { if ( !this.options.thumbnails || this.gallery.initialLength < 2 ) { return; } var thumbs = this.thumbs, slider = this.slider, holder = this.DOM.thumbsHolder, inner = this.DOM.thumbsInner, sizes = this.options.thumbnailSizes, RTL = this.options.rightToLeft, widths = Object.keys( sizes ).sort( function( a, b ) { return b - a; }), width = Math.max.apply( null, widths ), browser = window.innerWidth, size; // get current thumbnail size for ( var i = 0; i < widths.length; i++ ) { size = Number( widths[i] ); if ( browser <= size ) { width = size; } } // set thumbnails data thumbs.width = Number( sizes[width].width ); thumbs.gutter = Number( sizes[width].gutter ); thumbs.height = Number( sizes[width].height ); thumbs.size = thumbs.width + thumbs.gutter; thumbs.length = this.gallery.initialLength; // get thumbnails slider total width var totalWidth = thumbs.length * thumbs.size; // basic navigation thumbs.bound = { left: 0, right: totalWidth > slider.width ? slider.width - totalWidth : 0 }; if ( RTL ) { thumbs.bound.right = totalWidth > slider.width ? slider.width - thumbs.size : totalWidth - thumbs.size; thumbs.bound.left = totalWidth - thumbs.size; } // centered navigation if ( this.options.thumbnailsNav === 'centered' ) { thumbs.bound = { left : totalWidth > slider.width ? Math.floor( slider.width * 0.5 - thumbs.size * 0.5 ) : Math.floor( totalWidth * 0.5 - thumbs.size * 0.5 ), right : totalWidth > slider.width ? Math.ceil( slider.width * 0.5 - totalWidth + thumbs.size * 0.5 ) : -Math.ceil( totalWidth * 0.5 - thumbs.size * 0.5 ) }; if ( RTL ) { thumbs.bound.right = thumbs.bound.left; thumbs.bound.left = thumbs.bound.left + totalWidth - thumbs.size; } } // reset thumbnail animation thumbs.resetAnimate(); var position = this.getThumbPosition( thumbs ); // reset thumbnails position; thumbs.position.x = position; // set thumbnails inner position utils.translate( inner, position, 0 ); // set holder/inner sizes var hasHeight = this.getThumbHeight(); holder.style.visibility = hasHeight ? '' : 'hidden'; holder.style.height = hasHeight ? hasHeight + 'px' : ''; inner.style.height = hasHeight ? thumbs.height + Math.min( 10, thumbs.gutter ) + 'px' : ''; inner.style.width = thumbs.length * thumbs.size + 'px'; inner.style.right = totalWidth > slider.width && RTL ? 'auto' : ''; }; /** * Set thumbnails positions * @param {Object} thumbs */ proto.getThumbPosition = function( thumbs ) { var slider = this.slider, gallery = this.gallery, thumbNav = this.options.thumbnailsNav, RTL = this.isRTL(), left = RTL < 0 ? 'right' : 'left', index = utils.modulo( gallery.initialLength, gallery.index ), centered = slider.width * 0.5 - thumbs.size * 0.5, position = thumbs.bound[left] - index * thumbs.size * RTL; position = !thumbs.bound[left] ? position + centered : position + (RTL < 0 && thumbNav !== 'centered' ? - centered : 0); return Math.max( thumbs.bound.right, Math.min( thumbs.bound.left, position ) ); }; /** * Set cell (inside slides) positions * @param {number} index */ proto.setCellPosition = function( index ) { var cell = this.cells[index]; cell.resetAnimate(); utils.translate( this.slides[index].children[0], 0, 0, 1 ); }; /** * Set slider position * @param {Object} slider * @param {Object} slides */ proto.setSliderPosition = function( slider, slides ) { var RTL = this.options.rightToLeft, posX = - slides.index * slides.width; posX = RTL ? - posX : posX; slider.resetAnimate(); // set slider position/attraction to animate from right position slider.position.x = slider.attraction.x = posX; slider.bound = { left : 0, right : - ( this.gallery.length - 1 ) * slides.width }; if ( RTL ) { slider.bound.left = - slider.bound.right; slider.bound.right = 0; } utils.translate( this.DOM.slider, posX, 0 ); }; /** * Set & init animation (slider & cells) */ proto.setAnimation = function() { // set slider animations var slider = this.DOM.slider, friction = this.options.friction, attraction = this.options.attraction; this.slider = new Animate( // element to animate slider, // initial position { x : 0, y : 0 }, // friction coefficient Math.min( Math.max( friction.slider, 0 ), 1 ), // attraction coefficient Math.min( Math.max( attraction.slider, 0 ), 1 ) ); this.slider.on( 'settle.toanimate', this.settleSider.bind( this ) ); this.slider.on( 'render.toanimate', this.renderSlider.bind( this ) ); // set cell animations var slides = slider.children, length = slides.length; for ( var i = 0; i < length; i++ ) { this.cells[i] = new Animate( // element to animate slides[i].children[0], // initial position { x : 0, y : 0, s : 1 }, // friction coefficient Math.min( Math.max( friction.slide, 0 ), 1 ), // attraction coefficient Math.min( Math.max( attraction.slide, 0 ), 1 ) ); this.cells[i].on( 'settle.toanimate', this.settleCell.bind( this ) ); this.cells[i].on( 'render.toanimate', this.renderCell.bind( this ) ); } // set thumbnail animation this.thumbs = new Animate( // element to animate this.DOM.thumbsInner, // initial position { x : 0 }, // friction coefficient Math.min( Math.max( friction.thumbs, 0 ), 1 ), // attraction coefficient Math.min( Math.max( attraction.thumbs, 0 ), 1 ) ); this.thumbs.on( 'settle.toanimate', this.settleThumbs.bind( this ) ); this.thumbs.on( 'render.toanimate', this.renderThumbs.bind( this ) ); }; /** * Executed when the slider settle event is emitted from animate * @param {Object} slider */ proto.settleSider = function( slider ) { var media; utils.dispatchEvent( this, 'modulobox', 'sliderSettled', slider.position ); if ( this.states.open ) { this.setMedia(); this.replaceState(); } if ( this.options.fadeIfSettle ) { var slides = this.slides; for ( var i = 0; i < slides.length; i++ ) { var index = slides[i].media; media = this.gallery[index]; if ( media.dom.loaded ) { this.showMedia( media, i ); } } } }; /** * Executed when the cell settle event is emitted from animate * @param {Object} cell */ proto.settleCell = function( cell ) { var gesture = this.gesture; if ( gesture.closeBy ) { utils.dispatchEvent( this, 'modulobox', 'panYSettled', null, cell.position ); } if ( ( gesture.closeBy && gesture.canClose === false ) || !gesture.closeBy ) { utils.dispatchEvent( this, 'modulobox', 'panZoomSettled', null, cell.position ); } }; /** * Executed when the thumbs slider is settled * @param {Object} thumbs */ proto.settleThumbs = function( thumbs ) { utils.dispatchEvent( this, 'modulobox', 'thumbsSettled', null, thumbs.position ); }; /** * Executed when the slider is animated/rendered * @param {Object} slider */ proto.renderSlider = function( slider ) { // shift slide this.shiftSlides(); // calculate progress in float percent var RTL = this.isRTL(), length = this.gallery.initialLength, indexPos = - RTL * slider.position.x / this.slides.width, moduloPos = utils.modulo( length, indexPos ), progress = ( moduloPos > length - 0.5 ? 0 : moduloPos ) / ( length - 1 ); // dispatch sliderProgress event utils.dispatchEvent( this, 'modulobox', 'sliderProgress', null, Math.min( 1, Math.max( 0, progress ) ) ); }; /** * Executed when a cell is animated/rendered * @param {Object} cell */ proto.renderCell = function( cell ) { this.willClose( cell ); var progress; // dispatch panYProgress event if ( this.gesture.type === 'panY' || this.gesture.closeBy || ( this.gesture.type === 'dragSlider' && cell.position.y !== 0 ) ) { progress = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 ); utils.dispatchEvent( this, 'modulobox', 'panYProgress', null, progress ); } // dispatch panZoomProgress event if ( this.gesture.type !== 'panY' && cell.position.s !== 1 ) { progress = cell.position.s; utils.dispatchEvent( this, 'modulobox', 'panZoomProgress', null, progress ); } }; /** * Executed when the thumbnails slider is animated/rendered * @param {Object} thumbs */ proto.renderThumbs = function( thumbs ) { this.shiftThumbs( thumbs ); // calculate progress in float percent var progress = thumbs.bound.left !== thumbs.bound.right ? ( thumbs.bound.left - thumbs.position.x ) / ( thumbs.bound.left - thumbs.bound.right ) : 0; // dispatch thumbsProgress event utils.dispatchEvent( this, 'modulobox', 'thumbsProgress', null, progress ); }; // ----- gesture events ----- // /** * Touch start event (mouse/touch/pointer) * @param {Object} event */ proto.touchStart = function( event ) { // get current target tag name var element = event.target, tagName = element.tagName, className = element.className; // stop slideshow (if not play button touched) if ( event.which !== 3 && element !== this.buttons.play ) { this.stopSlideShow(); } // if right click or element touched is not part of the lightbox or a button/video/input/a if ( event.which === 3 || !this.isEl( event ) || ['BUTTON', 'VIDEO', 'INPUT', 'A'].indexOf( tagName ) > -1 ) { return; } // add dragging class if an image in a gallery is touched if ( tagName === 'IMG' && this.gallery.length > 1) { utils.addClass( this.DOM.holder, this.pre + '-dragging' ); } // prevent default action event.preventDefault(); // if share tooltip is open // after preventDefault() to prevent flicker on touch device (prevent touch scroll) if ( utils.hasClass( this.DOM.holder, this.pre + '-open-tooltip' ) ) { return; } // if no pointers available // then 1 first time pointer touches screen (without move) if ( !this.pointers.length ) { // reset can close var this.gesture.canClose = undefined; // bind move/end events utils.handleEvents( this, window, this.dragEvents.move, 'touchMove' ); utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd' ); } // add pointers this.addPointer( event ); // if thumbs not touched if ( className.indexOf( '-thumb' ) < 0 ) { // stop slider animation this.slider.stopAnimate(); // stop current cell animation var cell = this.getCell(); if ( Math.round( cell.position.s * 100 ) / 100 !== 1 || this.pointers.length === 2 || this.gesture.closeBy ) { cell.stopAnimate(); } } else { // stop thumbs slider animation this.thumbs.stopAnimate(); } // capture gestures this.gestures( 'start' ); }; /** * Touch move event (mouse/touch/pointer) * @param {Object} event */ proto.touchMove = function( event ) { // update active pointers this.updatePointer( event ); var gesture = this.gesture; // get number of actif pointers var pointerNb = this.pointers.length; // check if slider is near settled var isSettle = this.isSliderSettle(); // Switch panZoom to pan gesture (2 to 1 pointers) this.switchPointers(); // capture gestures this.gestures( 'move' ); // if a gesture occured if ( gesture.type ) { // handle/dispatch gesture Move this[gesture.type]( event ); utils.dispatchEvent( this, 'modulobox', gesture.type + 'Move', event, gesture ); // set gesture movement gesture.move = true; } // if dragged more than the threshold offset or pinched else if ( ( pointerNb === 2 && isSettle ) || Math.abs( gesture.dx ) > this.options.threshold || Math.abs( gesture.dy ) > this.options.threshold ) { // add threshold to prevent jump when panY/dragSlider gesture.sx += gesture.dx; gesture.sy += gesture.dy; // set for pinchToclose gesture.canZoom = this.isZoomable(); // reset closeBy gesture event on first move gesture.closeBy = false; // set gesture type gesture.type = Math.abs( gesture.dx ) < Math.abs( gesture.dy ) / 2 ? false : 'dragSlider'; gesture.type = this.options.dragToClose && !gesture.type && isSettle ? 'panY' : gesture.type; gesture.type = ( this.options.pinchToZoom || this.states.zoom ) && gesture.canZoom && isSettle && ( pointerNb === 2 || this.states.zoom ) ? 'panZoom' : gesture.type; gesture.type = this.options.pinchToClose && gesture.scale < 1 && isSettle && pointerNb === 2 ? 'panZoom' : gesture.type; gesture.type = event.target.className.indexOf( '-thumb' ) > -1 ? 'dragThumbs' : gesture.type; // update media before animation if ( gesture.type === 'dragSlider' ) { this.setMedia(); } // replace cell if dragSlider (slider move) if ( ['dragSlider', 'dragThumbs'].indexOf( gesture.type ) > -1 ) { var cell = this.getCell(); cell.startAnimate(); cell.releaseDrag(); cell.animateTo({ x : 0, y : 0, s : 1 }); } // replace slider if not settle and if cell move if ( gesture.type !== 'dragSlider' ) { var slider = this.slider, slides = this.slides; // if the slider does not snap to slides var RTL = this.isRTL(); if ( - RTL * slider.position.x !== slides.index * slides.width ) { slider.startAnimate(); slider.releaseDrag(); } } // if gesture occured if ( gesture.type ) { this.pauseVideo(); // dispatch start event utils.dispatchEvent( this, 'modulobox', gesture.type + 'Start', event, gesture ); // add dragging class if ( this.gallery.length > 1 || gesture.type !== 'dragSlider' ) { utils.addClass( this.DOM.holder, this.pre + '-dragging' ); } } } }; /** * Touch end event (mouse/touch/pointer) * @param {Object} event */ proto.touchEnd = function( event ) { // delete released pointers this.deletePointer( event ); // if no more pointer on screen if ( !this.pointers.length ) { // remove dragging class utils.removeClass( this.DOM.holder, this.pre + '-dragging' ); // remove events utils.handleEvents( this, window, this.dragEvents.move, 'touchMove', false ); utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd', false ); // if slider settle and no gesture happened if ( this.isSliderSettle() ) { var className = event.target.className; // play video if ( utils.hasClass( event.target, this.pre + '-video' ) ) { this.appendVideo(); // close lightbox } else if ( this.options.tapToClose && !this.states.zoom && ( className === this.pre + '-item-inner' || className === this.pre + '-top-bar' ) && Math.abs( this.gesture.dx ) < this.options.threshold ) { this.close(); return; } // check if double tap/click if ( event.target.tagName === 'IMG' ) { this.doubleTap( event ); } } // handle click on thumbnail if ( this.options.thumbnails && !this.gesture.move ) { this.thumbClick( event ); } // handle/dispatch gesture End var gestureEnd = this.gesture.type + 'End'; if ( this.gesture.type && typeof this[gestureEnd] === 'function' ) { this[gestureEnd]( event ); utils.dispatchEvent( this, 'modulobox', gestureEnd, event, this.gesture ); } // unset gesture this.gesture.type = this.gesture.move = false; // return if gesture trigger a close() (prevent to animate twice) if ( !this.states.open ) { return; } // make sure cell is settled var cell = this.getCell(); if ( !cell.settle ) { cell.startAnimate(); cell.releaseDrag(); } // make sure slider is settled var slider = this.slider; if ( !slider.settle ) { slider.startAnimate(); slider.releaseDrag(); } } }; /** * Switch panZoom to pan gestures */ proto.switchPointers = function() { // if switch from 2 pointers to 1 pointer (reset animation) if ( this.gesture.type === 'panZoom' && this.pointers.length === 1 && this.gesture.distance !== 0 ) { var moving = this.getCell(); moving.stopAnimate(); moving.startAnimate(); this.gesture.move = false; this.gestures( 'start' ); this.gestures( 'move' ); } }; /** * Handle double tap * @param {Object} event */ proto.doubleTap = function( event ) { event.preventDefault(); // map pointers var touches = this.mapPointer( event ), clientX = touches[0].clientX, clientY = touches[0].clientY; // check if double tap/click if ( typeof this.tap !== 'undefined' && // if already tap once +new Date() - this.tap.delay < 350 && // if delay between 2 taps < 350ms Math.abs( this.tap.deltaX - clientX ) < 30 && // if distanceX between 2 taps < 30px Math.abs( this.tap.deltaY - clientY ) < 30 // if distanceY between 2 taps < 30px ) { // restore idle state if double tap if ( this.states.tapIdle ) { clearTimeout( this.states.tapIdle ); } // zoom on image from pointer coordinates if ( this.options.doubleTapToZoom ) { this.zoomTo( clientX, clientY ); } // reset double tap data this.tap = undefined; // if first tap } else { // check if it's a touch device only (no mouse detect thanks to states.idle) if ( this.browser.touchDevice && this.options.timeToIdle && !this.states.idle ) { // wait to check if double tap will happen this.states.tapIdle = setTimeout( function() { var method = !utils.hasClass( this.DOM.holder, this.pre + '-idle' ) ? 'add' : 'remove'; utils[method + 'Class']( this.DOM.holder, this.pre + '-idle' ); }.bind( this ), 350 ); } // set tap data this.tap = { delay : +new Date(), deltaX : touches[0].clientX, deltaY : touches[0].clientY }; } }; /** * Check if slider is near settled * @return {boolean} */ proto.isSliderSettle = function() { // if a gesture occured return if ( this.gesture.type ) { return false; } // calculate from how much in percent we are from a settled position of the slider (threshold for pinch/close methods) var RTL = this.isRTL(), slides = this.slides, width = slides.width, toSettle = Math.abs( RTL * this.slider.position.x + slides.index * width ) / width * 100; // if slider position is a least inferior to 3% from viewport edges (treshold in percent) return toSettle <= 3 ? true : false; }; // ----- handle pointer event ----- // /** * Map mouse and touch events * @param {Object} event * @return {Object} */ proto.mapPointer = function( event ) { return event.touches ? event.changedTouches : [event]; }; /** * Add custom pointer for current event * Prevent multiple pointers breaking gesture * @param {Object} event */ proto.addPointer = function( event ) { var pointers = this.mapPointer( event ); for ( var i = 0; i < pointers.length; i++ ) { if ( this.pointers.length < 2 && ['dragSlider', 'panY', 'dragThumbs'].indexOf( this.gesture.type ) === -1 ) { var ev = pointers[i], // .pointerId for pointer events, .indentifier for touch events id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier; if ( !this.getPointer( id ) ) { this.pointers[this.pointers.length] = { id : id, // clientX/clientY are rounded because "pointer" event return float values // prevent from inaccurate calculations for the direction on W3C devices capable x : Math.round( ev.clientX ), y : Math.round( ev.clientY ) }; } } } }; /** * Update custom pointer for current event * @param {Object} event */ proto.updatePointer = function( event ) { var pointers = this.mapPointer( event ); for ( var i = 0; i < pointers.length; i++ ) { var ev = pointers[i], id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier, pt = this.getPointer( id ); if ( pt ) { pt.x = Math.round( ev.clientX ); pt.y = Math.round( ev.clientY ); } } }; /** * Delete custom pointer for current event * @param {Object} event */ proto.deletePointer = function( event ) { var pointers = this.mapPointer( event ); for ( var i = 0; i < pointers.length; i++ ) { var ev = pointers[i], id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier; for ( var p = 0; p < this.pointers.length; p++ ) { if ( this.pointers[p].id === id ) { this.pointers.splice( p, 1 ); } } } }; /** * Check if current pointer was record for the current gesture * @param {number} id * @return {Object} */ proto.getPointer = function( id ) { for ( var k in this.pointers ) { if ( this.pointers[k].id === id ) { return this.pointers[k]; } } return null; }; /** * Calculate distances from pointers * @param {string} type (start, move) */ proto.gestures = function( type ) { var g = this.gesture, pointers = this.pointers, distance; // if no pointer found return immediately if ( !pointers.length ) { return; } // set x direction g.direction = g.x ? pointers[0].x > g.x ? 1 : -1 : 0; // get current coordinates (1st pointer) g.x = pointers[0].x; g.y = pointers[0].y; if ( pointers.length === 2 ) { // get current coordinates (2nd pointer) var x2 = pointers[1].x, y2 = pointers[1].y; // calculate current distance between 2 pointers distance = this.getDistance( [g.x, g.y], [x2, y2] ); // get center coordinates (between 2 pointers) g.x = g.x - ( g.x - x2 ) / 2; g.y = g.y - ( g.y - y2 ) / 2; } // on touchStart only if ( type === 'start' ) { // store initial coordinates g.dx = 0; g.dy = 0; g.sx = g.x; g.sy = g.y; // get initial distance (between 2 pointers) g.distance = distance ? distance : 0; } else { // calculate distance in x & y when 1st pointer move g.dx = g.x - g.sx; g.dy = g.y - g.sy; // calculate scale factor (between 2 pointers) g.scale = distance && g.distance ? distance / g.distance : 1; } }; /** * Calculate distance between 2 pointers * @param {Array} p1 - pointer 1 * @param {Array} p2 - pointer 2 * @return {number} */ proto.getDistance = function( p1, p2 ) { var x = p2[0] - p1[0], y = p2[1] - p1[1]; return Math.sqrt( ( x * x ) + ( y * y ) ); }; // ----- handle gesture ----- // /** * Handle drag gesture in Y axis (1 pointer) */ proto.panY = function() { var moving = this.getCell(); moving.startAnimate(); moving.updateDrag({ x : moving.position.x, y : moving.start.y + this.gesture.dy, s : moving.position.s }); }; /** * Handle drag gesture in Y axis (1 pointer) end event */ proto.panYEnd = function() { var posY = 0, moving = this.getCell(), height = this.slider.height, resting = moving.resting.y; if ( 1 - Math.abs( resting ) / ( height * 0.5 ) < 0.8 ) { posY = Math.abs( resting ) < height * 0.5 ? Math.abs( resting ) / resting * height * 0.5 : resting; this.close(); moving.animateTo({ x : 0, y : posY, s : posY ? moving.resting.s : 1 }); moving.startAnimate(); moving.releaseDrag(); } }; /** * Handle panZoom gesture (2 pointers only) */ proto.panZoom = function() { var moving = this.getCell(), gesture = this.gesture, bound = this.mediaViewport( moving.position.s ), minZoom = this.options.pinchToClose && gesture.canClose ? 0.1 : 0.6, scale = Math.min( this.options.maxZoom * 1.5, Math.max( minZoom, moving.start.s * gesture.scale ) ), posX = moving.start.x + gesture.dx, posY = moving.start.y + gesture.dy, centerX = gesture.sx - this.slider.width * 0.5, centerY = gesture.sy - this.slider.height * 0.5; // prevent to zoom in if can't zoom but pinch to close if ( !gesture.canZoom || ( !this.options.pinchToZoom && !this.states.zoom ) ) { scale = Math.min( 1, scale ); } // prevent to zoom in if disabled (pinch) and keep current scale to pan if ( !this.options.pinchToZoom && this.states.zoom ) { scale = moving.position.s; } // if pan only, outside viewport, and just started animation // remove previous friction added outside viewport if ( !gesture.move && this.pointers.length === 1 ) { moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; moving.start.y += posY > bound.bottom ? posY - bound.bottom : posY < bound.top ? posY - bound.top : 0; } // calculate center between 1 or 2 pointers if zoom posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s ); posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s ); // add friction if pan (1 pointer) and outside viewport if ( this.pointers.length === 1 ) { posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; posY = posY > bound.bottom ? ( posY + bound.bottom ) * 0.5 : posY < bound.top ? ( posY + bound.top ) * 0.5 : posY; } moving.startAnimate(); moving.updateDrag({ x : posX, y : posY, s : scale }); this.updateZoom( scale ); }; /** * Handle panZoom gesture (2 pointers only) end event */ proto.panZoomEnd = function() { // if the resting scale value is near at 1 (under 1.1) force unzoom (allow faster gesture recognition) var moving = this.getCell(), gesture = this.gesture, scale = moving.resting.s > this.options.maxZoom ? this.options.maxZoom : moving.resting.s < 1 ? 1 : moving.resting.s, bound = this.mediaViewport( scale ), posX, posY; // if scaled more than authorized then recalculate position to max authorized scale if ( Math.round( moving.resting.s * 10 ) / 10 > this.options.maxZoom ) { var centerX = gesture.distance ? gesture.sx - this.slider.width * 0.5 : 0; var centerY = gesture.distance ? gesture.sy - this.slider.height * 0.5 : 0; posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s ); posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s ); posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX; posY = posY > bound.bottom ? bound.bottom : posY < bound.top ? bound.top : posY; } else { posX = moving.resting.x > bound.left ? bound.left : moving.resting.x < bound.right ? bound.right : undefined; posY = moving.resting.y > bound.bottom ? bound.bottom : moving.resting.y < bound.top ? bound.top : undefined; } // add resting to scale to animate when closing if ( this.options.pinchToClose && moving.resting.s < 0.8 && gesture.canClose ) { scale = moving.resting.s < 0.3 ? moving.resting.s : 0.15; posX = moving.resting.x; posY = moving.resting.y; this.close(); } moving.animateTo({ x : posX, y : posY, s : scale !== moving.resting.s ? scale : undefined }); moving.startAnimate(); moving.releaseDrag(); this.updateZoom( moving.resting.s ); }; /** * Handle drag gesture in X axis (1 pointer) */ proto.dragThumbs = function() { var moving = this.thumbs, bound = moving.bound, posX = moving.start.x + this.gesture.dx; // add resistance on ends if ( !this.gesture.move ) { moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; posX = moving.start.x + this.gesture.dx; } posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; moving.startAnimate(); // to add freescroll moving.attraction.x = undefined; moving.updateDrag({ x : posX }); }; /** * Handle drag gesture in X axis (1 pointer) */ proto.dragThumbsEnd = function() { var moving = this.thumbs, bound = moving.bound, posX = moving.resting.x; posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX; // if resting reach bounds/ends if ( posX !== moving.resting.x ) { moving.animateTo({ x: posX }); } moving.startAnimate(); moving.releaseDrag(); }; /** * Handle drag gesture in X axis (1 pointer) */ proto.dragSlider = function() { // prevent dragging slider if one media if ( this.gallery.length === 1 ) { return; } var moving = this.slider, posX = moving.start.x + this.gesture.dx; // add resistance if no loop if ( !this.states.loop ) { var bound = moving.bound; if ( !this.gesture.move ) { moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0; posX = moving.start.x + this.gesture.dx; } posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX; } moving.startAnimate(); moving.updateDrag({ x : posX }); }; /** * Handle drag gesture in X axis (1 pointer) end event */ proto.dragSliderEnd = function() { // prevent dragging slider if one media if ( this.gallery.length === 1 ) { return; } var moving = this.slider, slides = this.slides, oldIndex = slides.index, RTL = this.isRTL(), restingX = moving.resting.x, positionX = moving.position.x; this.getRestingIndex( positionX, restingX ); if ( oldIndex !== slides.index ) { this.updateMediaInfo(); } this.slider.animateTo({ x : - RTL * slides.index * slides.width, y : undefined, s : undefined }); moving.startAnimate(); moving.releaseDrag(); }; /** * Calculate new index based on velocity and slider position * @param {number} positionX * @param {number} restingX */ proto.getRestingIndex = function( positionX, restingX ) { var direction = this.gesture.direction, gallery = this.gallery, slides = this.slides, deltaX = this.gesture.dx, RTL = this.isRTL(), index = Math.round( - RTL * positionX / slides.width ), moved = Math.abs( restingX - positionX ); if ( Math.abs( deltaX ) < slides.width * 0.5 && moved ) { if ( deltaX > 0 && direction > 0 ) { index -= 1 * RTL; } else if ( deltaX < 0 && direction < 0 ) { index += 1 * RTL; } } // constrain to slide one slide by one slide var gap = Math.max( -1, Math.min( 1, index - slides.index ) ); // prevent going outside limit if no loop if ( !this.states.loop && ( ( gallery.index + gap < 0 ) || ( gallery.index + gap > gallery.length - 1 ) ) ) { return; } slides.index += gap; }; /** * Shift slide around when slider render */ proto.shiftSlides = function() { var slides = this.slides, gallery = this.gallery, loop = this.states.loop, RTL = this.isRTL(), from = RTL * Math.round( - this.slider.position.x / slides.width ) - 2, to = from + 5; if ( !loop && to > gallery.initialLength - 1 ) { from = gallery.initialLength - 5; to = from + 5; } if ( !loop && from < 0 ) { from = 0; to = 5; } for ( var i = from; i < to; i++ ) { var position = RTL * i * slides.width, slide_index = utils.modulo( slides.length, i ), slide = slides[slide_index]; if ( slide.index !== i || slide.position !== position ) { // set slide virtual index to get the media index slide.index = i; // set position (to resize) if slide width change slide.position = position; // apply new position slide.style.left = position + 'px'; } } // if open only because need to be set after sizing if ( this.states.open ) { // load only 3 media to prevent appending one or several DOM while dragging/realasing // before a touchMove media are set (5), so no reflow/repaint happen during an animation // improve performance on touch (mobile) devices this.setMedia( 3 ); } }; /** * Shift thumbnails and load thumbs img * @param {Object} thumbs */ proto.shiftThumbs = function( thumbs ) { var child = this.DOM.thumbsInner.children, slider = this.slider, gallery = this.gallery, RTL = this.isRTL(), length = child.length, index = utils.modulo( gallery.initialLength, gallery.index ), width = thumbs.size * ( length ) * 0.5, center = Math.round( ( - RTL * thumbs.position.x + RTL * width * 0.5 ) / thumbs.size ), from = Math.max( 0, center - Math.floor( length / 2 ) ), to = from + length; // current thumbnail viewport var tolerance = slider.width * 0.5, boundLeft = thumbs.position.x + tolerance, boundRight = thumbs.position.x - slider.width - tolerance; if ( to > gallery.initialLength ) { to = gallery.initialLength; from = to - length; } // make sure we are looping through all thumbs if ( to === gallery.initialLength - 1 && from - to < length ) { from = gallery.initialLength - length; } for ( var i = from; i < to; i++ ) { var thumb = child[utils.modulo( length, i )], position = RTL * i * thumbs.size + thumbs.gutter * 0.5, className = this.pre + '-active-thumb', isActive = utils.hasClass( thumb, className ); // set thumbnail position if ( thumb.index !== i || thumb.position !== position ) { thumb.index = i; thumb.position = position; thumb.style.left = position + 'px'; } // set thumbnail size this.setThumbSize( thumb, thumbs ); // if thumbnail is in the viewport if ( - thumb.position <= boundLeft && - thumb.position >= boundRight && thumb.media !== i ) { this.loadThumb( thumb, i ); } // add active class if ( isActive && index !== i ) { utils.removeClass( thumb, className ); } else if ( !isActive && index === i ) { utils.addClass( thumb, className ); } } }; /** * Set thumbnail size * @param {Object} thumb * @param {Object} thumbs */ proto.setThumbSize = function( thumb, thumbs ) { // set thumbnail size if not already assigned if ( thumb.width !== thumbs.width || thumb.height !== thumbs.height || thumb.gutter !== thumbs.gutter ) { // store sizes thumb.width = thumbs.width; thumb.height = thumbs.height; thumb.gutter = thumbs.gutter; // set thumbnail size thumb.style.width = thumbs.width + 'px'; thumb.style.height = thumbs.height + 'px'; } }; /** * Handle close gesture * @param {Object} cell */ proto.willClose = function( cell ) { var opacity = this.DOM.overlay.style.opacity, canClose = this.gesture.canClose, gestureType = this.gesture.type, gestureClose = this.gesture.closeBy, pinchToClose = gestureType === 'panZoom' || gestureClose === 'panZoom', dragYToClose = gestureType === 'panY' || gestureClose === 'panY'; // check if the lightbox can be closed with a pinch if ( cell.position.s > 1.1 && typeof canClose === 'undefined' ) { this.gesture.canClose = false; } else if ( cell.position.s < 1 && typeof canClose === 'undefined' ) { this.gesture.canClose = true; } // apply opacity if the user try to close the lightbox if ( this.options.pinchToClose && pinchToClose && this.gesture.canClose ) { opacity = cell.position.s; this.gesture.closeBy = 'panZoom'; } else if ( dragYToClose ) { opacity = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 ); this.gesture.closeBy = 'panY'; } else if ( opacity && opacity < 1 ) { opacity = 1; this.gesture.closeBy = false; } // normalize opacity value opacity = !opacity ? 1 : Math.max( 0, Math.min( 1, opacity ) ); // hide ui if the user try to close the lightbox // only if the opacity factor is enough to close (0.8) var method = opacity <= 0.8 || !opacity ? 'add' : 'remove'; utils[method + 'Class']( this.DOM.holder, this.pre + '-will-close' ); this.DOM.overlay.style.opacity = opacity; }; // ----- Slide Navigation ----- // /** * Go to prev slide * Throttle function to reduce sliding speed */ proto.prev = utils.throttle( function() { if ( !this.gesture.move ) { this.slideTo( this.slides.index - 1 * this.isRTL() ); } }, 120 ); /** * Go to next slide * Throttle function to reduce sliding speed */ proto.next = utils.throttle( function() { if ( !this.gesture.move ) { this.slideTo( this.slides.index + 1 * this.isRTL() ); } }, 120 ); /** * Slide to gallery index * @param {number} to * @param {boolean} slideShow */ proto.slideTo = function( to, slideShow ) { var slides = this.slides, gallery = this.gallery, holder = this.DOM.slider, RTL = this.isRTL(), length = gallery.initialLength, moduloTo = utils.modulo( length, to ), moduloFrom = utils.modulo( length, gallery.index ), slideBy = moduloTo - moduloFrom, fromEnds = length - Math.abs( slideBy ); // if no loop and outside gallery ends if ( !this.states.loop && ( to < 0 || to > this.gallery.initialLength - 1 ) ) { return; } // if we are closed to a slider end, then go directly if ( this.states.loop && fromEnds < 3 && fromEnds * 2 < length ) { slideBy = slideBy < 0 ? fromEnds : -fromEnds; } // if same slide but different media index // necessary in loop and thumb click or slideTo triggered manually if ( moduloTo === to ) { to = slides.index + slideBy; } // number of slides to animate slideBy = to - slides.index; // if same slide selected if ( !slideBy ) { return; } // unset zoom of current cell if ( this.states.zoom ) { this.zoom(); } // pause video this.pauseVideo(); // remove share tooltip this.share(); // if not triggered by the slideshow if ( !slideShow ) { this.stopSlideShow(); } // set new index slides.index = to; // prepare slider var slider = this.slider; // if the slide will move more than 2 slides // fake animation if ( Math.abs( slideBy ) > 2 ) { // hide slider with css utils.addClass( holder, this.pre + '-hide' ); // set slider position to selected media this.setSliderPosition( slider, slides ); this.setSlidesPositions( slides ); // set slider position 2 slide before/after selected media var moveBy = RTL * slides.width * Math.min( 2, Math.abs( slideBy ) ) * Math.abs( slideBy ) / slideBy; slider.position.x = slider.attraction.x = slider.position.x + moveBy; utils.translate( holder, slider.position.x, 0 ); // trigger reflow for css transitions. holder.getClientRects(); } // update media info with new index this.updateMediaInfo(); // reveal slider with css transition utils.removeClass( holder, this.pre + '-hide' ); slider.startAnimate(); slider.releaseDrag(); slider.animateTo({ x : - RTL * to * slides.width, y : 0, s : undefined }); }; /** * Handle keydown event * @param {Object} event */ proto.keyDown = function( event ) { var key = event.keyCode, opt = this.options; if ( opt.prevNextKey ) { if ( key === 37 ) { this.prev( event ); } else if ( key === 39 ) { this.next( event ); } } if ( key === 27 && opt.escapeToClose ) { this.close(); } // disable scroll on keydown // Space: 32, Page Up: 33, Page Down: 34, End: 35, Home: 36, Up arrow: 38, Down arrow: 40 if ( !opt.mouseWheel && [32, 33, 34, 35, 36, 38, 40].indexOf( key ) > -1 ) { event.preventDefault(); return false; } }; // ----- Zoom functionality ----- // /** * Handle zoom on icon click */ proto.zoom = function() { this.zoomTo(); }; /** * Zoom to a position (x & y) and scale factor on image * @param {number} x * @param {number} y * @param {number} scale */ proto.zoomTo = function( x, y, scale ) { if ( !this.isSliderSettle() || ( !this.isZoomable() && scale > 1 ) ) { return; } this.gesture.closeBy = false; var media = this.getMedia(); scale = scale ? scale : this.states.zoom ? 1 : media.dom.size.scale; var cell = this.getCell(), bound = this.mediaViewport( scale ), centX = x ? x - this.slider.width * 0.5 : 0, centY = y ? y - this.slider.height * 0.5 : 0, PosX = scale > 1 ? Math.ceil( centX + ( cell.position.x - centX ) * ( scale / cell.position.s ) ) : 0, PosY = scale > 1 ? Math.ceil( centY + ( cell.position.y - centY ) * ( scale / cell.position.s ) ) : 0; cell.startAnimate(); cell.releaseDrag(); cell.animateTo({ x : PosX > bound.left ? bound.left : PosX < bound.right ? bound.right : PosX, y : PosY > bound.bottom ? bound.bottom : PosY < bound.top ? bound.top : PosY, s : scale }); this.updateZoom( scale ); }; /** * Update zoom state when zoomTo * @param {number} scale */ proto.updateZoom = function( scale ) { this.states.zoom = scale > 1 ? true : false; utils[this.states.zoom ? 'addClass' : 'removeClass']( this.DOM.holder, this.pre + '-panzoom' ); }; /** * Destroy method */ proto.destroy = function() { // if no instance if ( !this.GUID ) { return; } // Close only if destroy method is called when opened if ( this.states.open ) { this.close(); } // setup sources var selectors = this.options.mediaSelector, sources = ''; // get all media attached to Modulobox try { sources = document.querySelectorAll( selectors ); } catch (error) {} // remove all events attached to media for ( var i = 0, l = sources.length; i < l; i++ ) { var source = sources[i]; if ( source.mobxListener ) { source.removeEventListener( 'click', source.mobxListener, false ); } } // unbind all events this.bindEvents( false ); // reset slider animation this.slider.resetAnimate(); // reset cells animations for ( i = 0; i < this.slides.length; i++ ) { this.cells[i].resetAnimate(); } // reset thumbs animation if ( this.thumbs ) { this.thumbs.resetAnimate(); } // remove main holders this.DOM.holder.parentNode.removeChild( this.DOM.holder ); this.DOM.comment.parentNode.removeChild( this.DOM.comment ); // delete instance; delete instances[this.GUID]; delete this.GUID; }; /** * jQuery plugin */ if ( typeof jQuery !== 'undefined' ) { ( function( $ ) { $.ModuloBox = function( options ) { return new ModuloBox( options ); }; })( jQuery ); } return ModuloBox; }));