PageSourceSearch

https://saltwellfarmkitchen.com/wp-content/plugins/grandrestaurant-elementor/assets/js/modulobox.js

js saltwellfarmkitchen.com collected 2026-09-28 09:34:22 UTC 154,547 bytes, 6,313 lines download raw bytes

1/**
2 * ModuloBox PACKAGED v1.3.0
3 * Touch & responsive multimedia Lightbox
4 *
5 * @author Themeone [https://theme-one.com/]
6 * Copyright © 2016 All Rights Reserved.
7 */
8
9/* global navigator, location, window, document, screen, history, setTimeout, clearTimeout, requestAnimationFrame, cancelAnimationFrame, XMLHttpRequest, Image */
10/* global define, module, require, jQuery, MediaElementPlayer */
11
12/**
13 * requestAnimationFrame polyfill
14 * Modified version of Paul Irish (https://gist.github.com/paulirish/1579671)
15 * v1.0.0
16 */
17( function() {
18
19	var win = window,
20		lastTime = 0;
21
22	// get rAF, prefixed, if present
23	win.requestAnimationFrame = win.requestAnimationFrame || win.webkitRequestAnimationFrame;
24
25	// fallback to setTimeout
26	if ( !win.requestAnimationFrame ) {
27
28		win.requestAnimationFrame = function( callback ) {
29
30			var currTime   = new Date().getTime(),
31				timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ),
32				id         = setTimeout( callback, timeToCall );
33
34			lastTime = currTime + timeToCall;
35
36			return id;
37
38		};
39
40	}
41
42	// fallback to setTimeout
43	if ( !win.cancelAnimationFrame ) {
44
45		win.cancelAnimationFrame = function( id ) {
46			clearTimeout( id );
47		};
48
49	}
50
51}());
52
53/**
54 * Themeone Utils
55 * Utilities
56 * v1.0.0
57 */
58( function( root, factory ) {
59
60	if ( typeof define === 'function' && define.amd ) {
61
62		// AMD
63		define(
64			'themeone-utils/utils',
65			factory
66		);
67
68	} else if ( typeof module === 'object' && module.exports ) {
69
70		// Node, CommonJS-like
71		module.exports = factory();
72
73	} else {
74
75		// Browser globals (root is window)
76		root.ThemeoneUtils = factory();
77
78	}
79
80}( this, function() {
81
82	"use strict";
83
84	var utils = {};
85	var Console = window.console;
86
87	/**
88	 * Output console error
89	 * @param {string} message
90	 */
91	utils.error = function( message ) {
92
93		if ( typeof Console !== 'undefined' ) {
94
95			Console.error( message );
96
97		}
98
99	};
100
101	/**
102	 * Extend an Object
103	 * @param {Object} options
104	 * @param {Object} defaults
105	 * @return {Object} defaults
106	 */
107	utils.extend = function( options, defaults ) {
108
109		if ( options ) {
110
111			if ( typeof options !== 'object' ) {
112
113				this.error( 'Custom options must be an object' );
114
115			} else {
116
117				for ( var prop in defaults ) {
118
119					if ( defaults.hasOwnProperty( prop ) && options.hasOwnProperty( prop ) ) {
120						defaults[prop] = options[prop];
121					}
122
123				}
124
125			}
126
127		}
128
129		return defaults;
130
131	};
132
133	/**
134	 * Find style property (with vendor prefix)
135	 * @param {string} prop
136	 * @return {string} prefixedProp
137	 */
138	utils.prop = function( prop ) {
139
140		var el = this.createEl(),
141			prefixes = ['', 'Webkit', 'Moz', 'ms', 'O'];
142
143		for ( var p = 0, pl = prefixes.length; p < pl; p++ ) {
144
145			var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt( 0 ).toUpperCase() + prop.slice( 1 ) : prop;
146
147			if ( el.style[prefixedProp] !== undefined ) {
148				return prefixedProp;
149			}
150
151		}
152
153		return '';
154
155	};
156
157	/**
158	 * Clone object properties
159	 * @param {Object} obj
160	 * @return {Object} copy
161	 */
162	utils.cloneObject = function( obj ) {
163
164		var copy = {};
165
166		for ( var attr in obj ) {
167
168			if ( obj.hasOwnProperty( attr ) ) {
169				copy[attr] = obj[attr];
170			}
171
172		}
173
174		return copy;
175
176	};
177
178	/**
179	 * Create DOM element
180	 * @param {string} tag
181	 * @param {string} classes
182	 * @return {Object} el
183	 */
184	utils.createEl = function( tag, classes ) {
185
186		var el = document.createElement( tag || 'div' );
187
188		if ( classes ) {
189			el.className = classes;
190		}
191
192		return el;
193
194	};
195
196	/**
197	 * Camel case string
198	 * @param {string} string
199	 * @return {string} string
200	 */
201	utils.camelize = function( string ) {
202
203		return string.replace( /-([a-z])/g, function( g ) {
204			return g[1].toUpperCase();
205		});
206
207	};
208
209	/**
210	 * Add/remove event listeners
211	 * @param {Object} _this
212	 * @param {Object} el
213	 * @param {Array} event
214	 * @param {string} fn
215	 * @param {boolean} isBind
216	 */
217	utils.handleEvents = function( _this, el, event, fn, isBind ) {
218
219		if ( typeof this.event_handlers !== 'object' ) {
220			this.event_handlers = {};
221		}
222
223		// register handlers for later (to remove)
224		if ( !this.event_handlers[fn] ) {
225			this.event_handlers[fn] = _this[fn].bind( _this );
226		}
227
228		// set bind method
229		isBind = isBind === undefined ? true : !!isBind;
230		var bindMethod = isBind ? 'addEventListener' : 'removeEventListener';
231
232		// loop through each event
233		event.forEach( function( ev ) {
234			el[bindMethod]( ev, this.event_handlers[fn], false );
235		}.bind( this ) );
236
237	};
238
239	/**
240	 * Emits events via EvEmitter and jQuery events
241	 * @param {Object} _this
242	 * @param {string} namespace - namespace of event
243	 * @param {string} type - name of event
244	 * @param {Event} event - original event
245	 * @param {Array} args - extra arguments
246	 */
247	utils.dispatchEvent = function( _this, namespace, type, event, args ) {
248
249		// add Namespace for the event
250		type += namespace ? '.' + namespace : '';
251		// add original event to arguments
252		var emitArgs = event ? [event].concat( args ) : [args];
253
254		// trigger vanilla JS event
255		_this.emitEvent( type, emitArgs );
256
257	};
258
259	/**
260	 * Function for applying a debounce effect to a function call.
261	 * @function
262	 * @param {Function} func - Function to be called at end of timeout.
263	 * @param {number} delay - Time in ms to delay the call of `func`.
264	 * @returns function
265	 */
266	utils.throttle = function( func, delay ) {
267
268		var timestamp = null,
269			limit = delay;
270
271		return function() {
272
273			var self = this,
274				args = arguments,
275				now = Date.now();
276
277			if ( !timestamp || now - timestamp >= limit ) {
278
279				timestamp = now;
280				func.apply( self, args );
281
282			}
283
284		};
285
286	};
287
288	/**
289	 * Modulo calculation
290	 * @param {number} length
291	 * @param {number} index
292	 * @return {number}
293	 */
294	utils.modulo = function( length, index ) {
295
296		return ( length + ( index % length ) ) % length;
297
298	};
299
300	/**
301	 * Regex classname from string
302	 * @param {string} className
303	 * @return {RegExp}
304	 */
305	utils.classReg = function( className ) {
306
307		return new RegExp( '(^|\\s+)' + className + '(\\s+|$)' );
308
309	};
310
311	/**
312	 * Check if element has class name
313	 * @param {Object} el
314	 * @param {string} className
315	 * @return {boolean}
316	 */
317	utils.hasClass = function( el, className ) {
318
319		return !!el.className.match( this.classReg( className ) );
320
321	};
322
323	/**
324	 * Add a class name to an element
325	 * @param {Object} el
326	 * @param {string} className
327	 */
328	utils.addClass = function( el, className ) {
329
330		if ( !this.hasClass( el, className ) ) {
331			el.className += ( el.className ? ' ' : '' )  + className;
332		}
333
334	};
335
336	/**
337	 * Remove a class name to an element
338	 * @param {Object} el
339	 * @param {string} className
340	 */
341	utils.removeClass = function( el, className ) {
342
343		if ( this.hasClass( el, className ) ) {
344			el.className = el.className.replace( this.classReg( className ), ' ' ).replace( /\s+$/, '' );
345		}
346
347	};
348
349	/**
350	 * Translate an element
351	 * @param {Object} el
352	 * @param {number} x
353	 * @param {number} y
354	 * @param {number} s
355	 */
356	utils.translate = function( el, x, y, s ) {
357
358		var scale = s ? ' scale(' + s + ',' + s + ')' : '';
359
360		el.style[this.browser.trans] = ( this.browser.gpu ) ?
361			'translate3d(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px, 0)' + scale :
362			'translate(' + ( x || 0 ) + 'px, ' + ( y || 0 ) + 'px)' + scale;
363
364	};
365
366	/*
367	 * Browser features detection
368	 */
369	utils.browser = {
370		trans : utils.prop( 'transform' ),
371		gpu   : utils.prop( 'perspective' ) ? true : false
372	};
373
374	return utils;
375
376}));
377
378/**
379 * EvEmitter (Emit event)
380 * Modified version of David Desandro (https://github.com/desandro/EventEmitter)
381 * v1.0.0
382 */
383( function( root, factory ) {
384
385	if ( typeof define === 'function' && define.amd ) {
386
387		// AMD
388		define(
389			'themeone-event/event',
390			factory
391		);
392
393	} else if ( typeof module === 'object' && module.exports ) {
394
395		// Node, CommonJS-like
396		module.exports = factory();
397
398	} else {
399
400		// Browser globals (root is window)
401		root.ThemeoneEvent = factory();
402
403	}
404
405}( typeof window !== 'undefined' ? window : this, function() {
406
407	"use strict";
408
409	var EvEmitter = function() {},
410		proto = EvEmitter.prototype;
411
412	/**
413	 * Bind on event
414	 * @param {string} eventName
415	 * @param {Object} listener
416	 * @return {Object} this
417	 */
418	proto.on = function( eventName, listener ) {
419
420		if ( !eventName || !listener ) {
421			return null;
422		}
423
424		// set events hash
425		var events = this._events = this._events || {};
426		// set listeners array
427		var listeners = events[eventName] = events[eventName] || [];
428
429		// only add once
430		if ( listeners.indexOf( listener ) === -1 ) {
431			listeners.push( listener );
432		}
433
434		return this;
435
436	};
437
438	/**
439	 * Unbind event event
440	 * @param {string} eventName
441	 * @param {Object} listener
442	 * @return {Object} this
443	 */
444	proto.off = function( eventName, listener ) {
445
446		var listeners = this._events && this._events[eventName];
447
448		if ( !listeners || !listeners.length ) {
449			return null;
450		}
451
452		var index = listeners.indexOf( listener );
453
454		if ( index !== -1 ) {
455			listeners.splice( index, 1 );
456		}
457
458		return this;
459
460	};
461
462	/**
463	 * Emit an event
464	 * @param {string} eventName
465	 * @param {Object} args
466	 * @return {Object} this
467	 */
468	proto.emitEvent = function( eventName, args ) {
469
470		var listeners = this._events && this._events[eventName];
471		if ( !listeners || !listeners.length ) {
472			return null;
473		}
474
475		var i = 0,
476			listener = listeners[i];
477			args = args || [];
478
479		// once stuff
480		var onceListeners = this._onceEvents && this._onceEvents[eventName];
481
482		while ( listener ) {
483
484			var isOnce = onceListeners && onceListeners[listener];
485
486			if ( isOnce ) {
487
488				// remove before trigger to prevent recursion
489				this.off( eventName, listener );
490				// unset once flag
491				delete onceListeners[listener];
492
493			}
494
495			// trigger listener
496			listener.apply( this, args );
497
498			// get next listener
499			i += isOnce ? 0 : 1;
500			listener = listeners[i];
501
502		}
503
504		return this;
505
506	};
507
508	return EvEmitter;
509
510}));
511
512/**
513 * Animate
514 * RAF animations
515 * v1.0.0
516 */
517( function( root, factory ) {
518
519	if ( typeof define === 'function' && define.amd ) {
520
521		// AMD
522		define(
523			'themeone-animate/animate',
524			['themeone-utils/utils',
525			'themeone-event/event'],
526			factory
527		);
528
529	} else if ( typeof module === 'object' && module.exports ) {
530
531		// Node, CommonJS-like
532		module.exports = factory(
533			require( 'themeone-utils' ),
534			require( 'themeone-event' )
535		);
536
537	} else {
538
539		// Browser globals (root is window)
540		root.ThemeoneAnimate = factory(
541			root.ThemeoneUtils,
542			root.ThemeoneEvent
543		);
544
545	}
546
547}( this, function( utils, EvEmitter ) {
548
549	'use strict';
550
551	/**
552	 * Animate
553	 * @param {Object} element
554	 * @param {Object} positions
555	 * @param {Object} friction
556	 * @param {Object} attraction
557	 */
558	var Animate = function( element, positions, friction, attraction ) {
559
560		this.element  = element;
561		this.defaults = positions;
562		this.forces   = {
563			friction   : friction || 0.28,
564			attraction : attraction || 0.028
565		};
566
567		this.resetAnimate();
568
569	};
570
571	var proto = Animate.prototype = Object.create( EvEmitter.prototype );
572
573	/**
574	 * Update animation on drag
575	 * @param {Object} obj
576	 */
577	proto.updateDrag = function( obj ) {
578
579		this.move = true;
580		this.drag = obj;
581
582	};
583
584	/**
585	 * Release drag
586	 */
587	proto.releaseDrag = function() {
588
589		this.move = false;
590
591	};
592
593	/**
594	 * Animate to a specific position
595	 * @param {Object} obj
596	 */
597	proto.animateTo = function( obj ) {
598
599		this.attraction = obj;
600
601	};
602
603	/**
604	 * Start render animation
605	 */
606	proto.startAnimate = function() {
607
608		this.move = true;
609		this.settle = false;
610		this.restingFrames = 0;
611
612		if ( !this.RAF ) {
613			this.animate();
614		}
615
616	};
617
618	/**
619	 * Stop animation
620	 */
621	proto.stopAnimate = function() {
622
623		this.move = false;
624		this.restingFrames = 0;
625
626		if ( this.RAF ) {
627			cancelAnimationFrame( this.RAF );
628			this.RAF = false;
629		}
630
631		this.start = utils.cloneObject( this.position );
632		this.velocity = {
633			x : 0,
634			y : 0,
635			s : 0
636		};
637
638	};
639
640	/**
641	 * Reset animation
642	 */
643	proto.resetAnimate = function() {
644
645		this.stopAnimate();
646
647		this.settle = true;
648		this.drag = utils.cloneObject( this.defaults );
649		this.start = utils.cloneObject( this.defaults );
650		this.resting = utils.cloneObject( this.defaults );
651		this.position = utils.cloneObject( this.defaults );
652		this.attraction = utils.cloneObject( this.defaults );
653
654	};
655
656	/**
657	 * Handle animation rendering
658	 */
659	proto.animate = function() {
660
661		// animation loop function
662		var loop = ( function() {
663
664			if ( typeof this.position !== 'undefined' ) {
665
666				// get clone previous values
667				var previous = utils.cloneObject( this.position );
668
669				// set main forces
670				this.applyDragForce();
671				this.applyAttractionForce();
672
673				// dispatch render event (before physic otherwise calculation will be wrong)
674				utils.dispatchEvent( this, 'toanimate', 'render', this );
675
676				// apply physics
677				this.integratePhysics();
678				this.getRestingPosition();
679
680				// render positions
681				this.render( 100 );
682
683				// loop
684				this.RAF = requestAnimationFrame( loop );
685
686				// cancel RAF if no animations
687				this.checkSettle( previous );
688
689			}
690
691		}).bind( this );
692
693		// start animation loop
694		this.RAF = requestAnimationFrame( loop );
695
696	};
697
698	/**
699	 * Simulation physic velocity
700	 */
701	proto.integratePhysics = function() {
702
703		for ( var k in this.position ) {
704
705			if ( typeof this.position[k] !== 'undefined' ) {
706
707				this.position[k] += this.velocity[k];
708				this.position[k] = ( k === 's' ) ? Math.max( 0.1, this.position[k] ) : this.position[k];
709				this.velocity[k] *= this.getFrictionFactor();
710
711			}
712
713		}
714
715	};
716
717	/**
718	 * Simulation physic friction
719	 */
720	proto.applyDragForce = function() {
721
722		if ( this.move ) {
723
724			for ( var k in this.drag ) {
725
726				if ( typeof this.drag[k] !== 'undefined' ) {
727
728					var dragVelocity = this.drag[k] - this.position[k];
729					var dragForce = dragVelocity - this.velocity[k];
730					this.applyForce( k, dragForce );
731
732				}
733
734			}
735
736		}
737
738	};
739
740	/**
741	 * Simulation physic attraction
742	 */
743	proto.applyAttractionForce = function() {
744
745		if ( !this.move ) {
746
747			for ( var k in this.attraction ) {
748
749				if ( typeof this.attraction[k] !== 'undefined' ) {
750
751					var distance = this.attraction[k] - this.position[k];
752					var force = distance * this.forces.attraction;
753					this.applyForce( k, force );
754
755				}
756
757			}
758
759		}
760
761	};
762
763	/**
764	 * Calculate estimated resting position from physic
765	 */
766	proto.getRestingPosition = function() {
767
768		for ( var k in this.position ) {
769
770			if ( typeof this.position[k] !== 'undefined' ) {
771				this.resting[k] = this.position[k] + this.velocity[k] / ( 1 - this.getFrictionFactor() );
772			}
773
774		}
775
776	};
777
778	/**
779	 * Apply an attraction force
780	 * @param {string} direction
781	 * @param {number} force
782	 */
783	proto.applyForce = function( direction, force ) {
784
785		this.velocity[direction] += force;
786
787	};
788
789	/**
790	 * Apply a friction factor
791	 * @return {number}
792	 */
793	proto.getFrictionFactor = function() {
794
795		return 1 - this.forces.friction;
796
797	};
798
799	/**
800	 * Round value to correctly calculate if animate is settle or not
801	 * @param {number} values
802	 * @param {number} round
803	 */
804	proto.roundValues = function( values, round ) {
805
806		for ( var k in values ) {
807
808			if ( typeof values[k] !== 'undefined' ) {
809
810				round = k === 's' ? round * 100 : round;
811				values[k] = Math.round( values[k] * round ) / round;
812
813			}
814
815		}
816
817	};
818
819	/**
820	 * Check if the current animated object is settled
821	 * @param {Object} previous
822	 */
823	proto.checkSettle = function( previous ) {
824
825		// keep track of frames where x hasn't moved
826		if ( !this.move ) {
827
828			var count = 0;
829
830			for ( var k in this.position ) {
831
832				if ( typeof this.position[k] !== 'undefined' ) {
833
834					var round = k === 's' ? 10000 : 100;
835
836					if ( Math.round( this.position[k] * round ) === Math.round( previous[k] * round ) ) {
837
838						count++;
839						if ( count === Object.keys( this.position ).length ) {
840							this.restingFrames++;
841						}
842
843					}
844
845				}
846
847			}
848
849		}
850
851		// stop RAF animation if position didn't change during 3 frames (60fps)
852		if ( this.restingFrames > 2 ) {
853
854			this.stopAnimate();
855			this.render( this.position.s > 1 ? 10 : 1 );
856			this.settle = true;
857
858			// dispatch settle only if moved
859			if ( JSON.stringify( this.start ) !== JSON.stringify( this.position ) ) {
860				utils.dispatchEvent( this, 'toanimate', 'settle', this );
861			}
862
863		}
864
865	};
866
867	/**
868	 * Render animation
869	 * @param {number} round
870	 */
871	proto.render = function( round ) {
872
873		// round new position values
874		this.roundValues( this.position, round );
875
876		// translate
877		utils.translate(
878			this.element,
879			this.position.x,
880			this.position.y,
881			this.position.s
882		);
883
884	};
885
886	return Animate;
887
888}));
889
890/**
891 * ModuloBox
892 * Core Plugin
893 * v1.3.0
894 */
895( function( root, factory ) {
896
897	if ( typeof define === 'function' && define.amd ) {
898
899		// AMD
900		define(
901			['themeone-utils/utils',
902			'themeone-event/event',
903			'themeone-animate/animate'],
904			factory
905		);
906
907	} else if ( typeof exports === 'object' && module.exports ) {
908
909		// Node, CommonJS-like
910		module.exports = factory(
911			require( 'themeone-utils' ),
912			require( 'themeone-event' ),
913			require( 'themeone-animate' )
914		);
915
916	} else {
917
918		// Browser globals (root is window)
919		root.ModuloBox = factory(
920			root.ThemeoneUtils,
921			root.ThemeoneEvent,
922			root.ThemeoneAnimate
923		);
924
925	}
926
927}( this, function( utils, EvEmitter, Animate ) {
928
929	'use strict';
930
931	// Modulobox version
932	var version = '1.3.0';
933
934	// Globally unique identifiers
935	var GUID = 0;
936
937	// Internal store of all plugin intances
938	var instances = {};
939
940	// Internal cache
941	var expando = 'mobx' + ( version + Math.random() ).replace( /\D/g, '' );
942	var cache   = { uid : 0 };
943
944	// Default options
945	var defaults = {
946		// Setup
947		mediaSelector      : '.mobx', // Media class selector used to fetch media in document
948		// Behaviour
949		threshold          : 5,       // Dragging doesn't start until 5px moved (not taken into account when pinched with 2 fingers)
950		attraction         : {        // Attracts the position of the slider to the selected cell. Higher value makes the slider move faster. Lower value makes it move slower.
951			slider : 0.055, // From 0 to 1
952			slide  : 0.018, // From 0 to 1
953			thumbs : 0.016  // From 0 to 1
954		},
955		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
956			slider : 0.62,  // From 0 to 1
957			slide  : 0.18,  // From 0 to 1
958			thumbs : 0.22   // From 0 to 1
959		},
960		rightToLeft        : false,   // Enable right to left layout
961		loop               : 3,       // From how much items infinite loop start (0: no loop, 1: always loop, 2: loop if 2 items at least, etc...)
962		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
963		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)
964		timeToIdle         : 4000,    // Hide controls when an idle state exceed 4000ms (0 - always keep controls visible)
965		history            : false,   // Enable/disable history in browser (deeplink) when opening/navigating/closing a gallery
966		mouseWheel         : true,    // Enable/disable mouseWheel (up/down/space keys) to scroll page
967		contextMenu        : true,    // Enable/disable contextmenu on right click (on image)
968		scrollBar          : true,    // Show/hide browser scrollbar
969		fadeIfSettle       : false,   // Show media only if the slider is settled (improves performance especially for iframe/html)
970		// User Interface
971		controls           : ['close'], // 'zoom', 'play', 'fullScreen', 'download', 'share', 'close'
972		prevNext           : true,    // Show/hide prev/next navigation buttons
973		prevNextTouch      : false,   // Show/hide prev/next navigation buttons on small touch capable devices
974		counterMessage     : '[index] / [total]', // Message used in the item counter. If empty, no counter will be displayed
975		caption            : true,    // Show/hide caption under media (globally)
976		autoCaption        : false,   // Generate captions from alt and/or title attributes if data-title and/or data-desc missing
977		captionSmallDevice : true,    // Show/hide caption under media on small browser width like mobile devices
978		thumbnails         : true,    // Enable/disable thumbnail
979		thumbnailsNav      : 'basic', // Navigation type for the thumbnail (basic or centered)
980		thumbnailSizes     : {        // Set thumbnails size for different browser widths (unlimited)
981			1920 : {          // browser width (px)
982				width  : 110, // thumbnail width (px) - 0 in width will hide thumbnails
983				height : 80,  // thumbnail height(px) - 0 in height will hide thumbnails
984				gutter : 10   // gutter width (px) between thumbnails
985			},
986			1280 : {
987				width  : 90,
988				height : 65,
989				gutter : 10
990			},
991			680 : {
992				width  : 70,
993				height : 50,
994				gutter : 8
995			},
996			480 : {
997				width  : 60,
998				height : 44,
999				gutter : 5
1000			}
1001		},
1002		spacing            : 0.1,     // Space in percent between each slide. For example, 0.1 will render as a 10% of sliding viewport width
1003		smartResize        : true,    // Allow images to overflow on top bar and/or caption on small devices only if image can fill the full screen height
1004		overflow           : false,   // Allow images to overflow on top bar and/or caption (if enable, smart resize will be ignored)
1005		loadError          : 'Sorry, an error occured while loading the content...',
1006		noContent          : 'Sorry, no content was found!',
1007		// functions
1008		prevNextKey        : true,    // Press keyboard left/right to navigate
1009		scrollToNav        : false,   // Scroll with a mousewheel to navigate next/prev (disabled if scrollToZoom set to true)
1010		scrollSensitivity  : 15,      // Threshold in 'px' from when a methods is called (Useful to fine tune touchpad and free mousewheel sensitivity)
1011		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...)
1012		minZoom            : 1.2,     // Min zoom factor required to enable Zoom feature on click or pinch
1013		maxZoom            : 4,       // Max zoom factor allowed when pinched/scrolled or if the zoomTo auto value is superior to maxZoom
1014		doubleTapToZoom    : true,    // double Tap/click to zoom on image
1015		scrollToZoom       : false,   // Scroll with a mousewheel to zoom in/out
1016		pinchToZoom        : true,    // Pinch in with 2 fingers to zoom on image
1017		escapeToClose      : true,    // Esc keyboard key to close the lightbox
1018		scrollToClose      : false,   // Scroll with a mousewheel to close the lightbox (disabled if scrollToNav and/or scrollToZoom set to true)
1019		pinchToClose       : true,    // Pinch out with 2 fingers to close the lightbox
1020		dragToClose        : true,    // Drag vertically to close the lightbox
1021		tapToClose         : true,    // Tap/click outside the image/media to close the lightbox
1022		shareButtons       : ['facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit'], // 'facebook', 'googleplus', 'twitter', 'pinterest', 'linkedin', 'reddit', 'stumbleupon', 'tumblr', 'blogger', 'buffer', 'digg', 'evernote'
1023		shareText          : 'Share on', // text displayed above social sharing buttons
1024		sharedUrl          : 'deeplink', // url used to share the media ('page', deeplink', 'media')
1025		slideShowInterval  : 4000,    // Time interval, in milliseconds, between slide changes in slideshow mode
1026		slideShowAutoPlay  : false,   // Automatically start slideShow mode on opening
1027		slideShowAutoStop  : false,   // Stop slideshow when the last item is reached (only if slider loop)
1028		countTimer         : true,    // Show a circular countdown timer next to the counter message when autoplay (slideshow)
1029		countTimerBg       : 'rgba(255,255,255,0.25)', // Background color of the circular timer stroke
1030		countTimerColor    : 'rgba(255,255,255,0.75)', // Color of the circular timer stroke
1031		mediaelement       : false,   // Play HTML5 videos with mediaelement (jQuery library required & mediaelement.js script)
1032		videoRatio         : 16 / 9,  // Video aspect ratio
1033		videoMaxWidth      : 1180,    // Video max width allowed in a slide
1034		videoAutoPlay      : false,   // Autoplay video on opening
1035		videoThumbnail     : false    // Automatically fetches poster/thumbnails of iframe videos if missing
1036	};
1037
1038	/**
1039	 * ModuloBox
1040	 * @constructor
1041	 * @param {Object} options
1042	 */
1043	var ModuloBox = function( options ) {
1044
1045		// extend defaults with user-set options
1046		this.options = utils.extend( options, defaults );
1047		// set main plugin variables
1048		this.setVar();
1049
1050	};
1051
1052	// Set plugin prototype (and add EvEmitter proto)
1053	var proto = ModuloBox.prototype = Object.create( EvEmitter.prototype );
1054
1055	/**
1056	 * Initialize ModuloBox
1057	 * Create main instance & DOM
1058	 */
1059	proto.init = function() {
1060
1061		// prevent to initialize twice
1062		if ( this.GUID ) {
1063			return instances[this.GUID];
1064		}
1065
1066		// set instance
1067		this.GUID = ++GUID;
1068		instances[this.GUID] = this;
1069
1070		this.createDOM();
1071		this.setAnimation();
1072		this.getGalleries();
1073		this.openFromQuery();
1074
1075	};
1076
1077	/**
1078	 * Set main plugin variable
1079	 */
1080	proto.setVar = function() {
1081
1082		var win = window,
1083			doc = document,
1084			nav = navigator;
1085
1086		// prefix used for class names
1087		this.pre = 'mobx';
1088
1089		// set main var
1090		this.gesture  = {};
1091		this.buttons  = {};
1092		this.slider   = {};
1093		this.slides   = {};
1094		this.cells    = {};
1095		this.states   = {};
1096		this.pointers = [];
1097
1098		// set internal cache
1099		this.expando  = expando;
1100		this.cache    = cache;
1101
1102		// drag events
1103		this.dragEvents = this.detectPointerEvents();
1104
1105		// browser detection
1106		this.browser = {
1107			touchDevice : ( 'ontouchstart' in win ) || ( nav.maxTouchPoints > 0 ) || ( nav.msMaxTouchPoints > 0 ),
1108			pushState   : 'history' in win && 'pushState' in history,
1109			fullScreen  : this.detectFullScreen(),
1110			mouseWheel  : 'onwheel' in doc.createElement( 'div' ) ?
1111						'wheel' : doc.onmousewheel !== undefined ?
1112						'mousewheel' : 'DOMMouseScroll'
1113		};
1114
1115		// add social & video stream support
1116		this.iframeVideo = this.iframeVideo();
1117		this.socialMedia = this.socialMedia();
1118
1119	};
1120
1121	/**
1122	 * Detect pointer type browser support
1123	 * @return {Object}
1124	 */
1125	proto.detectPointerEvents = function() {
1126
1127		var nav = navigator;
1128
1129		// listen for W3C Pointer Events (IE11)
1130		if ( nav.pointerEnabled ) {
1131
1132			return {
1133				start : ['pointerdown'],
1134				move  : ['pointermove'],
1135				end   : ['pointerup', 'pointercancel']
1136			};
1137
1138		}
1139
1140		// listen for IE10 Pointer Events
1141		if ( nav.msPointerEnabled ) {
1142
1143			return {
1144				start : ['MSPointerDown'],
1145				move  : ['MSPointerMove'],
1146				end   : ['MSPointerUp', 'MSPointerCancel']
1147			};
1148
1149		}
1150
1151		// listen for both Mouse & Touch Events
1152		return {
1153			start : ['mousedown', 'touchstart'],
1154			move  : ['mousemove', 'touchmove'],
1155			end   : ['mouseup', 'mouseleave', 'touchend', 'touchcancel']
1156		};
1157
1158	};
1159
1160	/**
1161	 * Detect fullScreen browser support
1162	 * Remove fullScreen button if not supported
1163	 * @return {Object}
1164	 */
1165	proto.detectFullScreen = function() {
1166
1167		var fullScreen = ['fullscreenEnabled', 'webkitFullscreenEnabled', 'mozFullScreenEnabled', 'msFullscreenEnabled'];
1168
1169		for ( var i = 0, l = fullScreen.length; i < l; i++ ) {
1170
1171			if ( document[fullScreen[i]] ) {
1172
1173				return {
1174					element : ['fullscreenElement', 'webkitFullscreenElement', 'mozFullScreenElement', 'msFullscreenElement'][i],
1175					request : ['requestFullscreen', 'webkitRequestFullscreen', 'mozRequestFullScreen', 'msRequestFullscreen'][i],
1176					change  : ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'][i],
1177					exit    : ['exitFullscreen', 'webkitExitFullscreen', 'mozCancelFullScreen', 'msExitFullscreen'][i]
1178				};
1179
1180			}
1181
1182		}
1183
1184		var controls = this.options.controls,
1185			index = controls.indexOf( 'fullScreen' );
1186
1187		// if no fullscreen support remove fullscreen button if exists
1188		if ( index > -1 ) {
1189			controls.splice( index, 1 );
1190		}
1191
1192		return null;
1193
1194	};
1195
1196	/**
1197	 * Iframe stream video
1198	 * HTML5 window post message API
1199	 * [ID] => iframe video ID
1200	 * @return {Object}
1201	 */
1202	proto.iframeVideo = function() {
1203
1204		return {
1205			youtube : {
1206				reg    : /(?:www\.)?youtu\.?be(?:\.com)?\/?.*?(?:watch|embed)?(?:.*v=|v\/|watch%3Fv%3D|\/)([\w\-_]+)&?/i,
1207				url    : 'https://www.youtube.com/embed/[ID]?enablejsapi=1&rel=0&autoplay=1',
1208				share  : 'https://www.youtube.com/watch?v=[ID]',
1209				poster : 'https://img.youtube.com/vi/[ID]/maxresdefault.jpg',
1210				thumb  : 'https://img.youtube.com/vi/[ID]/default.jpg',
1211				play   : { event : 'command', func : 'playVideo' },
1212				pause  : { event : 'command', func : 'pauseVideo' }
1213			},
1214			vimeo : {
1215				reg    : /(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/(?:[^\/]*)\/videos\/|album\/(?:\d+)\/video\/|video\/)?(\d+)(?:[a-zA-Z0-9_\-]+)?/i,
1216				url    : 'https://player.vimeo.com/video/[ID]?autoplay=1&api=1',
1217				share  : 'https://vimeo.com/[ID]',
1218				poster : 'https://vimeo.com/api/v2/video/[ID].json',
1219				play   : { event : 'command', method : 'play' },
1220				pause  : { event : 'command', method : 'pause' }
1221			},
1222			dailymotion : {
1223				reg    : /(?:www\.)?(?:dailymotion\.com(?:\/embed)(?:\/video|\/hub)|dai\.ly)\/([0-9a-z]+)(?:[\-_0-9a-zA-Z]+#video=(?:[a-zA-Z0-9_\-]+))?/i,
1224				url    : 'https://dailymotion.com/embed/video/[ID]?autoplay=1&api=postMessage',
1225				share  : 'https://www.dailymotion.com/video/[ID]',
1226				poster : 'https://www.dailymotion.com/thumbnail/video/[ID]',
1227				thumb  : 'https://www.dailymotion.com/thumbnail/video/[ID]',
1228				play   : 'play',
1229				pause  : 'pause'
1230			},
1231			wistia : {
1232				reg    : /(?:www\.)?(?:wistia\.(?:com|net)|wi\.st)\/(?:(?:m|medias|projects)|embed\/(?:iframe|playlists))\/([a-zA-Z0-9_\-]+)/i,
1233				url    : 'https://fast.wistia.net/embed/iframe/[ID]?version=3&enablejsapi=1&html5=1&autoplay=1',
1234				share  : 'https://fast.wistia.net/embed/iframe/[ID]',
1235				poster : 'https://fast.wistia.com/oembed?url=https://home.wistia.com/medias/[ID].json',
1236				play   : { event : 'cmd', method : 'play' },
1237				pause  : { event : 'cmd', method : 'pause' }
1238			}
1239		};
1240
1241	};
1242
1243	/**
1244	 * Social media url
1245	 * [url]   => location deeplink or media url
1246	 * [text]  => text from title or null
1247	 * [image] => image or video poster or null
1248	 * @return {Object}
1249	 */
1250	proto.socialMedia = function() {
1251
1252		return {
1253			facebook    : 'https://www.facebook.com/sharer/sharer.php?u=[url]',
1254			googleplus  : 'https://plus.google.com/share?url=[url]',
1255			twitter     : 'https://twitter.com/intent/tweet?text=[text]&url=[url]',
1256			pinterest   : 'https://www.pinterest.com/pin/create/button/?url=[url]&media=[image]&description=[text]',
1257			linkedin    : 'https://www.linkedin.com/shareArticle?url=[url]&mini=true&title=[text]',
1258			reddit      : 'https://www.reddit.com/submit?url=[url]&title=[text]',
1259			stumbleupon : 'https://www.stumbleupon.com/badge?url=[url]&title=[text]',
1260			tumblr      : 'https://www.tumblr.com/share?v=3&u=[url]&t=[text]',
1261			blogger     : 'https://www.blogger.com/blog_this.pyra?t&u=[url]&n=[text]',
1262			buffer      : 'https://bufferapp.com/add?url=[url]title=[text]',
1263			digg        : 'https://digg.com/submit?url=[url]&title=[text]',
1264			evernote    : 'https://www.evernote.com/clip.action?url=[url]&title=[text]'
1265		};
1266
1267	};
1268
1269	// ----- Create & append Lightbox in document ----- //
1270
1271	/**
1272	 * Create DOM for the lightbox
1273	 */
1274	proto.createDOM = function() {
1275
1276		this.DOM = {};
1277
1278		var elements = [
1279			'holder',
1280			'overlay',
1281			'slider',
1282			'item',
1283			'item-inner',
1284			'ui',
1285			'top-bar',
1286			'bottom-bar',
1287			'share-tooltip',
1288			'counter',
1289			'caption',
1290			'caption-inner',
1291			'thumbs-holder',
1292			'thumbs-inner'
1293		];
1294
1295		for ( var i = 0; i < elements.length; i++ ) {
1296			this.DOM[utils.camelize( elements[i] )] = utils.createEl( 'div', this.pre + '-' + elements[i] );
1297		}
1298
1299		this.appendDOM( this.DOM );
1300
1301	};
1302
1303	/**
1304	 * Append lightbox DOM to body
1305	 * @param {{holder, item, itemInner, overlay, slider, ui, topBar}} dom
1306	 */
1307	proto.appendDOM = function( dom ) {
1308
1309		var opt = this.options;
1310
1311		// append main containers
1312		dom.holder.appendChild( dom.overlay );
1313		dom.holder.appendChild( dom.slider );
1314		dom.holder.appendChild( dom.ui );
1315
1316		// append slides & cells to slider
1317		for ( var i = 0; i < 5; i++ ) {
1318
1319			var slide = dom.item.cloneNode( true );
1320			slide.appendChild( dom.itemInner.cloneNode( true ) );
1321			dom.slider.appendChild( slide );
1322
1323			// manually assign dom to slides object
1324			this.slides[i] = slide;
1325
1326		}
1327
1328		// set slides length
1329		this.slides.length = dom.slider.children.length;
1330
1331		// create UI elements
1332		this.createUI( dom, opt );
1333
1334		// add lightbox attribute
1335		dom.holder.setAttribute( 'tabindex', -1 );
1336		dom.holder.setAttribute( 'aria-hidden', true );
1337
1338		// add plugin version in comment
1339		this.DOM.comment = document.createComment( ' ModuloBox (v' + version + ') by Themeone ' );
1340		document.body.appendChild( this.DOM.comment );
1341
1342		// Dispatch event before appending lightbox DOM (to add custom markup)
1343		utils.dispatchEvent( this, 'modulobox', 'beforeAppendDOM', dom );
1344
1345		// append lightbox to body
1346		document.body.appendChild( dom.holder );
1347
1348		// store top bar height for later
1349		dom.topBar.height = dom.topBar.clientHeight;
1350
1351	};
1352
1353	/**
1354	 * Create UI elements
1355	 * @param {{holder, ui, topBar, bottomBar, thumbsHolder, thumbsInner, counter, shareTooltip, caption, captionInner}} dom
1356	 * @param {Object} opt
1357	 */
1358	proto.createUI = function( dom, opt ) {
1359
1360		var shareIndex = opt.controls.indexOf( 'share' );
1361
1362		// append share tooltip
1363		if ( shareIndex > -1 ) {
1364
1365			var buttons = opt.shareButtons,
1366				i = buttons.length;
1367
1368			// check if each social button exists
1369			while ( i-- ) {
1370
1371				if ( !this.socialMedia.hasOwnProperty( buttons[i] ) ) {
1372					buttons.splice( i, 1 );
1373				}
1374
1375			}
1376
1377			// if there are social button(s) registered
1378			if ( buttons.length ) {
1379
1380				dom.ui.appendChild( dom.shareTooltip );
1381
1382				// add share text
1383				if ( opt.shareText ) {
1384					dom.shareTooltip.appendChild( utils.createEl( 'span' ) ).textContent = opt.shareText;
1385				}
1386
1387				this.createButtons( buttons, dom.shareTooltip, 'shareOn' );
1388
1389				// remove share button if no social media
1390			} else {
1391				opt.controls.splice( shareIndex, 1 );
1392			}
1393
1394		}
1395
1396		// append top bar & buttons
1397		if ( opt.controls.length || opt.counterMessage ) {
1398
1399			var slideShow = opt.controls.indexOf( 'play' );
1400
1401			dom.ui.appendChild( dom.topBar );
1402
1403			// append counter message
1404			if ( opt.counterMessage ) {
1405				dom.topBar.appendChild( dom.counter );
1406			}
1407
1408			// remove play button if slideShow interval <= 0
1409			if ( opt.slideShowInterval < 1 && slideShow > -1 ) {
1410				opt.controls.splice( slideShow, 1 );
1411			}
1412
1413			// append countdown timer
1414			if ( opt.countTimer && slideShow > -1 ) {
1415
1416				var timer = this.DOM.timer = utils.createEl( 'canvas', this.pre + '-timer' );
1417				timer.setAttribute( 'width', 48 );
1418				timer.setAttribute( 'height', 48 );
1419				dom.topBar.appendChild( timer );
1420
1421			}
1422
1423			// append crontrol buttons
1424			if ( opt.controls.length ) {
1425				// clone array (prevent options to be reversed if destroyed and initialized again)
1426				var controls = opt.controls.slice();
1427				this.createButtons( controls.reverse(), dom.topBar );
1428			}
1429
1430		}
1431
1432		// append bottom bar
1433		if ( opt.caption || opt.thumbnails ) {
1434
1435			dom.ui.appendChild( dom.bottomBar );
1436
1437			// append caption
1438			if ( opt.caption ) {
1439
1440				dom.bottomBar.appendChild( dom.caption )
1441					.appendChild( dom.captionInner );
1442
1443
1444			}
1445
1446			// append thumbnails
1447			if ( opt.thumbnails ) {
1448
1449				dom.bottomBar.appendChild( dom.thumbsHolder )
1450					.appendChild( dom.thumbsInner );
1451
1452			}
1453
1454		}
1455
1456		// append prev/next buttons
1457		if ( opt.prevNext ) {
1458			this.createButtons( ['prev', 'next'], dom.ui );
1459		}
1460
1461	};
1462
1463	/**
1464	 * Create Buttons elements
1465	 * @param {Array} buttons
1466	 * @param {Object} dom
1467	 * @param {string} event (event name attached to a button)
1468	 */
1469	proto.createButtons = function( buttons, dom, event ) {
1470
1471		var length = buttons.length;
1472
1473		for ( var i = 0; i < length; i++ ) {
1474
1475			var type = buttons[i];
1476
1477			// create and append button
1478			this.buttons[type] = utils.createEl( 'BUTTON', this.pre + '-' + type.toLowerCase() );
1479			dom.appendChild( this.buttons[type] );
1480
1481			// attach event if button have a corresponding prototype event
1482			if ( ( type && typeof this[type] === 'function' ) || event ) {
1483
1484				this.buttons[type].event  = event ? event : type;
1485				this.buttons[type].action = type;
1486
1487				if ( event === 'shareOn' ) {
1488					this.buttons[type].setAttribute( 'title', type.charAt( 0 ).toUpperCase() + type.slice( 1 ) );
1489				}
1490
1491			}
1492
1493		}
1494
1495	};
1496
1497	/**
1498	 * Get media galleries (img, video, iframe, HTML)
1499	 */
1500	proto.getGalleries = function() {
1501
1502		// prepare galleries
1503		// reset if executed again
1504		this.galleries = {};
1505
1506		// prepare querySelector
1507		var selectors = this.options.mediaSelector,
1508			sources   = '';
1509
1510		// if no selector set (prevent to trigger unnecessary errors)
1511		if ( !selectors ) {
1512			return false;
1513		}
1514
1515		// setup sources
1516		try {
1517			sources = document.querySelectorAll( selectors );
1518		} catch (error) {
1519			utils.error( 'Your current mediaSelector is not a valid selector: "' + selectors + '"' );
1520		}
1521
1522		for ( var i = 0, l = sources.length; i < l; i++ ) {
1523
1524			var source = sources[i],
1525				media = {};
1526
1527			// get original image url/src depending of the tagName
1528			media.src = source.tagName === 'A'   ? source.getAttribute( 'href' ) : null;
1529			media.src = source.tagName === 'IMG' ? source.currentSrc || source.src  : media.src;
1530			// if mediaSelector have data-src attr, take this one instead of src or href attributes
1531			media.src = source.getAttribute( 'data-src' ) || media.src;
1532
1533			if ( media.src ) {
1534
1535				this.getMediaAtts( source, media );
1536				this.setMediaType( media );
1537
1538				// If the media have a known type
1539				if ( media.type ) {
1540
1541					this.getMediaThumb( source, media );
1542					this.getVideoThumb( media );
1543					this.getMediaCaption( source, media );
1544
1545					// format caption
1546					this.setMediaCaption( media );
1547
1548					// get the gallery name & features
1549					var gallery = this.setGalleryName( source );
1550					this.setGalleryFeatures( gallery, media );
1551
1552					// assign media index
1553					media.index = gallery.length;
1554					// push media data in gallery
1555					gallery.push( media );
1556
1557					// attach click event to media
1558					this.setMediaEvent( source, gallery.name, media.index );
1559
1560				}
1561
1562			}
1563
1564		}
1565
1566		// dispatch updateGalleries event
1567		utils.dispatchEvent( this, 'modulobox', 'updateGalleries', this.galleries );
1568
1569	};
1570
1571	/**
1572	 * Add media in a gallery
1573	 * @param {number|string} name (gallery name)
1574	 * @param {Object} media (media collection)
1575	 */
1576	proto.addMedia = function( name, media ) {
1577
1578		if ( !media || typeof media !== 'object' ) {
1579
1580			utils.error( 'No media was found to addMedia() in a gallery' );
1581			return false;
1582
1583		}
1584
1585		// set gallery
1586		name = name === '' ? 1 : name;
1587		var gallery = this.galleries[name];
1588		gallery = !gallery ? ( this.galleries[name] = [] ) : gallery;
1589		gallery.name = name;
1590
1591		var length = media.length;
1592		for ( var i = 0; i < length; i++ ) {
1593
1594			var item = utils.cloneObject( media[i] );
1595
1596			if ( item.src ) {
1597
1598				this.setMediaType( item );
1599				this.getVideoThumb( item );
1600				this.setMediaCaption( item );
1601				this.setGalleryFeatures( gallery, item );
1602
1603				// assign media index
1604				item.index = gallery.length;
1605				// push media data in gallery
1606				gallery.push( item );
1607
1608			}
1609
1610		}
1611
1612	};
1613
1614	/**
1615	 * Set media type depending of its content
1616	 * Image/video types only, iframe & HTML must be set manually
1617	 * @param {Object} media
1618	 */
1619	proto.setMediaType = function( media ) {
1620
1621		// if the media type is already set and valid
1622		if ( ['image', 'video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) {
1623			return;
1624		}
1625
1626		// reset media type in case not valid
1627		media.type = null;
1628
1629		// get source
1630		var source = media.src ? media.src : null;
1631
1632		/* jslint bitwise: true */
1633		// get extension without query string
1634		var extension = ( source.split( /[?#]/ )[0] || source ).substr( ( ~-source.lastIndexOf( '.' ) >>> 0 ) + 2 );
1635		/* jslint bitwise: false */
1636
1637		// detect image
1638		if ( /(jpg|jpeg|png|bmp|gif|tif|tiff|jfi|jfif|exif|svg)/i.test( extension ) || ['external.xx.fbcdn', 'drscdn.500px.org'].indexOf( source ) > -1 ) {
1639
1640			media.type = 'image';
1641			media.src  = this.getSrc( source );
1642
1643		// detect HTML5 video
1644		} else if ( /(mp4|webm|ogv)/i.test( extension ) ) {
1645
1646			media.type   = 'video';
1647			media.format = 'html5';
1648
1649		// detect iframe video
1650		} else {
1651
1652			var stream = this.iframeVideo;
1653
1654			for ( var type in stream ) {
1655
1656				if ( stream.hasOwnProperty( type ) ) {
1657
1658					var regs = source.match( stream[type].reg );
1659
1660					if ( regs && regs[1] ) {
1661
1662						var object = stream[type];
1663
1664						media.type   = 'video';
1665						media.format = type;
1666						media.share  = object.share.replace( '[ID]', regs[1] );
1667						media.src    = object.url.replace( '[ID]', regs[1] );
1668						media.pause  = object.pause;
1669						media.play   = object.play;
1670
1671						if ( this.options.videoThumbnail ) {
1672							media.poster = !media.poster && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.poster;
1673							media.thumb  = !media.thumb && object.poster ? object.poster.replace( '[ID]', regs[1] ) : media.thumb;
1674						}
1675
1676						break;
1677
1678					}
1679
1680				}
1681
1682			}
1683
1684		}
1685
1686	};
1687
1688	/**
1689	 * Get image srcset custom
1690	 * @param {string} source
1691	 * @return {string} image url
1692	 */
1693	proto.getSrc = function( source ) {
1694
1695		var srcset = ( source || '' ).split( /,/ ),
1696			length = srcset.length,
1697			width  = 0;
1698
1699		if ( length <= 1 ) {
1700			return source;
1701		}
1702
1703		for ( var i = 0; i < length; i++ ) {
1704
1705			var parts = srcset[i].replace(/\s+/g, ' ').trim().split( / / ),
1706				value = parseFloat( parts[1] ) || 0,
1707				unit  = parts[1] ? parts[1].slice(-1) : null;
1708
1709			if ( ( unit === 'w' && screen.width >= value && value > width ) || !value || i === 0 ) {
1710
1711				width  = value;
1712				source = parts[0];
1713
1714			}
1715
1716		}
1717
1718		return source;
1719
1720	};
1721
1722	/**
1723	 * Get media attributes
1724	 * @param {Object} source
1725	 * @param {Object} media
1726	 */
1727	proto.getMediaAtts = function( source, media ) {
1728
1729		var auto = this.options.autoCaption,
1730			data = this.getAttr( source ),
1731			img  = source.firstElementChild;
1732			img  = source.tagName !== 'IMG' && img && img.tagName === 'IMG' ? img : source;
1733
1734		media.type   = !media.type ? data.type || source.getAttribute( 'data-type' ) : media.type;
1735		media.title  = data.title  || source.getAttribute( 'data-title' ) || ( auto ? img.title : null );
1736		media.desc   = data.desc   || source.getAttribute( 'data-desc' )  || ( auto ? img.alt  : null );
1737		media.thumb  = data.thumb  || source.getAttribute( 'data-thumb' );
1738		media.poster = this.getSrc( data.poster || source.getAttribute( 'data-poster' ) );
1739		media.width  = data.width  || source.getAttribute( 'data-width' );
1740		media.height = data.height || source.getAttribute( 'data-height' );
1741
1742		// prevent duplicate content if autoCaption enabled
1743		if ( media.title === media.desc ) {
1744			media.desc = null;
1745		}
1746
1747	};
1748
1749	/**
1750	 * Get media thumbnail
1751	 * @param {Object} source
1752	 * @param {Object} media
1753	 */
1754	proto.getMediaThumb = function( source, media ) {
1755
1756		var thumbnail = source.getElementsByTagName( 'img' );
1757
1758		if ( !media.thumb && thumbnail[0] ) {
1759			media.thumb = thumbnail[0].src;
1760		}
1761
1762	};
1763
1764	/**
1765	 * Get iframe video thumbnail
1766	 * @param {Object} media
1767	 */
1768	proto.getVideoThumb = function( media ) {
1769
1770		if ( !this.options.videoThumbnail ||  media.type !== 'video' || media.format === 'html5' ) {
1771			return;
1772		}
1773
1774		var hasPoster = media.poster && media.poster.indexOf( '.json' ) > -1,
1775			hasThumb  = media.thumb && media.thumb.indexOf( '.json' ) > -1;
1776
1777		if ( hasPoster || hasThumb ) {
1778
1779			var uri = hasPoster ? media.poster : media.thumb,
1780				xhr = new XMLHttpRequest();
1781
1782			xhr.onload = function( event ) {
1783
1784				/** @type {?|{thumbnail_large: string, thumbnail_url: string, thumbnail_small: string}} */
1785				var response = event.target.responseText;
1786					response = JSON.parse( response );
1787					response = response.hasOwnProperty( 0 ) ? response[0] : response;
1788
1789				if ( response ) {
1790
1791					media.poster = response.thumbnail_large || response.thumbnail_url;
1792
1793					// replace background image if image DOM already built
1794					if ( media.dom ) {
1795						media.dom.style.backgroundImage = 'url("' + media.poster + '")';
1796					}
1797
1798					if ( hasThumb ) {
1799
1800						var thumb = response.thumbnail_small || response.thumbnail_url;
1801
1802						// replace background image if thumbnail already built
1803						if ( typeof media.thumb === 'object' ) {
1804							media.thumb.style.backgroundImage = 'url("' + thumb + '")';
1805						} else {
1806							media.thumb = thumb;
1807						}
1808
1809					}
1810
1811				}
1812
1813			}.bind( this );
1814
1815			xhr.open( 'GET', encodeURI( uri ), true );
1816
1817			setTimeout( function() {
1818				xhr.send();
1819			}, 0 );
1820
1821		}
1822
1823	};
1824
1825	/**
1826	 * Get media caption
1827	 * @param {Object} source
1828	 * @param {Object} media
1829	 */
1830	proto.getMediaCaption = function( source, media ) {
1831
1832		var next = source.nextElementSibling;
1833
1834		// if gallery follow schema.org markup
1835		if ( next && next.tagName === 'FIGCAPTION' ) {
1836
1837			var caption = next.innerHTML;
1838
1839			if ( ! media.title ) {
1840				media.title = caption;
1841			} else if ( ! media.desc ) {
1842				media.desc  = caption;
1843			}
1844
1845		}
1846
1847	};
1848
1849	/**
1850	 * Build media caption
1851	 * @param {Object} media
1852	 */
1853	proto.setMediaCaption = function( media ) {
1854
1855		media.title   = media.title ? '<div class="' + this.pre + '-title">' + media.title.trim() + '</div>' : '';
1856		media.desc    = media.desc ? '<div class="' + this.pre + '-desc">' + media.desc.trim() + '</div>' : '';
1857		media.caption = media.title + media.desc;
1858
1859	};
1860
1861	/**
1862	 * Get gallery name from DOM
1863	 * @param {Object} source
1864	 */
1865	proto.getGalleryName = function( source ) {
1866
1867		var parent = source,
1868			node = 0;
1869
1870		while ( parent && node < 2 ) {
1871
1872			parent = parent.parentNode;
1873
1874			if ( parent && parent.tagName === 'FIGURE' && parent.parentNode ) {
1875
1876				return parent.parentNode.getAttribute( 'id' );
1877
1878			}
1879
1880			node++;
1881
1882		}
1883
1884	};
1885
1886	/**
1887	 * Set gallery name
1888	 * @param {Object} source
1889	 */
1890	proto.setGalleryName = function( source ) {
1891
1892		var data = this.getAttr( source );
1893
1894		// get name
1895		var name = data.rel || source.getAttribute( 'data-rel' );
1896		name = !name ? this.getGalleryName( source ) : name;
1897		name = !name ? Object.keys( this.galleries ).length + 1 : name;
1898
1899		// set gallery
1900		var gallery = this.galleries[name];
1901		gallery = !gallery ? ( this.galleries[name] = [] ) : gallery;
1902		gallery.name = name;
1903
1904		return gallery;
1905
1906	};
1907
1908	/**
1909	 * Set gallery features
1910	 * @param {Object} gallery
1911	 * @param {Object} media
1912	 */
1913	proto.setGalleryFeatures = function( gallery, media ) {
1914
1915		if ( !gallery.zoom && media.type === 'image' ) {
1916			gallery.zoom = true;
1917		}
1918
1919		if ( !gallery.download && ( media.type === 'image' || media.format === 'html5' ) ) {
1920			gallery.download = true;
1921		}
1922
1923	};
1924
1925	/**
1926	 * Attach click event to Media DOM
1927	 * @param {Object} source
1928	 * @param {number} name (of gallery)
1929	 * @param {number} index
1930	 */
1931	proto.setMediaEvent = function( source, name, index ) {
1932
1933		// if media already have a click event attached
1934		if ( source.mobxListener ) {
1935			source.removeEventListener( 'click', source.mobxListener, false );
1936		}
1937
1938		// set listener function to remove later if gallery updated
1939		source.mobxListener = this.open.bind( this, name, index );
1940		source.addEventListener( 'click', source.mobxListener, false );
1941
1942	};
1943
1944	/**
1945	 * Open lightbox from a given Gallery name and media index
1946	 * @param {string} name
1947	 * @param {number} index
1948	 * @param {Object} event
1949	 */
1950	proto.open = function( name, index, event ) {
1951
1952		// prevent default action and to bubbling up the DOM tree
1953		if ( event ) {
1954
1955			event.preventDefault();
1956			event.stopPropagation();
1957
1958		}
1959
1960		// if the instance was destroyed or does not exist
1961		if ( !this.GUID ) {
1962			return false;
1963		}
1964
1965		// check if gallery exists
1966		if ( !this.galleries.hasOwnProperty( name ) ) {
1967
1968			utils.error( 'This gallery name : "' + name + '", does not exist!' );
1969			return false;
1970
1971		}
1972
1973		// check if gallery have media
1974		if ( !this.galleries[name].length ) {
1975
1976			utils.error( 'Sorry, no media was found for the current gallery.' );
1977			return false;
1978
1979		}
1980
1981		// check if current media index exists (mainly to open from query param)
1982		if ( !this.galleries[name][index] ) {
1983
1984			utils.error( 'Sorry, no media was found for the current media index: ' + index );
1985			return false;
1986
1987		}
1988
1989		// dispatch beforeOpen Event
1990		utils.dispatchEvent( this, 'modulobox', 'beforeOpen', name, index );
1991
1992		// reset slide index
1993		this.slides.index = index;
1994
1995		// set gallery
1996		this.gallery = this.galleries[name];
1997		this.gallery.name   = name;
1998		this.gallery.index  = index;
1999		this.gallery.loaded = false;
2000
2001		// prepare lightbox
2002		this.removeContent();
2003		this.wrapAround();
2004		this.hideScrollBar();
2005		this.setSlider();
2006		this.setThumbs();
2007		this.setCaption();
2008		this.setMedia( this.options.preload );
2009		this.updateMediaInfo();
2010		this.replaceState();
2011		this.setControls();
2012		this.bindEvents( true );
2013		this.show();
2014
2015		// autoplay video on opening
2016		if ( this.options.videoAutoPlay ) {
2017			this.appendVideo();
2018		}
2019
2020		// autoplay slideshow on opening (only if video autoplay not set or not a video item opened)
2021		if ( this.options.slideShowAutoPlay && this.options.controls.indexOf( 'play' ) > -1 &&
2022			( ! this.options.videoAutoPlay || this.galleries[name][index].type !== 'video' ) ) {
2023			this.startSlideShow();
2024		}
2025
2026		// set lightbox states
2027		this.states.zoom = false;
2028		this.states.open = true;
2029
2030	};
2031
2032	/**
2033	 * Open lightbox from query string in url
2034	 */
2035	proto.openFromQuery = function() {
2036
2037		var query = this.getQueryString( window.location.search );
2038
2039		if ( query.hasOwnProperty( 'guid' ) && query.hasOwnProperty( 'mid' ) ) {
2040
2041			var open = this.open(
2042				decodeURIComponent( query.guid ),
2043				decodeURIComponent( query.mid ) - 1
2044			);
2045
2046			// remove query string if failed to open
2047			if ( open === false ) {
2048
2049				this.replaceState( true );
2050
2051			}
2052
2053		}
2054
2055	};
2056
2057	/**
2058	 * Reveal lightbox
2059	 */
2060	proto.show = function() {
2061
2062		var holder = this.DOM.holder,
2063			method = this.options.rightToLeft ? 'add' : 'remove';
2064
2065		holder.setAttribute( 'aria-hidden', false );
2066		utils.removeClass( holder, this.pre + '-idle' );
2067		utils.removeClass( holder, this.pre + '-panzoom' );
2068		utils.removeClass( holder, this.pre + '-will-close' );
2069		utils[method + 'Class']( holder, this.pre + '-rtl' );
2070		utils.addClass( holder, this.pre + '-open' );
2071
2072	};
2073
2074	/**
2075	 * Close lightbox
2076	 */
2077	proto.close = function( event ) {
2078
2079		// Prevent click/touch propagation
2080		if ( event ) {
2081			event.preventDefault();
2082		}
2083
2084		var holder  = this.DOM.holder,
2085			gallery = this.gallery,
2086			index   = gallery ? gallery.index : 'undefined',
2087			name    = gallery ? gallery.name : 'undefined';
2088
2089		// dispatch beforeClose Event
2090		utils.dispatchEvent( this, 'modulobox', 'beforeClose', name, index );
2091
2092		// exist from fullscreen
2093		if ( this.states.fullScreen ) {
2094
2095			this.exitFullScreen();
2096			utils.removeClass( holder, this.pre + '-fullscreen' );
2097
2098		}
2099
2100		// stop all actions
2101		this.share();
2102		this.stopSlideShow();
2103		this.pauseVideo();
2104
2105		this.bindEvents( false );
2106		this.replaceState( true );
2107		this.hideScrollBar();
2108
2109		// hide lightbox
2110		holder.setAttribute( 'aria-hidden', true );
2111		utils.removeClass( holder, this.pre + '-open' );
2112
2113		this.states.open = false;
2114
2115	};
2116
2117	/**
2118	 * Set controls and buttons states
2119	 */
2120	proto.setControls = function() {
2121
2122		// handle play/prev/next buttons
2123		var gallery = this.gallery,
2124			options = this.options,
2125			buttons = this.buttons;
2126
2127		// hide counter message if one media
2128		if ( this.DOM.counter ) {
2129			this.DOM.counter.style.display = ( gallery.initialLength > 1 ) ? '' : 'none';
2130		}
2131
2132		// hide play button if one media
2133		if ( options.controls.indexOf( 'play' ) > -1 ) {
2134			buttons.play.style.display = ( gallery.initialLength > 1 ) ? '' : 'none';
2135		}
2136
2137		// hide zoom button if one media and not zoomable
2138		if ( options.controls.indexOf( 'zoom' ) > -1 ) {
2139			buttons.zoom.style.display = !gallery.zoom ? 'none' : '';
2140		}
2141
2142		// hide download button if one media and not Downloadable
2143		if ( options.controls.indexOf( 'download' ) > -1 ) {
2144			buttons.download.style.display = !gallery.download ? 'none' : '';
2145		}
2146
2147		// hide/show prev & next buttons
2148		this.setPrevNextButtons();
2149
2150	};
2151
2152	/**
2153	 * Set responsive Prev/Next buttons
2154	 */
2155	proto.setPrevNextButtons = function() {
2156
2157		if ( this.options.prevNext ) {
2158
2159			var hide = this.slider.width < 680 && this.browser.touchDevice && !this.options.prevNextTouch;
2160			this.buttons.prev.style.display =
2161			this.buttons.next.style.display = ( this.gallery.length > 1 && !hide ) ? '' : 'none';
2162
2163		}
2164
2165	};
2166
2167	/**
2168	 * Set caption display depending of the screen size
2169	 */
2170	proto.setCaption = function() {
2171
2172		this.states.caption = !( !this.options.captionSmallDevice && ( this.slider.width <= 480 || this.slider.height <= 480 ) );
2173		this.DOM.caption.style.display = this.states.caption ? '' : 'none';
2174
2175	};
2176
2177	/**
2178	 * Set buttons states
2179	 */
2180	proto.hideScrollBar = function() {
2181
2182		if ( !this.options.scrollBar ) {
2183
2184			var open = this.states.open,
2185				scrollBar = open === 'undefined' || !open ? true : false;
2186
2187			document.body.style.overflow =
2188			document.documentElement.style.overflow = scrollBar ? 'hidden' : '';
2189
2190		}
2191
2192	};
2193
2194	// ----- Handle events ----- //
2195
2196	/**
2197	 * Bind/unbind all events attached to the lightbox
2198	 * @param {boolean} bind
2199	 */
2200	proto.bindEvents = function( bind ) {
2201
2202		var win     = window,
2203			doc     = document,
2204			opt     = this.options,
2205			holder  = this.DOM.holder,
2206			buttons = this.buttons,
2207			scrollMethod;
2208
2209		// handle buttons
2210		for ( var type in buttons ) {
2211
2212			if ( buttons.hasOwnProperty( type ) ) {
2213
2214				var DOM = ( type !== 'share' ) ? buttons[type] : win;
2215				utils.handleEvents( this, DOM, ['click', 'touchend'], buttons[type].event, bind );
2216
2217			}
2218
2219		}
2220
2221		// touch events
2222		utils.handleEvents( this, holder, this.dragEvents.start, 'touchStart', bind );
2223
2224		// keyboard event
2225		utils.handleEvents( this, win, ['keydown'], 'keyDown', bind );
2226
2227		// window resize event
2228		utils.handleEvents( this, win, ['resize', 'orientationchange'], 'resize', bind );
2229
2230		// transition end event
2231		utils.handleEvents( this, holder, ['transitionend', 'webkitTransitionEnd', 'oTransitionEnd', 'otransitionend', 'MSTransitionEnd'], 'opened' );
2232
2233		// disable double tap to zoom
2234		utils.handleEvents( this, holder, ['touchend'], 'disableZoom', bind );
2235
2236		// handle fullscreen
2237		if ( this.browser.fullScreen ) {
2238			utils.handleEvents( this, doc, [this.browser.fullScreen.change], 'toggleFullScreen', bind );
2239		}
2240
2241		// handle history navigation (replaceState)
2242		if ( opt.history ) {
2243			utils.handleEvents( this, win, ['mouseout'], 'mouseOut', bind );
2244		}
2245
2246		// handle idle state
2247		if ( opt.timeToIdle > 0 ) {
2248			utils.handleEvents( this, holder, ['mousemove'], 'mouseMove', bind );
2249		}
2250
2251		// prevent right click
2252		if ( !opt.contextMenu ) {
2253			utils.handleEvents( this, holder, ['contextmenu'], 'contextMenu', bind );
2254		}
2255
2256		// disable scroll
2257		if ( !opt.mouseWheel ) {
2258			this.disableScroll( bind );
2259		}
2260
2261		// scroll to zoom
2262		if ( opt.scrollToZoom ) {
2263			scrollMethod = 'scrollToZoom';
2264		}
2265		// scroll to navigate
2266		else if ( opt.scrollToNav ) {
2267			scrollMethod = 'scrollToNav';
2268		}
2269		// scroll to close
2270		else if ( opt.scrollToClose ) {
2271			scrollMethod = 'scrollToClose';
2272		}
2273
2274		// handle scroll method
2275		if ( scrollMethod ) {
2276			utils.handleEvents( this, holder, [this.browser.mouseWheel], scrollMethod, bind );
2277		}
2278
2279	};
2280
2281	/**
2282	 * Detect animation transition end
2283	 * Set open/close state
2284	 * Unload/remove media only when the lightbox is closed and hidden (transitionend)
2285	 * @param {Object} event
2286	 */
2287	proto.opened = function( event ) {
2288
2289		if ( event.propertyName === 'visibility' && event.target === this.DOM.holder ) {
2290
2291			var name  = this.gallery.name,
2292				index = this.gallery.index;
2293
2294			if ( !this.states.open ) {
2295
2296				// remove media in slide and thumbnails
2297				this.removeContent();
2298
2299				// dispatch afterClose Event
2300				utils.dispatchEvent( this, 'modulobox', 'afterClose', name, index );
2301
2302			} else {
2303
2304				// dispatch afterOpen Event
2305				utils.dispatchEvent( this, 'modulobox', 'afterOpen', name, index );
2306
2307			}
2308
2309		}
2310
2311	};
2312
2313	/**
2314	 * Detect when mouse leave browser window
2315	 * @param {Object} event
2316	 */
2317	proto.mouseOut = function( event ) {
2318
2319		var e = event ? event : window.event,
2320			from = e.relatedTarget || e.toElement;
2321
2322		if ( !from || from.nodeName === 'HTML' ) {
2323
2324			this.replaceState();
2325
2326		}
2327
2328	};
2329
2330	/**
2331	 * Detect idle state from mousemove
2332	 */
2333	proto.mouseMove = function() {
2334
2335		var holder    = this.DOM.holder,
2336			idleClass = this.pre + '-idle';
2337
2338		clearTimeout( this.states.idle );
2339
2340		this.states.idle = setTimeout( function() {
2341
2342			// prevent to hide top bar if tooltip opened
2343			if ( !utils.hasClass( holder, this.pre + '-open-tooltip' ) ) {
2344
2345				utils.addClass( holder, idleClass );
2346
2347			}
2348
2349		}.bind( this ), this.options.timeToIdle );
2350
2351		utils.removeClass( holder, idleClass );
2352
2353	};
2354
2355	/**
2356	 * Disable contextmenu (right click) on image
2357	 * @param {Object} event
2358	 */
2359	proto.contextMenu = function( event ) {
2360
2361		var target    = event.target,
2362			tagName   = target.tagName,
2363			className = target.className;
2364
2365		if ( tagName === 'IMG' || tagName === 'VIDEO' ||
2366			className.indexOf( this.pre + '-video' ) > -1 ||
2367			className.indexOf( this.pre + '-thumb-bg' ) > -1 ||
2368			className === this.pre + '-thumb' ) {
2369
2370			event.preventDefault();
2371
2372		}
2373
2374	};
2375
2376	/**
2377	 * Disable scroll
2378	 * @param {boolean} bind
2379	 */
2380	proto.disableScroll = function( bind ) {
2381
2382		var doc  = document,
2383			win  = window;
2384
2385		var prevent = function( e ) {
2386
2387			if ( !this.isEl( e ) ) {
2388				return;
2389			}
2390
2391			e = e || win.event;
2392
2393			if ( e.preventDefault ) {
2394
2395				e.preventDefault();
2396				e.returnValue = false;
2397				return false;
2398
2399			}
2400
2401		};
2402
2403		win.onwheel      =
2404		win.ontouchmove  =
2405		win.onmousewheel =
2406		doc.onmousewheel =
2407		doc.onmousewheel = bind ? prevent.bind( this ) : null;
2408
2409	};
2410
2411	/**
2412	 * Scroll to zoom
2413	 * @param {Object} event
2414	 */
2415	proto.scrollToZoom = function( event ) {
2416
2417		if ( !this.isEl( event ) ) {
2418			return;
2419		}
2420
2421		var data = this.normalizeWheel( event );
2422
2423		if ( data && data.deltaY ) {
2424
2425			var cell  = this.getCell(),
2426				scale = cell.attraction.s || cell.position.s;
2427				scale = Math.min( this.options.maxZoom, Math.max( 1, scale - Math.abs( data.deltaY ) / data.deltaY ) );
2428
2429			this.stopSlideShow();
2430			this.zoomTo( event.clientX, event.clientY, Math.round( scale * 10 ) / 10 );
2431
2432		}
2433
2434	};
2435
2436	/**
2437	 * Scroll to navigate (next/prev slide)
2438	 * @param {Object} event
2439	 */
2440	proto.scrollToNav = function( event ) {
2441
2442		if ( !this.isEl( event ) ) {
2443			return;
2444		}
2445
2446		var data = this.normalizeWheel( event );
2447
2448		if ( data && data.delta ) {
2449
2450			this[data.delta * this.isRTL() < 0 ? 'prev' : 'next']();
2451
2452		}
2453
2454	};
2455
2456	/**
2457	 * Scroll to close
2458	 * @param {Object} event
2459	 */
2460	proto.scrollToClose = function( event ) {
2461
2462		if ( !this.isEl( event ) ) {
2463			return;
2464		}
2465
2466		event.preventDefault();
2467		this.close();
2468
2469	};
2470
2471	/**
2472	 * Disable double tap to zoom on touch devices
2473	 * @param {Object} event
2474	 */
2475	proto.disableZoom = function( event ) {
2476
2477		var node = event.target;
2478
2479		while ( node ) {
2480
2481			// Prevent issue on Android with native HTML5 video controls
2482			// If preventDefault on Android devices, video controls will never be shown on tap
2483			// Keep click on link/input for HTML content
2484			if ( ['VIDEO', 'INPUT', 'A'].indexOf( node.tagName ) > -1 ) {
2485				return;
2486			}
2487
2488			node = node.parentElement;
2489
2490		}
2491
2492		event.preventDefault();
2493
2494	};
2495
2496	/**
2497	 * Resize lightbox
2498	 */
2499	proto.resize = function( event ) {
2500
2501		// Set new top bar height (in case icons are replaced)
2502		this.DOM.topBar.height = this.DOM.topBar.clientHeight;
2503
2504		this.share();
2505		this.setSlider();
2506		this.setThumbsPosition();
2507		this.setCaption();
2508		this.resizeMedia();
2509		this.updateMediaInfo();
2510		this.setPrevNextButtons();
2511
2512		// reset current slide zoom
2513		this.states.zoom = false;
2514		utils.removeClass( this.DOM.holder, this.pre + '-panzoom' );
2515
2516		// dispatch resize Event
2517		utils.dispatchEvent( this, 'modulobox', 'resize', event );
2518
2519	};
2520
2521	/**
2522	 * Resize media on window resize
2523	 */
2524	proto.resizeMedia = function() {
2525
2526		var slides = this.slides;
2527
2528		for ( var i = 0; i < slides.length; i++ ) {
2529
2530			if ( !this.gallery ) {
2531				break;
2532			}
2533
2534			var media = this.gallery[slides[i].media];
2535
2536			if ( media && ( ( media.dom && media.dom.loaded ) || ( media.dom && ['video', 'iframe', 'HTML'].indexOf( media.type ) > -1 ) ) ) {
2537
2538				this.setMediaSize( media, slides[i] );
2539
2540			}
2541
2542		}
2543
2544	};
2545
2546	// ----- main plugin helper functions  ----- //
2547
2548	/**
2549	 * Check if current element is part of Modulobox UI
2550	 * @param {Object} event
2551	 * @return {boolean}
2552	 */
2553	proto.isEl = function( event ) {
2554
2555		var name = event.target.className;
2556			name = typeof name === 'string' ? name : name.baseVal;
2557
2558		return name.indexOf( this.pre ) > -1;
2559
2560	};
2561
2562	/**
2563	 * Check if current media is zoomable
2564	 * @return {boolean}
2565	 */
2566	proto.isZoomable = function() {
2567
2568		var media = this.getMedia(),
2569			zoom = false;
2570
2571		if ( media.type === 'image' && media.dom && media.dom.size && media.dom.size.scale > 1 ) {
2572			zoom = true;
2573		}
2574
2575		this.DOM.holder.setAttribute( 'data-zoom', zoom );
2576		return zoom;
2577
2578	};
2579
2580	/**
2581	 * Check if current media is downloadable
2582	 * @return {boolean}
2583	 */
2584	proto.isDownloadable = function() {
2585
2586		var media = this.getMedia(),
2587			download = true;
2588
2589		if ( media.type !== 'image' && media.format !== 'html5' ) {
2590			download = false;
2591		}
2592
2593		this.DOM.holder.setAttribute( 'data-download', download );
2594		return download;
2595
2596	};
2597
2598	/**
2599	 * Check if RTL layout is enabled
2600	 * @return {number}
2601	 */
2602	proto.isRTL = function() {
2603
2604		return this.options.rightToLeft ? - 1 : 1;
2605
2606	};
2607
2608	/**
2609	 * Add attribute(s) in cache
2610	 * @param {Object} el
2611	 * @param {Object} attrs
2612	 */
2613	proto.addAttr = function( el, attrs ) {
2614
2615		var cacheID;
2616
2617		if ( typeof el[this.expando] === 'undefined' ) {
2618
2619			cacheID = this.cache.uid++;
2620			el[this.expando] = cacheID;
2621			this.cache[cacheID] = {};
2622
2623		} else {
2624			cacheID = el[this.expando];
2625		}
2626
2627		for ( var attr in attrs ) {
2628
2629			if ( attrs.hasOwnProperty( attr ) ) {
2630
2631				this.cache[cacheID][attr] = attrs[attr];
2632
2633			}
2634
2635		}
2636
2637	};
2638
2639	/**
2640	 * Get attribute(s) in cache
2641	 * @param {Object} el
2642	 * @return {Object}
2643	 */
2644	proto.getAttr = function( el ) {
2645
2646		return this.cache[el[this.expando]] || {};
2647
2648	};
2649
2650	/**
2651	 * Get thumbnail height
2652	 * @return {number}
2653	 */
2654	proto.getThumbHeight = function() {
2655
2656		var thumb = this.thumbs;
2657		return thumb.height > 0 && thumb.width > 0 ? thumb.height + Math.min( 10, thumb.gutter ) * 2 : 0;
2658
2659	};
2660
2661	/**
2662	 * Get current media in opened gallery
2663	 * @return {Object}
2664	 */
2665	proto.getMedia = function() {
2666
2667		var gallery = this.gallery;
2668		return gallery ? gallery[gallery.index] : null;
2669
2670	};
2671
2672	/**
2673	 * Get current cell in slide
2674	 * @return {Object}
2675	 */
2676	proto.getCell = function() {
2677
2678		var slides = this.slides,
2679			index  = utils.modulo( slides.length, slides.index );
2680
2681		return this.cells[index];
2682
2683	};
2684
2685	/**
2686	 * Remove/reset media in slide and remove thumbnails
2687	 */
2688	proto.removeContent = function() {
2689
2690		// remove media in each slide
2691		for ( var i = 0; i < this.slides.length; i++ ) {
2692
2693			var slide = this.slides[i];
2694
2695			// clean slide content
2696			this.unloadMedia( slide );
2697			this.removeMedia( slide );
2698
2699			// reset index and media index previously set
2700			slide.index =
2701				slide.media = null;
2702
2703		}
2704
2705		// remove all thumbnails
2706		this.removeMedia( this.DOM.thumbsHolder );
2707
2708	};
2709
2710	/**
2711	 * Get query string paramaters from url
2712	 * @param {string} search
2713	 * @return {Object}
2714	 */
2715	proto.getQueryString = function( search ) {
2716
2717		var params = {};
2718
2719		search.substr( 1 ).split( '&' ).forEach( function( param ) {
2720
2721			param = param.split( '=' );
2722			params[decodeURIComponent( param[0] )] = param.length > 1 ? decodeURIComponent( param[1] ) : '';
2723
2724		});
2725
2726		return params;
2727
2728	};
2729
2730	/**
2731	 * Replace query parameter in url
2732	 * @param {Object} key
2733	 * @return {string}
2734	 */
2735	proto.setQueryString = function( key ) {
2736
2737		var search = window.location.search,
2738			query  = this.getQueryString( search );
2739
2740		// decode URI part
2741		search = decodeURI( search );
2742
2743		for ( var prop in key ) {
2744
2745			if ( key.hasOwnProperty( prop ) ) {
2746
2747				// encode string component
2748				// value to replace in URI
2749				var replace = encodeURIComponent( key[prop] );
2750
2751				if ( query.hasOwnProperty( prop ) ) {
2752
2753					// already decoded from getQueryString
2754					// value to search from decoded search URI
2755					var value = query[prop];
2756
2757					if ( !replace ) {
2758
2759						search = search.replace( '&' + prop + '=' + value, '' );
2760						search = search.replace( prop + '=' + value, '' );
2761
2762					} else {
2763
2764						search = search.replace( prop + '=' + value, prop + '=' + replace );
2765
2766					}
2767
2768				} else {
2769
2770					if ( replace ) {
2771
2772						search = search + ( !search ? '?' : '&' ) + prop + '=' + replace;
2773
2774					} else {
2775
2776						search = search.replace( prop + '=', '' );
2777
2778					}
2779
2780				}
2781
2782			}
2783
2784		}
2785
2786		// create base url
2787		var base = [location.protocol, '//', location.host, location.pathname].join( '' );
2788		// check if search query is empty or not
2789		search = !search.substr( 1 ) ? search.substr( 1 ) : search;
2790
2791		// encode complete URI
2792		return encodeURI( base + search );
2793
2794	};
2795
2796	/**
2797	 * Replace url state with HTML5 API replaceState method
2798	 * @param {boolean} remove
2799	 */
2800	proto.replaceState = function( remove ) {
2801
2802		if ( ( this.options.history || remove ) && this.browser.pushState && !this.states.push ) {
2803
2804			var prevData = window.history.state,
2805				data = {
2806					guid : !remove ? this.gallery.name : '',
2807					mid  : !remove ? utils.modulo( this.gallery.initialLength, this.gallery.index ) + 1 : ''
2808				};
2809
2810			if ( !prevData || prevData.mid !== data.mid ) {
2811
2812				var search = this.setQueryString( data );
2813
2814				try {
2815
2816					window.history.replaceState( data, '', search );
2817
2818				} catch (error) {
2819
2820					// disable history
2821					this.options.history = false;
2822					// display error message in console
2823					utils.error( 'SecurityError: A history state object with origin \'null\' cannot be created. Please run the script on a server.' );
2824
2825				}
2826
2827			}
2828
2829		}
2830
2831		this.states.push = false;
2832
2833	};
2834
2835	/**
2836	 * Normalize mousewheel event accross browsers
2837	 * @param {Object} event
2838	 */
2839	proto.normalizeWheel = function( event ) {
2840
2841		var ev = event || window.event,
2842			data = null,
2843			deltaX,
2844			deltaY,
2845			delta;
2846
2847		event.preventDefault();
2848
2849		// old scrollwheel events
2850		if ( 'detail' in ev ) {
2851			deltaY = ev.detail * -1;
2852		}
2853		if ( 'wheelDelta' in ev ) {
2854			deltaY = ev.wheelDelta * -1;
2855		}
2856		if ( 'wheelDeltaY' in ev ) {
2857			deltaY = ev.wheelDeltaY * -1;
2858		}
2859		if ( 'wheelDeltaX' in ev ) {
2860			deltaX = ev.wheelDeltaX * -1;
2861		}
2862
2863		// W3C wheel event
2864		if ( 'deltaY' in ev ) {
2865			deltaY = ev.deltaY;
2866		}
2867		if ( 'deltaX' in ev ) {
2868			deltaX = ev.deltaX * -1;
2869		}
2870
2871		// deltaX/Y values are specified in lines
2872		if ( ev.deltaMode === 1 ) {
2873
2874			deltaX *= 40;
2875			deltaY *= 40;
2876
2877			// deltaX/Y values are specified in pages
2878		} else if ( ev.deltaMode === 2 ) {
2879
2880			deltaX *= 100;
2881			deltaY *= 100;
2882
2883		}
2884
2885		// normalize deltaX & deltaY
2886		delta = Math.abs( deltaX ) > Math.abs( deltaY ) ? deltaX : deltaY;
2887		delta = Math.min( 100, Math.max( -100, delta ) );
2888
2889		// if no scroll happened
2890		if ( Math.abs( delta ) < this.options.scrollSensitivity ) {
2891			this.states.prevDelta = delta;
2892			return data;
2893		}
2894
2895		var time = +new Date();
2896		// if mouse wheel velocity increases (touchpad or free wheel)
2897		// or elapsed time between mouse wheel events is superior to 60ms (user touchend touchpad or free wheel stopped)
2898		if ( Math.abs( delta ) > Math.abs( this.states.prevDelta ) || time - this.states.prevScroll > 60 ) {
2899
2900			// set data
2901			data = {
2902				'deltaX' : deltaX,
2903				'deltaY' : deltaY,
2904				'delta'  : delta
2905			};
2906
2907		}
2908
2909		// store last scroll data
2910		this.states.prevDelta  = delta;
2911		this.states.prevScroll = time;
2912
2913		return data;
2914
2915	};
2916
2917	// ----- Main function / events ----- //
2918
2919	/**
2920	 * Reveal/hide share tooltip holder on click
2921	 * @param {Object} event
2922	 */
2923	proto.share = function( event ) {
2924
2925		// prevent issue on Android with native HTML5 video controls
2926		if ( event && event.target.tagName === 'VIDEO' ) {
2927			return;
2928		}
2929
2930		var holder    = this.DOM.holder,
2931			className = this.pre + '-open-tooltip',
2932			element   = event ? event.target.className : null,
2933			method    = utils.hasClass( holder, className ) ? 'remove' : 'add';
2934
2935		if ( ( method === 'remove' && ( element !== this.pre + '-share' || !event ) ) || element === this.pre + '-share' ) {
2936
2937			if ( method === 'add' ) {
2938				this.setShareTooltip();
2939			}
2940
2941			utils[method + 'Class']( holder, className );
2942
2943		}
2944
2945	};
2946
2947	/**
2948	 * Share lightbox media from query url on social icon click
2949	 * @param {Object} event
2950	 */
2951	proto.shareOn = function( event ) {
2952
2953		var type    = event.target.action,
2954			gallery = this.gallery,
2955			media   = this.getMedia(),
2956			image   = media.type === 'image' ? media.src : media.poster,
2957			url     = this.socialMedia[type],
2958			uri;
2959
2960		if ( url ) {
2961
2962			// prepare uri from base url
2963			if ( this.options.sharedUrl === 'page' ) {
2964
2965				uri = [location.protocol, '//', location.host, location.pathname].join( '' );
2966
2967			// prepare uri from deeplink
2968			} else if ( this.options.sharedUrl === 'deeplink' || ['iframe', 'HTML'].indexOf( media.type ) > -1 ) {
2969
2970				uri = this.setQueryString({
2971					guid : gallery.name,
2972					mid  : gallery.index + 1
2973				});
2974
2975			// prepare uri from media file
2976			} else {
2977
2978				uri = media.src.replace( /\s/g, '' ).split( ',' )[0];
2979
2980				if ( media.type === 'video' && media.format !== 'html5' ) {
2981					uri = media.share;
2982				}
2983
2984			}
2985
2986			// convert to absolute path if necessary
2987			var link = utils.createEl( 'a' );
2988			link.href = image;
2989			image = link.href;
2990			link.href = uri;
2991			uri = link.href;
2992
2993			var tmp = utils.createEl( 'div' );
2994			tmp.innerHTML = media.caption;
2995			var text = ( tmp.textContent || tmp.innerText ).replace( /\s+/g, ' ' ).trim() || '';
2996
2997			url = url.replace( '[url]', encodeURIComponent( uri ) )
2998					.replace( '[image]', encodeURIComponent( image ) )
2999					.replace( '[text]', encodeURIComponent( text || document.title ) );
3000
3001			if ( url ) {
3002
3003				var left = Math.round( window.screenX + ( window.outerWidth - 626 ) / 2 ),
3004					top  = Math.round( window.screenY + ( window.outerHeight - 436 ) / 2 );
3005
3006				window.open( url, this.pre + '_share', 'status=0,resizable=1,location=1,toolbar=0,width=626,height=436,top=' + top + ',left=' + left );
3007
3008			}
3009
3010		} else {
3011
3012			utils.error( 'This social share media does not exist' );
3013
3014		}
3015
3016		return false;
3017
3018	};
3019
3020	/**
3021	 * Set share tooltip position
3022	 */
3023	proto.setShareTooltip = function() {
3024
3025		if ( this.options.controls.indexOf( 'share' ) > -1 ) {
3026
3027			var attribute = 'right',
3028				tooltip   = this.DOM.shareTooltip,
3029				width     = tooltip.clientWidth,
3030				button    = this.buttons.share.getBoundingClientRect(),
3031				position  = button.left - width + button.width / 2 + 20;
3032
3033			if ( position < 0 ) {
3034				attribute = 'left';
3035				position  = button.left + button.width / 2 - 20;
3036			}
3037
3038			tooltip.setAttribute( 'data-position', attribute );
3039			tooltip.style.top  = this.DOM.topBar.height + 6 + 'px';
3040			tooltip.style.left = position + 'px';
3041
3042		}
3043
3044	};
3045
3046	/**
3047	 * Download a media, generate downloadable link
3048	 */
3049	proto.download = function() {
3050
3051		if ( !this.isDownloadable() ) {
3052			return false;
3053		}
3054
3055		var media = this.getMedia(),
3056			url   = media.src.replace( /\s/g, '' ).split( ',' )[0],
3057			link  = document.createElement( 'a' );
3058
3059		link.href = url;
3060		link.download = new Date().getTime();
3061		link.setAttribute( 'target', '_blank' );
3062		document.body.appendChild( link );
3063		link.click();
3064		document.body.removeChild( link );
3065
3066	};
3067
3068	/**
3069	 * Handle click on fullScreen button
3070	 */
3071	proto.fullScreen = function() {
3072
3073		var fullScreenElement = this.browser.fullScreen.element;
3074
3075		if ( !document[fullScreenElement] ) {
3076			this.requestFullScreen();
3077		} else {
3078			this.exitFullScreen();
3079		}
3080
3081	};
3082
3083	/**
3084	 * Switch fullscreen state on fullScreen change
3085	 */
3086	proto.toggleFullScreen = function() {
3087
3088		var holder = this.DOM.holder,
3089			fullScreenElement = document[this.browser.fullScreen.element];
3090
3091		if ( !fullScreenElement ) {
3092
3093			this.share();
3094			this.states.fullScreen = false;
3095			utils.removeClass( holder, this.pre + '-fullscreen' );
3096
3097		} else if ( fullScreenElement === holder ) {
3098
3099			// set share tooltip position (fix issue on Safari)
3100			this.setShareTooltip();
3101			this.states.fullScreen = true;
3102			utils.addClass( holder, this.pre + '-fullscreen' );
3103
3104		}
3105
3106		this.videoFullScreen();
3107
3108	};
3109
3110	/**
3111	 * RequestFullScreen lightbox holder
3112	 */
3113	proto.requestFullScreen = function() {
3114
3115		var request = this.browser.fullScreen.request;
3116
3117		if ( document.documentElement[request] ) {
3118			this.DOM.holder[request]();
3119		}
3120
3121	};
3122
3123	/**
3124	 * ExitFullScreen lightbox holder
3125	 */
3126	proto.exitFullScreen = function() {
3127
3128		var exit = this.browser.fullScreen.exit;
3129
3130		if ( document[exit] ) {
3131			document[exit]();
3132		}
3133
3134	};
3135
3136	// ----- Slideshow & count down timer ----- //
3137
3138	/**
3139	 * Play slideshow event on click
3140	 */
3141	proto.play = function() {
3142
3143		if ( this.states.play ) {
3144			this.stopSlideShow();
3145		} else {
3146			this.startSlideShow();
3147		}
3148
3149	};
3150
3151	/**
3152	 * Start play and countdown timer
3153	 */
3154	proto.startSlideShow = function() {
3155
3156		var start    = 0,
3157			gallery  = this.gallery,
3158			options  = this.options,
3159			loop     = this.states.loop,
3160			autoStop = options.slideShowAutoStop,
3161			cycle    = Math.max( 120, options.slideShowInterval ),
3162			count    = options.countTimer,
3163			canvas   = count && this.DOM.timer ? this.DOM.timer.getContext( '2d' ) : null;
3164
3165		var countDown = ( function( now ) {
3166
3167			now = !now ? +new Date() : now;
3168			start = !start ? now : start;
3169
3170			if ( !loop || autoStop ) {
3171
3172				if ( gallery.index === gallery.initialLength - 1 ) {
3173
3174					this.stopSlideShow();
3175					return;
3176
3177				}
3178
3179			}
3180
3181			if ( count && canvas ) {
3182
3183				var percent = Math.min( 1, ( now - start + cycle ) / cycle - 1 ),
3184					radians = percent * 360 * ( Math.PI / 180 );
3185
3186				canvas.clearRect( 0, 0, 48, 48 );
3187				this.timerProgress( canvas, options.countTimerBg, 100 );
3188				this.timerProgress( canvas, options.countTimerColor, radians );
3189
3190			}
3191
3192			if ( now >= start + cycle ) {
3193
3194				start = now;
3195				this.slideTo( this.slides.index + 1, true );
3196
3197			}
3198
3199			this.timer = requestAnimationFrame( countDown );
3200
3201		}).bind( this );
3202
3203		// reveal canvas timer
3204		utils.addClass( this.DOM.holder, this.pre + '-autoplay' );
3205		// play state
3206		this.states.play = true;
3207		// start timer
3208		this.timer = requestAnimationFrame( countDown );
3209
3210	};
3211
3212	/**
3213	 * Stop slideshow
3214	 */
3215	proto.stopSlideShow = function() {
3216
3217		cancelAnimationFrame( this.timer );
3218		utils.removeClass( this.DOM.holder, this.pre + '-autoplay' );
3219		this.states.play = false;
3220
3221	};
3222
3223	/**
3224	 * Canvas arc countdown timer progress
3225	 * @param {Object} canvas
3226	 * @param {string} color
3227	 * @param {number} end
3228	 */
3229	proto.timerProgress = function( canvas, color, end ) {
3230
3231		var start = 1.5 * Math.PI;
3232		canvas.strokeStyle = color;
3233		canvas.lineWidth = 5;
3234		canvas.beginPath();
3235		canvas.arc( 24, 24, 18, start, start + end, false );
3236		canvas.stroke();
3237
3238	};
3239
3240	// ----- Handle videos ----- //
3241
3242	/**
3243	 * Append video DOM in slide on click
3244	 */
3245	proto.appendVideo = function() {
3246
3247		var media = this.getMedia();
3248
3249		if ( media.type !== 'video' ) {
3250			return;
3251		}
3252
3253		utils.addClass( media.dom, this.pre + '-loading' );
3254		utils.removeClass( media.dom, this.pre + '-playing' );
3255
3256		if ( !media.video ) {
3257
3258			// append HTML5 video
3259			if ( media.format === 'html5' ) {
3260
3261				// keep in memory video dom (for progression)
3262				media.video = utils.createEl( 'video' );
3263				media.video.setAttribute( 'controls', '' );
3264				media.video.setAttribute( 'autoplay', '' );
3265
3266				var urls = media.src.replace( /\s/g, '' ).split( ',' );
3267
3268				// loop through each video source
3269				for ( var i = 0; i < urls.length; i++ ) {
3270
3271					var frag   = document.createDocumentFragment(),
3272						source = utils.createEl( 'source' ),
3273						type   = ( /^.+\.([^.]+)$/ ).exec( urls[i] );
3274
3275					// check if video type is HTML5 valid
3276					if ( type && ['mp4', 'webm', 'ogv'].indexOf( type[1] ) > -1 ) {
3277
3278						source.src = urls[i];
3279						source.setAttribute( 'type', 'video/' + ( type[1] === 'ogv' ? 'ogg' : type[1] ) );
3280						frag.appendChild( source );
3281
3282					}
3283
3284					media.video.appendChild( frag );
3285
3286				}
3287
3288			}
3289			// append iFrame video
3290			else if ( media.format ) {
3291
3292				// create iframe
3293				media.video = utils.createEl( 'iframe' );
3294				media.video.src = media.src;
3295				media.video.setAttribute( 'frameborder', 0 );
3296				media.video.setAttribute( 'allowfullscreen', '' );
3297
3298			}
3299
3300			// set width/height
3301			media.video.setAttribute( 'width', '100%' );
3302			media.video.setAttribute( 'height', '100%' );
3303
3304		}
3305
3306		// append video if not exists
3307		if ( !media.dom.firstChild ) {
3308
3309			// append video if not exists
3310			media.dom.appendChild( media.video );
3311
3312			// reset loaded for iframe to preload before reveal
3313			if ( media.format !== 'html5' ) {
3314				media.video.loaded = false;
3315			}
3316
3317		}
3318
3319		// play video
3320		this.playVideo( media );
3321
3322	};
3323
3324	/**
3325	 * Play video
3326	 * @param {Object} media
3327	 */
3328	proto.onVideoLoaded = function( media ) {
3329
3330		media.video.loaded = true;
3331
3332		utils.removeClass( media.dom, this.pre + '-loading' );
3333		utils.addClass( media.dom, this.pre + '-playing' );
3334
3335		this.cloneVideo( media );
3336
3337	};
3338
3339	/**
3340	 * Clone video
3341	 * allows to preserve video state if wrapped around (loop)
3342	 * @param {Object} media
3343	 */
3344	proto.cloneVideo = function( media ) {
3345
3346		if ( this.states.loop && media.format === 'html5' ) {
3347
3348			var gallery = this.gallery,
3349				length  = gallery.length,
3350				initial = gallery.initialLength,
3351				current = utils.modulo( initial, media.index );
3352
3353			for ( var i = 0; i < length; i++ ) {
3354
3355				var index = utils.modulo( initial, gallery[i].index );
3356
3357				if ( index === current && gallery[i].index !== media.index ) {
3358					gallery[i].video = media.video;
3359				}
3360
3361			}
3362
3363		}
3364
3365	};
3366
3367	/**
3368	 * Enable/disable iframe video fullscreen
3369	 * Disable fullscreen iframe if lightbox in fullscreen
3370	 * Prevent issue on webkit browsers
3371	 */
3372	proto.videoFullScreen = function() {
3373
3374		var media = this.getMedia(),
3375			fullScreen = this.states.fullScreen;
3376
3377		if ( media.type === 'video' && media.format !== 'html5' && media.video ) {
3378
3379			media.video[fullScreen ? 'removeAttribute' : 'setAttribute']( 'allowfullscreen', '' );
3380
3381		}
3382
3383	};
3384
3385	/**
3386	 * Play video
3387	 * @param {{player}} media
3388	 */
3389	proto.playVideo = function( media ) {
3390
3391		// if video already loaded
3392		if ( media.video.loaded ) {
3393
3394			// trigger reflow to apply css transition
3395			media.video.getClientRects();
3396
3397			utils.removeClass( media.dom, this.pre + '-loading' );
3398			utils.addClass( media.dom, this.pre + '-playing' );
3399
3400			if ( media.format !== 'html5' ) {
3401
3402				if ( media.play ) {
3403
3404					var message = typeof media.play === 'object' ? JSON.stringify( media.play ) : String( media.play );
3405					media.video.contentWindow.postMessage( message, '*' );
3406
3407				}
3408
3409			} else if ( !media.video.error ) {
3410
3411				if ( typeof MediaElementPlayer === 'function' && typeof jQuery !== 'undefined' && this.options.mediaelement ) {
3412
3413					// because on IOS/Android the native HTML5 player is preserved
3414					var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0];
3415
3416					if ( mejs.player ) {
3417						mejs.player.setControlsSize();
3418					}
3419
3420					mejs.play();
3421
3422				} else {
3423
3424					media.video.play();
3425
3426				}
3427
3428			}
3429
3430		} else {
3431
3432			var _this = this;
3433
3434			// if mediaelement player
3435			if ( typeof jQuery !== 'undefined' && typeof MediaElementPlayer === 'function' && !media.play && this.options.mediaelement && !media.video.player ) {
3436
3437				MediaElementPlayer( media.video, {
3438					features                 : ['playpause', 'stop', 'current', 'progress', 'duration', 'volume', 'fullscreen'],
3439					videoVolume              : 'horizontal',
3440					startVolume              : 0.8,
3441					keyActions               : false,
3442					enableKeyboard           : false,
3443					iPadUseNativeControls    : true,
3444					iPhoneUseNativeControls  : true,
3445					AndroidUseNativeControls : true,
3446					success : function( mejs ) {
3447
3448						mejs.addEventListener( 'loadeddata', function() {
3449
3450							// get right DOM (because on mobile devices preserve original HTML5 player)
3451							// (iPadUseNativeControls, iPhoneUseNativeControls, AndroidUseNativeControls)
3452							media.video = media.dom.lastChild;
3453
3454							// if video is still in a slide
3455							if ( media.video ) {
3456
3457								// remove offscreen div to prevent display issue
3458								var offScreen = media.video.previousSibling;
3459								if ( offScreen && offScreen.parentNode ) {
3460									offScreen.parentNode.removeChild( offScreen );
3461								}
3462
3463								_this.onVideoLoaded( media );
3464
3465							}
3466
3467						}, media, false );
3468
3469					},
3470					error : function() {
3471
3472						_this.onVideoLoaded( media );
3473
3474					}
3475				});
3476
3477			} else if ( !media.video.onload ) {
3478
3479				media.video.onload  =
3480				media.video.onerror =
3481				media.video.onloadedmetadata = function() {
3482
3483						// if video is still in a slide
3484						if ( media.dom.firstChild ) {
3485
3486							_this.onVideoLoaded( media );
3487							_this.videoFullScreen();
3488
3489						}
3490
3491					};
3492
3493				media.video.src = media.src.replace( /\s/g, '' ).split( ',' )[0];
3494
3495			}
3496
3497		}
3498
3499	};
3500
3501	/**
3502	 * Pause video
3503	 */
3504	proto.pauseVideo = function() {
3505
3506		var media = this.getMedia();
3507
3508		if ( media && media.type === 'video' && media.video ) {
3509
3510			utils.removeClass( media.dom, this.pre + '-playing' );
3511
3512			// prevent iframe to autoplay if pause while loading
3513			if ( !media.video.loaded ) {
3514
3515				media.dom.innerHTML = '';
3516				utils.removeClass( media.dom, this.pre + '-loading' );
3517				return;
3518
3519			}
3520
3521			if ( media.format === 'html5' ) {
3522
3523				if ( typeof MediaElementPlayer === 'function' && typeof jQuery !== 'undefined' && this.options.mediaelement ) {
3524
3525					// because on IOS/Android the native HTML5 player is preserved
3526					var mejs = ( media.video.tagName === 'VIDEO' ) ? media.video : media.video.getElementsByTagName( 'video' )[0];
3527					mejs.pause();
3528
3529				} else {
3530
3531					media.video.pause();
3532
3533				}
3534
3535			} else {
3536
3537				if ( media.pause && media.format !== 'dailymotion' ) {
3538
3539					var message = typeof media.pause === 'object' ? JSON.stringify( media.pause ) : String( media.pause );
3540					media.video.contentWindow.postMessage( message, '*' );
3541
3542				// to handle embed videos with unknown postMessage methods
3543				} else {
3544
3545					media.dom.innerHTML = '';
3546					media.video = null;
3547
3548				}
3549
3550			}
3551
3552		}
3553
3554	};
3555
3556	// ----- Handle Media loading/sizing ----- //
3557
3558	/**
3559	 * Append media in slider
3560	 * @param {number} media_index
3561	 * @param {number} slide_index
3562	 */
3563	proto.insertMedia = function( media_index, slide_index ) {
3564
3565		// get media from current gallery
3566		var media = this.gallery[media_index];
3567
3568		if ( !media ) {
3569			return;
3570		}
3571
3572		// assign an index if not exists
3573		// necessary if manually pushed from the API
3574		if ( typeof media.index === 'undefined' ) {
3575			media.index = this.gallery.indexOf( media );
3576		}
3577
3578		this.buildMedia( media );
3579		this.appendMedia( media, slide_index );
3580		this.loadMedia( media, slide_index );
3581
3582	};
3583
3584	/**
3585	 * Build media DOM
3586	 * @param {Object} media
3587	 */
3588	proto.buildMedia = function( media ) {
3589
3590		// if the media is not defined
3591		if ( typeof media.dom === 'undefined' ) {
3592
3593			// detect media type
3594			switch ( media.type ) {
3595
3596				case 'image':
3597					media.dom = utils.createEl( 'img', this.pre + '-img' );
3598					media.dom.src = media.src;
3599					break;
3600
3601				case 'video':
3602					media.dom = utils.createEl( 'div', this.pre + '-video' );
3603					if ( media.poster ) {
3604						media.dom.style.backgroundImage = 'url("' + media.poster + '")';
3605					} else {
3606						media.dom.loaded = true;
3607					}
3608					break;
3609
3610				case 'iframe':
3611					media.dom = utils.createEl( 'iframe', this.pre + '-iframe' );
3612					media.dom.setAttribute( 'allowfullscreen', '' );
3613					media.dom.setAttribute( 'frameborder', 0 );
3614					media.dom.src = media.src;
3615					break;
3616
3617				case 'HTML':
3618					var element = media.src;
3619					var content = document.querySelector( element );
3620					media.dom = utils.createEl( 'div', this.pre + '-html' );
3621					media.dom.appendChild( utils.createEl( 'div', this.pre + '-html-inner' ) );
3622					media.dom.firstChild.innerHTML = content ? content.innerHTML : null;
3623					media.src = content ? content : '';
3624					media.dom.loaded = true;
3625					break;
3626
3627			}
3628
3629			// handle no media type & not content found (HMTL src empty for example)
3630			if ( !media.type || !media.src ) {
3631
3632				// build no content message
3633				media.dom = utils.createEl( 'div', this.pre + '-error' );
3634				media.dom.textContent = this.options.noContent;
3635				media.dom.loaded = true;
3636				media.dom.error  = true;
3637
3638				// dispatch load error event
3639				utils.dispatchEvent( this, 'modulobox', 'noContent', this.gallery.name, parseInt( media.index, 10 ) );
3640
3641			}
3642
3643		}
3644
3645	};
3646
3647	/**
3648	 * Append in a slide media DOM
3649	 * @param {Object} media
3650	 * @param {number} slide_index
3651	 */
3652	proto.appendMedia = function( media, slide_index ) {
3653
3654		// get media slide
3655		var slide  = this.slides[slide_index],
3656			holder = slide.firstChild,
3657			loader;
3658
3659		// if slide empty
3660		if ( !holder.childElementCount ) {
3661
3662			var fragment = document.createDocumentFragment();
3663
3664			loader = utils.createEl( 'div', this.pre + '-loader' );
3665			fragment.appendChild( loader );
3666			fragment.appendChild( media.dom );
3667			holder.appendChild( fragment );
3668
3669			// if a media is already present in the slide
3670		} else {
3671
3672			var oldMedia = holder.lastChild;
3673			loader = holder.firstChild;
3674
3675			loader.style.visibility = '';
3676
3677			// prevent unnecessary DOM manipulations if media already exists in slide
3678			if ( media.dom !== oldMedia ) {
3679
3680				// prevent to remove and duplicate media from another slide (because of replaceChild)
3681				// small hack for low performance (like IE9 or too much long key frames)
3682				// slideTo() method already includes a throttle (120ms) which will also fix it more naturally
3683				var method = holder.childElementCount === 1 ? 'appendChild' : 'replaceChild';
3684				holder[method]( media.dom, oldMedia );
3685
3686			}
3687
3688		}
3689
3690		// Assign media index to slide
3691		slide.media = media.index;
3692
3693	};
3694
3695	/**
3696	 * Load image/iframe
3697	 * @param {Object} media
3698	 * @param {number} slide_index
3699	 */
3700	proto.loadMedia = function( media, slide_index ) {
3701
3702		// show immediatly media if already loaded
3703		if ( media.dom.loaded ) {
3704
3705			this.showMedia( media, slide_index );
3706			return;
3707
3708		}
3709
3710		var _this = this,
3711			dom = media.type === 'iframe' ? media.dom : media.dom.img = new Image();
3712
3713		// on media load complete
3714		var onComplete = function() {
3715
3716			// dispatch loaded event
3717			if ( !media.dom.error ) {
3718				utils.dispatchEvent( _this, 'modulobox', 'loadComplete', _this.gallery.name, parseInt( media.index, 10 ) );
3719			}
3720
3721			// need to reload iframe because loaded DOM can't be stored due to cross-origin policy
3722			media.dom.loaded = media.type !== 'iframe' ? true : false;
3723			// reveal media
3724			_this.showMedia( media, slide_index );
3725
3726		};
3727
3728		// handle onload events
3729		dom.onload = onComplete;
3730
3731		// handle onerror event
3732		dom.onerror = function(e) {
3733
3734			// set error message
3735			// not for video because it only concerns a poster and not the media itself
3736			if ( media.type !== 'video' ) {
3737
3738				// build load error message
3739				media.dom = utils.createEl( 'p', _this.pre + '-error' );
3740				media.dom.textContent = _this.options.loadError;
3741				media.dom.error = true;
3742
3743				// append error message
3744				_this.appendMedia( media, slide_index );
3745
3746			}
3747
3748			// dispatch loaded error event
3749			utils.dispatchEvent( _this, 'modulobox', 'loadError', _this.gallery.name, parseInt( media.index, 10 ) );
3750
3751			// trigger complete to reveal error
3752			onComplete();
3753
3754		};
3755
3756		// set src
3757		dom.src = ( media.type === 'video' ) ? media.poster : media.src;
3758
3759	};
3760
3761	/**
3762	 * Unload media src
3763	 * @param {Object} slide
3764	 */
3765	proto.unloadMedia = function( slide ) {
3766
3767		if ( !this.gallery ) {
3768			return;
3769		}
3770
3771		var index = slide.media,
3772			media = this.gallery[index];
3773
3774		if ( !media || !media.dom ) {
3775			return;
3776		}
3777
3778		// cancel old media if exists (and option enabled)
3779		if ( this.options.unload && media.type === 'image' && !media.dom.loaded && !media.dom.complete && !media.dom.naturalWidth ) {
3780
3781			// unset events and src
3782			media.dom.onload  = null;
3783			media.dom.onerror = null;
3784			media.dom.src     = '';
3785
3786			if ( media.dom.img ) {
3787
3788				// unset events and src for img object
3789				media.dom.img.onload  = null;
3790				media.dom.img.onerror = null;
3791				media.dom.img.src     = '';
3792				delete media.dom.img;
3793
3794			}
3795
3796			// unset dom to reset src from buildMedia method
3797			delete media.dom;
3798
3799		}
3800		// remove iframe video (prevent autoplay if loop enable)
3801		else if ( media.type === 'video' && media.format !== 'html5' && media.dom.firstChild ) {
3802
3803			media.video = null;
3804			media.dom.removeChild( media.dom.firstChild );
3805
3806		}
3807
3808	};
3809
3810	/**
3811	 * Remove all content present in slide
3812	 * @param {Object} holder
3813	 */
3814	proto.removeMedia = function( holder ) {
3815
3816		var content = holder.firstChild;
3817
3818		if ( !content ) {
3819			return;
3820		}
3821
3822		while ( content.firstChild ) {
3823			content.removeChild( content.firstChild );
3824		}
3825
3826	};
3827
3828	/**
3829	 * Reveal media content in slide
3830	 * @param {Object} media
3831	 * @param {number} slide_index
3832	 */
3833	proto.showMedia = function( media, slide_index ) {
3834
3835		// get slider states
3836		var slider = this.slider;
3837
3838		// if option fade when settled, check if slider settled to reveal
3839		if ( this.options.fadeIfSettle && !slider.settle && !media.dom.revealed ) {
3840			return;
3841		}
3842
3843		// get slide DOM
3844		var slide   = this.slides[slide_index],
3845			gallery = this.gallery,
3846			holder  = slide.firstChild,
3847			loader  = holder.firstChild,
3848			preload = this.options.preload;
3849
3850		// set media size
3851		this.setMediaSize( media, slide );
3852
3853		// check if media is zoomable
3854		if ( media.index === gallery.index ) {
3855			this.isZoomable();
3856		}
3857
3858		// reveal media object
3859		// even if media is not in a slide, it will be revealed in media object (stored in gallery obj)
3860		utils.addClass( media.dom, this.pre + '-media-loaded' );
3861		media.dom.revealed = true;
3862
3863		// if current media match slide
3864		if ( slide.media === media.index ) {
3865
3866			// hide loader
3867			loader.style.visibility = 'hidden';
3868
3869			// increment number of media loaded in the gallery
3870			gallery.loaded += 1;
3871
3872			// set closest media
3873			if ( gallery.loaded === preload && preload < 4 ) {
3874				this.setMedia( preload + 2 );
3875			}
3876
3877		}
3878
3879		// if iframe than reset loaded
3880		// because we can't store content due to cross-origin policy
3881		if ( media.type === 'iframe' ) {
3882			media.dom.loaded = false;
3883		}
3884
3885
3886	};
3887
3888	/**
3889	 * Calculate and set media size styles
3890	 * @param {Object} media
3891	 * @param {Object} slide
3892	 */
3893	proto.setMediaSize = function( media, slide ) {
3894
3895		var object   = media.dom,
3896			slider   = this.slider,
3897			viewport = object.viewport,
3898			thumbs   = this.getThumbHeight();
3899
3900		// if error no size to calculate
3901		if ( object.error ) {
3902			return;
3903		}
3904
3905		// if the media was not already sized for the current slider viewport
3906		// prevent unnecessary calculations and reflows from caption calculations (clientHeight)
3907		if ( !viewport ||
3908			viewport.width !== slider.width ||
3909			viewport.height !== slider.height - thumbs ) {
3910
3911			this.getCaptionHeight( media, slide );
3912			this.getMediaSize( media, slide );
3913			this.fitMediaSize( media, slide );
3914			this.setMediaOffset( media, slide );
3915
3916		}
3917
3918		var style = object.style;
3919
3920		// media is not displayed (no reflow)
3921		style.width  = object.size.width + 'px';
3922		style.height = object.size.height + 'px';
3923		style.left   = object.offset.left + 'px';
3924		style.top    = object.offset.top + 'px';
3925
3926	};
3927
3928	/**
3929	 * Calculate caption height & assign slide size
3930	 * @param {Object} media
3931	 * @param {Object} slide
3932	 */
3933	proto.getCaptionHeight = function( media, slide ) {
3934
3935		var caption = this.DOM.captionInner,
3936			topBar  = this.DOM.topBar.height,
3937			content = caption.innerHTML,
3938			thumbs  = this.getThumbHeight();
3939
3940		if ( this.options.caption && this.states.caption && media.caption ) {
3941
3942			// set media caption
3943			caption.innerHTML = media.caption;
3944			// get caption height
3945			caption.height = Math.max( topBar, parseInt( caption.clientHeight, 10 ) ) || topBar;
3946			// restore current caption
3947			caption.innerHTML = content;
3948
3949			// if caption is hidden
3950		} else {
3951
3952			// set caption height to top bar
3953			// allow to center image in the viewport
3954			// if thumbnails fit to the top of the thumbs slider
3955			caption.height = thumbs ? 0 : topBar;
3956
3957		}
3958
3959		// set slide size (not real size, just an helper)
3960		slide.width  = this.slider.width;
3961		slide.height = this.slider.height - topBar - caption.height - thumbs;
3962
3963	};
3964
3965	/**
3966	 * Calculate natural media size
3967	 * @param {Object} media
3968	 * @param {Object} slide
3969	 */
3970	proto.getMediaSize = function( media, slide ) {
3971
3972		var size = media.dom.size = {};
3973
3974		switch ( media.type ) {
3975
3976			case 'image':
3977				size.width  = media.dom.naturalWidth;
3978				size.height = media.dom.naturalHeight;
3979				break;
3980
3981			case 'video':
3982				size.width  = this.options.videoMaxWidth;
3983				size.height = size.width / this.options.videoRatio;
3984				break;
3985
3986			case 'iframe':
3987				size.width  = media.width ? media.width : slide.width > 680 ? slide.width * 0.8 : slide.width;
3988				size.height = media.height ? media.height : slide.height;
3989				break;
3990
3991			case 'HTML':
3992				size.width  = media.width ? media.width : slide.width;
3993				size.height = media.height ? media.height : slide.height;
3994				break;
3995
3996		}
3997
3998	};
3999
4000	/**
4001	 * Fit media size to slide
4002	 * @param {Object} media
4003	 * @param {Object} slide
4004	 */
4005	proto.fitMediaSize = function( media, slide ) {
4006
4007		var slider      = this.slider,
4008			options     = this.options,
4009			zoom        = options.zoomTo,
4010			size        = media.dom.size,
4011			ratio       = size.width / size.height,
4012			thumbs      = this.getThumbHeight(),
4013			smallDevice = slider.width <= 480 || slider.height <= 680,
4014			canOverflow = ['video', 'iframe', 'HTML'].indexOf( media.type ) < 0,
4015			smartResize = options.smartResize && smallDevice,
4016			width, height;
4017
4018		// add slide height viewport
4019		var viewports = [slide.height];
4020
4021		// add slider height viewport
4022		if ( ( smartResize || options.overflow ) && canOverflow ) {
4023
4024			viewports.unshift( slider.height - thumbs );
4025
4026		}
4027
4028		// check if media size fit to viewports (viewport)
4029		viewports.forEach( function( viewport ) {
4030
4031			if ( !height || height < slider.height - thumbs ) {
4032
4033				width  = Math.min( size.width, ratio * viewport );
4034				width  = width > slide.width ? slide.width : Math.round( width );
4035				height = Math.ceil( 1 / ratio * width );
4036				height = height % viewport < 2 ? viewport : height;
4037
4038			}
4039
4040		});
4041
4042		// Calculate from how much the image can be zoomed
4043		var scale = Number( ( size.width / width ).toFixed( 3 ) );
4044			zoom  = zoom === 'auto' ? scale : zoom;
4045
4046		media.dom.size = {
4047			width  : width,
4048			height : height,
4049			scale  : scale >= options.minZoom ? Math.min( options.maxZoom, zoom ) : 1
4050		};
4051
4052	};
4053
4054	/**
4055	 * Calculate media offset
4056	 * @param {Object} media
4057	 * @param {Object} slide
4058	 */
4059	proto.setMediaOffset = function( media, slide ) {
4060
4061		var size    = media.dom.size,
4062			slider  = this.slider,
4063			topBar  = this.DOM.topBar.height,
4064			thumbs  = this.getThumbHeight(),
4065			fromTop = 0;
4066
4067		// if the media can be centered
4068		if ( size.height <= slide.height ) {
4069
4070			// center from the available slide height
4071			fromTop = topBar + ( slide.height - size.height ) * 0.5;
4072
4073		}
4074
4075		// set media offset
4076		media.dom.offset = {
4077			top  : fromTop < 0 ? 0 : Math.round( fromTop ),
4078			left : Math.round( ( slide.width - size.width ) * 0.5 )
4079		};
4080
4081		// set media viewport
4082		media.dom.viewport = {
4083			width  : slider.width,
4084			height : slider.height - thumbs
4085		};
4086
4087	};
4088
4089	/**
4090	 * Calculate media viewport
4091	 * @param {number} scale
4092	 * @return {Object}
4093	 * hGap : difference between slider height and image size
4094	 * vGap : difference image offset and image overflow
4095	 * cGap : distance between image center and slider center
4096	 * sGap : cGap scaled
4097	 * these values allows to take into account the image center gap regarding the slide center
4098	 */
4099	proto.mediaViewport = function( scale ) {
4100
4101		var media = this.getMedia();
4102
4103		// if loading or not a media (error)
4104		if ( !media.dom || !media.dom.size ) {
4105
4106			return {
4107				top    : 0,
4108				bottom : 0,
4109				left   : 0,
4110				right  : 0
4111			};
4112
4113		}
4114
4115		// calculate media viewport
4116		var size   = media.dom.size,
4117			offset = media.dom.offset,
4118			height = this.slider.height,
4119			width  = this.slider.width,
4120			hGap   = ( height - size.height ) * 0.5,
4121			vGap   = offset.top * 2 - hGap,
4122			cGap   = ( hGap - vGap ) * 0.5,
4123			sGap   = cGap * scale - cGap * 2 - vGap,
4124			left   = size.width / 2 * ( scale - 1 ) - offset.left,
4125			top    = size.height * scale <= height ? cGap * scale : -size.height / 2 * ( scale - 1 ) + height - size.height + sGap,
4126			bottom = size.height * scale <= height ? cGap * scale : size.height / 2 * ( scale - 1 ) + sGap;
4127
4128		// set panZoom viewport
4129		return {
4130			top    : scale <= 1 ? 0 : Math.round( top ),
4131			bottom : scale <= 1 ? 0 : Math.round( bottom ),
4132			left   : size.width * scale < width ? 0 : Math.round( left ),
4133			right  : size.width * scale < width ? 0 : Math.round( -left )
4134		};
4135
4136	};
4137
4138	/**
4139	 * Set/add media in slides
4140	 * @param {number} number
4141	 */
4142	proto.setMedia = function( number ) {
4143
4144		var gallery = this.gallery,
4145			slides  = this.slides,
4146			loop    = this.states.loop,
4147			RTL     = this.isRTL(),
4148			index   = Math.round( - RTL * this.slider.position.x / slides.width ),
4149			length  = gallery.initialLength - 1,
4150			adjust  = 0,
4151			toLoad  = [],
4152			i;
4153
4154		// find number of media to load
4155		if ( !number && !gallery.loaded ) {
4156
4157			number = 0;
4158
4159			for ( i = 0; i < slides.length; i++ ) {
4160
4161				if ( slides[i].firstChild.childElementCount ) {
4162					number++;
4163				}
4164
4165			}
4166
4167			number += 2;
4168			gallery.loaded = this.options.preload;
4169
4170		}
4171
4172		// prepare positions
4173		switch ( number ) {
4174			case 0:
4175			case 1:
4176				toLoad = [0];
4177				break;
4178			case 2:
4179			case 3:
4180				toLoad = [-1, 0, 1];
4181				break;
4182			default:
4183				number = 5;
4184				toLoad = [-2, -1, 0, 1, 2];
4185		}
4186
4187		// adjust positions to ends if no loop
4188		if ( !loop ) {
4189
4190			var maxMedia = index + toLoad[number - 1],
4191				minMedia = index + toLoad[0];
4192
4193			adjust = ( minMedia < 0 ) ? -minMedia : 0;
4194			adjust = ( maxMedia > length ) ? length - maxMedia : adjust;
4195
4196		}
4197
4198		// convert positions to media index
4199		toLoad = toLoad.map( function( i ) {
4200
4201			return utils.modulo( gallery.length, i + adjust + index );
4202
4203		});
4204
4205		// insert media in corresponding slides
4206		for ( i = 0; i < slides.length; i++ ) {
4207
4208			var slide = slides[i],
4209				media_index = utils.modulo( gallery.length, slide.index );
4210
4211			// if no loop && media index bigger than slide index
4212			if ( !loop && slide.index > media_index ) {
4213				continue;
4214			}
4215
4216			// if the media corresponds to a slide and is not already assigned
4217			if ( toLoad.indexOf( media_index ) > -1 && slide.media !== media_index ) {
4218
4219				// unload previous media
4220				this.unloadMedia( slide );
4221				// insert media in slide
4222				this.insertMedia( media_index, i );
4223
4224			}
4225
4226		}
4227
4228	};
4229
4230	/**
4231	 * Update media info based on current slide index
4232	 */
4233	proto.updateMediaInfo = function() {
4234
4235		var slides   = this.slides,
4236			gallery  = this.gallery;
4237
4238		// update gallery index
4239		gallery.index = utils.modulo( gallery.length, slides.index );
4240
4241		this.isZoomable();
4242		this.isDownloadable();
4243		this.updateCounter();
4244		this.updateCaption();
4245		this.updateThumbs();
4246
4247		// dispatch update event
4248		utils.dispatchEvent( this, 'modulobox', 'updateMedia', this.getMedia() );
4249
4250	};
4251
4252	/**
4253	 * Set thumbnails
4254	 */
4255	proto.setThumbs = function() {
4256
4257		var gallery      = this.gallery,
4258			thumbs       = this.thumbs,
4259			length       = gallery.initialLength,
4260			thumbnails   = this.options.thumbnails,
4261			thumbsHolder = this.DOM.thumbsHolder;
4262
4263		// return if no thumb opt or if only one thumbnail to display
4264		if ( !thumbnails || length < 2 ) {
4265
4266			// hide thumbs and reset caption position
4267			this.DOM.caption.style.bottom = 0;
4268			thumbsHolder.style.visibility = 'hidden';
4269			thumbsHolder.style.height     = 0;
4270
4271			// unset thumbs
4272			thumbs.height =
4273			thumbs.gutter = 0;
4274
4275			return;
4276
4277		}
4278
4279		// get max screen width available on the current device
4280		var sizes   = this.options.thumbnailSizes,
4281			screenW = Math.max( window.innerWidth, Math.max( screen.width, screen.height ) ),
4282			thumbNb = 0,
4283			i;
4284
4285		// sort thumbnail browser widths
4286		var widths = Object.keys( sizes ).sort( function( a, b ) {
4287			return a - b;
4288		});
4289
4290		// find number of thumbnails needed to build the thumbnails slider
4291		for ( i = 0; i < widths.length; i++ ) {
4292
4293			var size   = widths[i],
4294				width  = i === widths.length - 1 ? screenW : Math.min( screenW, size ),
4295				number = Math.ceil( width / ( sizes[size].width + sizes[size].gutter ) * 2 );
4296
4297			if ( isFinite( number ) && number > thumbNb ) {
4298				thumbNb = number;
4299			}
4300
4301			if ( size >= screenW ) {
4302				break;
4303			}
4304
4305		}
4306
4307		// set the number of thumbnail requiered for the current gallery
4308		var fragment = document.createDocumentFragment();
4309		length = length > 50 ? Math.min( thumbNb, length ) : length;
4310
4311		// prepare thumbnails
4312		for ( i = 0; i < length; i++ ) {
4313
4314			var thumb = utils.createEl( 'div', this.pre + '-thumb' );
4315			fragment.appendChild( thumb );
4316
4317		}
4318
4319		// append thumbnails
4320		this.DOM.thumbsInner.appendChild( fragment );
4321		// set thumbnails position
4322		this.setThumbsPosition();
4323
4324	};
4325
4326	/**
4327	 * Set thumbnails
4328	 * @param {Object} event
4329	 */
4330	proto.thumbClick = function( event ) {
4331
4332		var target = event.target;
4333
4334		if ( !utils.hasClass( target, this.pre + '-thumb' ) ) {
4335			target = target.parentNode;
4336		}
4337
4338		if ( parseInt( target.index, 10 ) >= 0 ) {
4339			this.slideTo( target.index );
4340		}
4341
4342	};
4343
4344	/**
4345	 * Set & init animation (slider & cells)
4346	 * @param {Object} thumb
4347	 * @param {number} index
4348	 */
4349	proto.loadThumb = function( thumb, index ) {
4350
4351		// get media
4352		var media = this.gallery[index],
4353			src;
4354
4355		// if the thumbnail was not loaded (is not DOM object)
4356		if ( !media.thumb || typeof media.thumb !== 'object' ) {
4357
4358			src = media.thumb;
4359			media.thumb = utils.createEl( 'div', this.pre + '-thumb-bg' );
4360			media.thumb.style.backgroundImage = src && src.indexOf( '.json' ) < 0 ? 'url(' + src + ')' : null;
4361
4362			// add video icon
4363			if ( media.type === 'video' ) {
4364
4365				utils.addClass( media.thumb, this.pre + '-thumb-video' );
4366				utils.addClass( media.thumb, this.pre + '-thumb-loaded' );
4367
4368			}
4369
4370		}
4371
4372		// append/replace thumbnail
4373		var method = thumb.firstChild ? 'replaceChild' : 'appendChild';
4374		thumb[method]( media.thumb, thumb.firstChild );
4375
4376		// save media index to thumb
4377		thumb.media = index;
4378
4379		// load thumbnail
4380		if ( src ) {
4381
4382			var dom = new Image();
4383
4384			// handle onload events (no need to handle error/abort)
4385			dom.onload = function() {
4386
4387				// reveal thumbnail
4388				utils.addClass( media.thumb, this.pre + '-thumb-loaded' );
4389
4390			}.bind( this );
4391
4392			// set image src
4393			dom.src = src;
4394
4395		}
4396
4397	};
4398
4399	/**
4400	 * Update thumbnail active
4401	 */
4402	proto.updateThumbs = function() {
4403
4404		var gallery = this.gallery;
4405
4406		if ( !this.options.thumbnails || gallery.initialLength < 2 ) {
4407			return;
4408		}
4409
4410		var thumbs   = this.thumbs,
4411			position = this.getThumbPosition( thumbs );
4412
4413		// stop current animation
4414		thumbs.stopAnimate();
4415
4416		// if thumb slider already at the right position
4417		if ( position === thumbs.position.x ) {
4418
4419			this.shiftThumbs( thumbs );
4420			return;
4421
4422		}
4423
4424		// move thumbnails without animation
4425		if ( Math.abs( position - thumbs.position.x ) > 50 * thumbs.size ) {
4426
4427			this.DOM.thumbsHolder.style.visibility = 'hidden';
4428			thumbs.position.x = position;
4429			utils.translate( this.DOM.thumbsInner, position, 0 );
4430			this.renderThumbs( thumbs );
4431			this.DOM.thumbsHolder.style.visibility = '';
4432
4433		// move thumbnails
4434		} else {
4435
4436			thumbs.startAnimate();
4437			thumbs.releaseDrag();
4438			thumbs.animateTo({
4439				x : position
4440			});
4441
4442		}
4443
4444	};
4445
4446	/**
4447	 * Set caption height
4448	 */
4449	proto.updateCaption = function() {
4450
4451		if ( this.options.caption ) {
4452
4453			// get caption content
4454			var media   = this.getMedia(),
4455				content = media.caption ? media.caption : '',
4456				caption = this.DOM.captionInner;
4457
4458			// set the caption content if not already present
4459			if ( caption.innerHTML !== content ) {
4460				caption.innerHTML = content;
4461			}
4462
4463		}
4464
4465	};
4466
4467	/**
4468	 * Set counter index
4469	 */
4470	proto.updateCounter = function() {
4471
4472		if ( this.options.counterMessage ) {
4473
4474			var gallery = this.gallery,
4475				length  = gallery.initialLength,
4476				index   = utils.modulo( length, gallery.index ),
4477				message = this.options.counterMessage,
4478				content = message.replace( '[index]', index + 1 ).replace( '[total]', length ),
4479				counter = this.DOM.counter;
4480
4481			// set the counter if not already set
4482			if ( counter.textContent !== content ) {
4483				counter.textContent = content;
4484			}
4485
4486		}
4487
4488	};
4489
4490	// ----- Set position and animations ----- //
4491
4492	/**
4493	 * Wrap media around for infinite loop
4494	 */
4495	proto.wrapAround = function() {
4496
4497		var loop    = this.options.loop,
4498			gallery = this.gallery,
4499			length  = gallery.length;
4500
4501		// store initial length
4502		if ( !gallery.initialLength ) {
4503			gallery.initialLength = length;
4504		}
4505
4506		// set loop state
4507		this.states.loop = loop && loop <= length ? true : false;
4508
4509		// add fake media if infinite loop and media length inferior to slides length
4510		if ( this.states.loop && length < this.slides.length ) {
4511
4512			var add = Math.ceil( this.slides.length / length ) * length - length;
4513
4514			for ( var i = 0; i < add; i++ ) {
4515
4516				var index = length + i;
4517				gallery[index] = utils.cloneObject( gallery[utils.modulo( length, i )] );
4518				gallery[index].index = index;
4519
4520			}
4521
4522		}
4523
4524	};
4525
4526	/**
4527	 * Set slider
4528	 */
4529	proto.setSlider = function() {
4530
4531		var slider = this.slider,
4532			slides = this.slides;
4533
4534		this.setSizes( slider, slides );
4535		this.setSliderPosition( slider, slides );
4536		this.setSlidesPositions( slides );
4537
4538		// show overlay
4539		this.DOM.overlay.style.opacity = 1;
4540
4541	};
4542
4543	/**
4544	 * Set slider and slide sizes
4545	 * @param {Object} slider
4546	 * @param {Object} slides
4547	 */
4548	proto.setSizes = function( slider, slides ) {
4549
4550		// prevent issue on Google Chrome and mobile devices on orientation change
4551		// this.DOM.holder.clientWidth wrong on mobile devices with Chrome because fixed position
4552		slider.width  = document.body.clientWidth;
4553		slider.height = window.innerHeight; // always right value on IOS devices (prevent Safari top bar issue on resize)
4554		slides.width  = slider.width + Math.round( slider.width * this.options.spacing );
4555
4556	};
4557
4558	/**
4559	 * Set slides/cells positions
4560	 * @param {Object} slides
4561	 */
4562	proto.setSlidesPositions = function( slides ) {
4563
4564		for ( var i = 0; i < slides.length; i++ ) {
4565
4566			slides[i].position = null;
4567			this.setCellPosition( i );
4568
4569		}
4570
4571		this.shiftSlides();
4572
4573	};
4574
4575	/**
4576	 * Set thumbnails positions
4577	 */
4578	proto.setThumbsPosition = function() {
4579
4580		if ( !this.options.thumbnails || this.gallery.initialLength < 2 ) {
4581			return;
4582		}
4583
4584		var thumbs  = this.thumbs,
4585			slider  = this.slider,
4586			holder  = this.DOM.thumbsHolder,
4587			inner   = this.DOM.thumbsInner,
4588			sizes   = this.options.thumbnailSizes,
4589			RTL     = this.options.rightToLeft,
4590			widths  = Object.keys( sizes ).sort( function( a, b ) { return b - a; }),
4591			width   = Math.max.apply( null, widths ),
4592			browser = window.innerWidth,
4593			size;
4594
4595		// get current thumbnail size
4596		for ( var i = 0; i < widths.length; i++ ) {
4597
4598			size = Number( widths[i] );
4599
4600			if ( browser <= size ) {
4601
4602				width = size;
4603
4604			}
4605
4606		}
4607
4608		// set thumbnails data
4609		thumbs.width  = Number( sizes[width].width );
4610		thumbs.gutter = Number( sizes[width].gutter );
4611		thumbs.height = Number( sizes[width].height );
4612		thumbs.size   = thumbs.width + thumbs.gutter;
4613		thumbs.length = this.gallery.initialLength;
4614
4615		// get thumbnails slider total width
4616		var totalWidth = thumbs.length * thumbs.size;
4617
4618		// basic navigation
4619		thumbs.bound = {
4620			left: 0,
4621			right: totalWidth > slider.width ? slider.width - totalWidth : 0
4622		};
4623
4624		if ( RTL ) {
4625
4626			thumbs.bound.right = totalWidth > slider.width ? slider.width - thumbs.size : totalWidth - thumbs.size;
4627			thumbs.bound.left  = totalWidth - thumbs.size;
4628
4629		}
4630
4631		// centered navigation
4632		if ( this.options.thumbnailsNav === 'centered' ) {
4633
4634			thumbs.bound = {
4635				left  : totalWidth > slider.width ? Math.floor( slider.width * 0.5 - thumbs.size * 0.5 ) : Math.floor( totalWidth * 0.5 - thumbs.size * 0.5 ),
4636				right : totalWidth > slider.width ? Math.ceil( slider.width * 0.5 - totalWidth + thumbs.size * 0.5 ) : -Math.ceil( totalWidth * 0.5 - thumbs.size * 0.5 )
4637			};
4638
4639			if ( RTL ) {
4640
4641				thumbs.bound.right = thumbs.bound.left;
4642				thumbs.bound.left  = thumbs.bound.left + totalWidth - thumbs.size;
4643
4644			}
4645
4646		}
4647
4648		// reset thumbnail animation
4649		thumbs.resetAnimate();
4650
4651		var position = this.getThumbPosition( thumbs );
4652
4653		// reset thumbnails position;
4654		thumbs.position.x = position;
4655
4656		// set thumbnails inner position
4657		utils.translate( inner, position, 0 );
4658
4659		// set holder/inner sizes
4660		var hasHeight = this.getThumbHeight();
4661		holder.style.visibility = hasHeight ? '' : 'hidden';
4662		holder.style.height     = hasHeight ? hasHeight + 'px' : '';
4663		inner.style.height      = hasHeight ? thumbs.height + Math.min( 10, thumbs.gutter ) + 'px' : '';
4664		inner.style.width       = thumbs.length * thumbs.size + 'px';
4665		inner.style.right       = totalWidth > slider.width && RTL ? 'auto' : '';
4666
4667	};
4668
4669	/**
4670	 * Set thumbnails positions
4671	 * @param {Object} thumbs
4672	 */
4673	proto.getThumbPosition = function( thumbs ) {
4674
4675		var slider   = this.slider,
4676			gallery  = this.gallery,
4677			thumbNav = this.options.thumbnailsNav,
4678			RTL      = this.isRTL(),
4679			left     = RTL < 0 ? 'right' : 'left',
4680			index    = utils.modulo( gallery.initialLength, gallery.index ),
4681			centered = slider.width * 0.5 - thumbs.size * 0.5,
4682			position = thumbs.bound[left] - index * thumbs.size * RTL;
4683			position = !thumbs.bound[left] ? position + centered : position + (RTL < 0 && thumbNav !== 'centered' ? - centered : 0);
4684
4685		return Math.max( thumbs.bound.right, Math.min( thumbs.bound.left, position ) );
4686
4687	};
4688
4689	/**
4690	 * Set cell (inside slides) positions
4691	 * @param {number} index
4692	 */
4693	proto.setCellPosition = function( index ) {
4694
4695		var cell = this.cells[index];
4696
4697		cell.resetAnimate();
4698
4699		utils.translate( this.slides[index].children[0], 0, 0, 1 );
4700
4701	};
4702
4703	/**
4704	 * Set slider position
4705	 * @param {Object} slider
4706	 * @param {Object} slides
4707	 */
4708	proto.setSliderPosition = function( slider, slides ) {
4709
4710		var RTL  = this.options.rightToLeft,
4711			posX = - slides.index * slides.width;
4712			posX = RTL ? - posX : posX;
4713
4714		slider.resetAnimate();
4715
4716		// set slider position/attraction to animate from right position
4717		slider.position.x =
4718		slider.attraction.x = posX;
4719
4720		slider.bound = {
4721			left  : 0,
4722			right : - ( this.gallery.length - 1 ) * slides.width
4723		};
4724
4725		if ( RTL ) {
4726			slider.bound.left  = - slider.bound.right;
4727			slider.bound.right = 0;
4728		}
4729
4730		utils.translate( this.DOM.slider, posX, 0 );
4731
4732	};
4733
4734	/**
4735	 * Set & init animation (slider & cells)
4736	 */
4737	proto.setAnimation = function() {
4738
4739		// set slider animations
4740		var slider     = this.DOM.slider,
4741			friction   = this.options.friction,
4742			attraction = this.options.attraction;
4743
4744		this.slider = new Animate(
4745			// element to animate
4746			slider,
4747			// initial position
4748			{ x : 0, y : 0 },
4749			// friction coefficient
4750			Math.min( Math.max( friction.slider, 0 ), 1 ),
4751			// attraction coefficient
4752			Math.min( Math.max( attraction.slider, 0 ), 1 )
4753		);
4754
4755		this.slider.on( 'settle.toanimate', this.settleSider.bind( this ) );
4756		this.slider.on( 'render.toanimate', this.renderSlider.bind( this ) );
4757
4758		// set cell animations
4759		var slides = slider.children,
4760			length = slides.length;
4761
4762		for ( var i = 0; i < length; i++ ) {
4763
4764			this.cells[i] = new Animate(
4765				// element to animate
4766				slides[i].children[0],
4767				// initial position
4768				{ x : 0, y : 0, s : 1 },
4769				// friction coefficient
4770				Math.min( Math.max( friction.slide, 0 ), 1 ),
4771				// attraction coefficient
4772				Math.min( Math.max( attraction.slide, 0 ), 1 )
4773			);
4774
4775			this.cells[i].on( 'settle.toanimate', this.settleCell.bind( this ) );
4776			this.cells[i].on( 'render.toanimate', this.renderCell.bind( this ) );
4777
4778		}
4779
4780		// set thumbnail animation
4781		this.thumbs = new Animate(
4782			// element to animate
4783			this.DOM.thumbsInner,
4784			// initial position
4785			{ x : 0 },
4786			// friction coefficient
4787			Math.min( Math.max( friction.thumbs, 0 ), 1 ),
4788			// attraction coefficient
4789			Math.min( Math.max( attraction.thumbs, 0 ), 1 )
4790		);
4791
4792		this.thumbs.on( 'settle.toanimate', this.settleThumbs.bind( this ) );
4793		this.thumbs.on( 'render.toanimate', this.renderThumbs.bind( this ) );
4794
4795	};
4796
4797	/**
4798	 * Executed when the slider settle event is emitted from animate
4799	 * @param {Object} slider
4800	 */
4801	proto.settleSider = function( slider ) {
4802
4803		var media;
4804
4805		utils.dispatchEvent( this, 'modulobox', 'sliderSettled', slider.position );
4806
4807		if ( this.states.open ) {
4808
4809			this.setMedia();
4810			this.replaceState();
4811
4812		}
4813
4814		if ( this.options.fadeIfSettle ) {
4815
4816			var slides = this.slides;
4817
4818			for ( var i = 0; i < slides.length; i++ ) {
4819
4820				var index = slides[i].media;
4821				media = this.gallery[index];
4822
4823				if ( media.dom.loaded ) {
4824					this.showMedia( media, i );
4825				}
4826
4827			}
4828
4829		}
4830
4831	};
4832
4833	/**
4834	 * Executed when the cell settle event is emitted from animate
4835	 * @param {Object} cell
4836	 */
4837	proto.settleCell = function( cell ) {
4838
4839		var gesture = this.gesture;
4840
4841		if ( gesture.closeBy ) {
4842			utils.dispatchEvent( this, 'modulobox', 'panYSettled', null, cell.position );
4843		}
4844
4845		if ( ( gesture.closeBy && gesture.canClose === false ) || !gesture.closeBy ) {
4846			utils.dispatchEvent( this, 'modulobox', 'panZoomSettled', null, cell.position );
4847		}
4848
4849	};
4850
4851	/**
4852	 * Executed when the thumbs slider is settled
4853	 * @param {Object} thumbs
4854	 */
4855	proto.settleThumbs = function( thumbs ) {
4856
4857		utils.dispatchEvent( this, 'modulobox', 'thumbsSettled', null, thumbs.position );
4858
4859	};
4860
4861	/**
4862	 * Executed when the slider is animated/rendered
4863	 * @param {Object} slider
4864	 */
4865	proto.renderSlider = function( slider ) {
4866
4867		// shift slide
4868		this.shiftSlides();
4869
4870		// calculate progress in float percent
4871		var RTL       = this.isRTL(),
4872			length    = this.gallery.initialLength,
4873			indexPos  = - RTL * slider.position.x / this.slides.width,
4874			moduloPos = utils.modulo( length, indexPos ),
4875			progress  = ( moduloPos > length - 0.5 ? 0 : moduloPos ) / ( length - 1 );
4876
4877		// dispatch sliderProgress event
4878		utils.dispatchEvent( this, 'modulobox', 'sliderProgress', null, Math.min( 1, Math.max( 0, progress ) ) );
4879
4880	};
4881
4882	/**
4883	 * Executed when a cell is animated/rendered
4884	 * @param {Object} cell
4885	 */
4886	proto.renderCell = function( cell ) {
4887
4888		this.willClose( cell );
4889
4890		var progress;
4891
4892		// dispatch panYProgress event
4893		if ( this.gesture.type === 'panY' || this.gesture.closeBy || ( this.gesture.type === 'dragSlider' && cell.position.y !== 0 ) ) {
4894
4895			progress = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 );
4896			utils.dispatchEvent( this, 'modulobox', 'panYProgress', null, progress );
4897
4898		}
4899
4900		// dispatch panZoomProgress event
4901		if ( this.gesture.type !== 'panY' && cell.position.s !== 1 ) {
4902
4903			progress = cell.position.s;
4904			utils.dispatchEvent( this, 'modulobox', 'panZoomProgress', null, progress );
4905
4906		}
4907
4908	};
4909
4910	/**
4911	 * Executed when the thumbnails slider is animated/rendered
4912	 * @param {Object} thumbs
4913	 */
4914	proto.renderThumbs = function( thumbs ) {
4915
4916		this.shiftThumbs( thumbs );
4917
4918		// calculate progress in float percent
4919		var progress = thumbs.bound.left !== thumbs.bound.right ? ( thumbs.bound.left - thumbs.position.x ) / ( thumbs.bound.left - thumbs.bound.right ) : 0;
4920
4921		// dispatch thumbsProgress event
4922		utils.dispatchEvent( this, 'modulobox', 'thumbsProgress', null, progress );
4923
4924	};
4925
4926	// ----- gesture events ----- //
4927
4928	/**
4929	 * Touch start event (mouse/touch/pointer)
4930	 * @param {Object} event
4931	 */
4932	proto.touchStart = function( event ) {
4933
4934		// get current target tag name
4935		var element   = event.target,
4936			tagName   = element.tagName,
4937			className = element.className;
4938
4939		// stop slideshow (if not play button touched)
4940		if ( event.which !== 3 && element !== this.buttons.play ) {
4941			this.stopSlideShow();
4942		}
4943
4944		// if right click or element touched is not part of the lightbox or a button/video/input/a
4945		if ( event.which === 3 || !this.isEl( event ) || ['BUTTON', 'VIDEO', 'INPUT', 'A'].indexOf( tagName ) > -1 ) {
4946			return;
4947		}
4948
4949		// add dragging class if an image in a gallery is touched
4950		if ( tagName === 'IMG' && this.gallery.length > 1) {
4951			utils.addClass( this.DOM.holder, this.pre + '-dragging' );
4952		}
4953
4954		// prevent default action
4955		event.preventDefault();
4956
4957		// if share tooltip is open
4958		// after preventDefault() to prevent flicker on touch device (prevent touch scroll)
4959		if ( utils.hasClass( this.DOM.holder, this.pre + '-open-tooltip' ) ) {
4960			return;
4961		}
4962
4963		// if no pointers available
4964		// then 1 first time pointer touches screen (without move)
4965		if ( !this.pointers.length ) {
4966
4967			// reset can close var
4968			this.gesture.canClose = undefined;
4969
4970			// bind move/end events
4971			utils.handleEvents( this, window, this.dragEvents.move, 'touchMove' );
4972			utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd' );
4973
4974		}
4975
4976		// add pointers
4977		this.addPointer( event );
4978
4979		// if thumbs not touched
4980		if ( className.indexOf( '-thumb' ) < 0 ) {
4981
4982			// stop slider animation
4983			this.slider.stopAnimate();
4984
4985			// stop current cell animation
4986			var cell = this.getCell();
4987			if ( Math.round( cell.position.s * 100 ) / 100 !== 1 || this.pointers.length === 2 || this.gesture.closeBy ) {
4988				cell.stopAnimate();
4989			}
4990
4991		} else {
4992
4993			// stop thumbs slider animation
4994			this.thumbs.stopAnimate();
4995
4996		}
4997
4998		// capture gestures
4999		this.gestures( 'start' );
5000
5001	};
5002
5003	/**
5004	 * Touch move event (mouse/touch/pointer)
5005	 * @param {Object} event
5006	 */
5007	proto.touchMove = function( event ) {
5008
5009		// update active pointers
5010		this.updatePointer( event );
5011
5012		var gesture = this.gesture;
5013		// get number of actif pointers
5014		var pointerNb = this.pointers.length;
5015		// check if slider is near settled
5016		var isSettle = this.isSliderSettle();
5017
5018		// Switch panZoom to pan gesture (2 to 1 pointers)
5019		this.switchPointers();
5020
5021		// capture gestures
5022		this.gestures( 'move' );
5023
5024		// if a gesture occured
5025		if ( gesture.type ) {
5026
5027			// handle/dispatch gesture Move
5028			this[gesture.type]( event );
5029			utils.dispatchEvent( this, 'modulobox', gesture.type + 'Move', event, gesture );
5030
5031			// set gesture movement
5032			gesture.move = true;
5033
5034		}
5035		// if dragged more than the threshold offset or pinched
5036		else if ( ( pointerNb === 2 && isSettle ) ||
5037			Math.abs( gesture.dx ) > this.options.threshold ||
5038			Math.abs( gesture.dy ) > this.options.threshold ) {
5039
5040			// add threshold to prevent jump when panY/dragSlider
5041			gesture.sx += gesture.dx;
5042			gesture.sy += gesture.dy;
5043
5044			// set for pinchToclose
5045			gesture.canZoom = this.isZoomable();
5046			// reset closeBy gesture event on first move
5047			gesture.closeBy = false;
5048
5049			// set gesture type
5050			gesture.type = Math.abs( gesture.dx ) < Math.abs( gesture.dy ) / 2 ? false : 'dragSlider';
5051			gesture.type = this.options.dragToClose  && !gesture.type && isSettle ? 'panY' : gesture.type;
5052			gesture.type = ( this.options.pinchToZoom || this.states.zoom ) && gesture.canZoom && isSettle && ( pointerNb === 2 || this.states.zoom ) ? 'panZoom' : gesture.type;
5053			gesture.type = this.options.pinchToClose && gesture.scale < 1 && isSettle && pointerNb === 2 ? 'panZoom' : gesture.type;
5054			gesture.type = event.target.className.indexOf( '-thumb' ) > -1 ? 'dragThumbs' : gesture.type;
5055
5056			// update media before animation
5057			if ( gesture.type === 'dragSlider' ) {
5058				this.setMedia();
5059			}
5060
5061			// replace cell if dragSlider (slider move)
5062			if ( ['dragSlider', 'dragThumbs'].indexOf( gesture.type ) > -1 ) {
5063
5064				var cell = this.getCell();
5065				cell.startAnimate();
5066				cell.releaseDrag();
5067				cell.animateTo({
5068					x : 0,
5069					y : 0,
5070					s : 1
5071				});
5072
5073			}
5074
5075			// replace slider if not settle and if cell move
5076			if ( gesture.type !== 'dragSlider' ) {
5077
5078				var slider = this.slider,
5079					slides = this.slides;
5080
5081				// if the slider does not snap to slides
5082				var RTL = this.isRTL();
5083				if ( - RTL * slider.position.x !== slides.index * slides.width ) {
5084					slider.startAnimate();
5085					slider.releaseDrag();
5086				}
5087
5088			}
5089
5090			// if gesture occured
5091			if ( gesture.type ) {
5092
5093				this.pauseVideo();
5094
5095				// dispatch start event
5096				utils.dispatchEvent( this, 'modulobox', gesture.type + 'Start', event, gesture );
5097
5098				// add dragging class
5099				if ( this.gallery.length > 1 || gesture.type !== 'dragSlider' ) {
5100					utils.addClass( this.DOM.holder, this.pre + '-dragging' );
5101				}
5102
5103			}
5104
5105		}
5106
5107	};
5108
5109	/**
5110	 * Touch end event (mouse/touch/pointer)
5111	 * @param {Object} event
5112	 */
5113	proto.touchEnd = function( event ) {
5114
5115		// delete released pointers
5116		this.deletePointer( event );
5117
5118		// if no more pointer on screen
5119		if ( !this.pointers.length ) {
5120
5121			// remove dragging class
5122			utils.removeClass( this.DOM.holder, this.pre + '-dragging' );
5123
5124			// remove events
5125			utils.handleEvents( this, window, this.dragEvents.move, 'touchMove', false );
5126			utils.handleEvents( this, window, this.dragEvents.end, 'touchEnd', false );
5127
5128			// if slider settle and no gesture happened
5129			if ( this.isSliderSettle() ) {
5130
5131				var className = event.target.className;
5132
5133				// play video
5134				if ( utils.hasClass( event.target, this.pre + '-video' ) ) {
5135					this.appendVideo();
5136				// close lightbox
5137				} 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 ) {
5138
5139					this.close();
5140					return;
5141
5142				}
5143
5144				// check if double tap/click
5145				if ( event.target.tagName === 'IMG' ) {
5146					this.doubleTap( event );
5147				}
5148
5149			}
5150
5151			// handle click on thumbnail
5152			if ( this.options.thumbnails && !this.gesture.move ) {
5153				this.thumbClick( event );
5154			}
5155
5156			// handle/dispatch gesture End
5157			var gestureEnd = this.gesture.type + 'End';
5158			if ( this.gesture.type && typeof this[gestureEnd] === 'function' ) {
5159
5160				this[gestureEnd]( event );
5161				utils.dispatchEvent( this, 'modulobox', gestureEnd, event, this.gesture );
5162
5163			}
5164
5165			// unset gesture
5166			this.gesture.type =
5167				this.gesture.move = false;
5168
5169			// return if gesture trigger a close() (prevent to animate twice)
5170			if ( !this.states.open ) {
5171				return;
5172			}
5173
5174			// make sure cell is settled
5175			var cell = this.getCell();
5176			if ( !cell.settle ) {
5177
5178				cell.startAnimate();
5179				cell.releaseDrag();
5180
5181			}
5182
5183			// make sure slider is settled
5184			var slider = this.slider;
5185			if ( !slider.settle ) {
5186
5187				slider.startAnimate();
5188				slider.releaseDrag();
5189
5190			}
5191
5192		}
5193
5194	};
5195
5196	/**
5197	 * Switch panZoom to pan gestures
5198	 */
5199	proto.switchPointers = function() {
5200
5201		// if switch from 2 pointers to 1 pointer (reset animation)
5202		if ( this.gesture.type === 'panZoom' && this.pointers.length === 1 && this.gesture.distance !== 0 ) {
5203
5204			var moving = this.getCell();
5205
5206			moving.stopAnimate();
5207			moving.startAnimate();
5208
5209			this.gesture.move = false;
5210			this.gestures( 'start' );
5211			this.gestures( 'move' );
5212
5213		}
5214
5215	};
5216
5217	/**
5218	 * Handle double tap
5219	 * @param {Object} event
5220	 */
5221	proto.doubleTap = function( event ) {
5222
5223		event.preventDefault();
5224
5225		// map pointers
5226		var touches = this.mapPointer( event ),
5227			clientX = touches[0].clientX,
5228			clientY = touches[0].clientY;
5229
5230		// check if double tap/click
5231		if ( typeof this.tap !== 'undefined' && // if already tap once
5232			+new Date() - this.tap.delay < 350 && // if delay between 2 taps < 350ms
5233			Math.abs( this.tap.deltaX - clientX ) < 30 && // if distanceX between 2 taps < 30px
5234			Math.abs( this.tap.deltaY - clientY ) < 30 // if distanceY between 2 taps < 30px
5235			) {
5236
5237			// restore idle state if double tap
5238			if ( this.states.tapIdle ) {
5239				clearTimeout( this.states.tapIdle );
5240			}
5241
5242			// zoom on image from pointer coordinates
5243			if ( this.options.doubleTapToZoom ) {
5244				this.zoomTo( clientX, clientY );
5245			}
5246
5247			// reset double tap data
5248			this.tap = undefined;
5249
5250		// if first tap
5251		} else {
5252
5253			// check if it's a touch device only (no mouse detect thanks to states.idle)
5254			if ( this.browser.touchDevice && this.options.timeToIdle && !this.states.idle ) {
5255
5256				// wait to check if double tap will happen
5257				this.states.tapIdle = setTimeout( function() {
5258
5259					var method = !utils.hasClass( this.DOM.holder, this.pre + '-idle' ) ? 'add' : 'remove';
5260					utils[method + 'Class']( this.DOM.holder, this.pre + '-idle' );
5261
5262				}.bind( this ), 350 );
5263
5264			}
5265
5266			// set tap data
5267			this.tap = {
5268				delay  : +new Date(),
5269				deltaX : touches[0].clientX,
5270				deltaY : touches[0].clientY
5271			};
5272
5273		}
5274
5275	};
5276
5277	/**
5278	 * Check if slider is near settled
5279	 * @return {boolean}
5280	 */
5281	proto.isSliderSettle = function() {
5282
5283		// if a gesture occured return
5284		if ( this.gesture.type ) {
5285			return false;
5286		}
5287
5288		// calculate from how much in percent we are from a settled position of the slider (threshold for pinch/close methods)
5289		var RTL      = this.isRTL(),
5290			slides   = this.slides,
5291			width    = slides.width,
5292			toSettle = Math.abs( RTL * this.slider.position.x + slides.index * width ) / width * 100;
5293
5294		// if slider position is a least inferior to 3%  from viewport edges (treshold in percent)
5295		return toSettle <= 3 ? true : false;
5296
5297	};
5298
5299	// ----- handle pointer event ----- //
5300
5301	/**
5302	 * Map mouse and touch events
5303	 * @param {Object} event
5304	 * @return {Object}
5305	 */
5306	proto.mapPointer = function( event ) {
5307
5308		return event.touches ? event.changedTouches : [event];
5309
5310	};
5311
5312	/**
5313	 * Add custom pointer for current event
5314	 * Prevent multiple pointers breaking gesture
5315	 * @param {Object} event
5316	 */
5317	proto.addPointer = function( event ) {
5318
5319		var pointers = this.mapPointer( event );
5320
5321		for ( var i = 0; i < pointers.length; i++ ) {
5322
5323			if ( this.pointers.length < 2 && ['dragSlider', 'panY', 'dragThumbs'].indexOf( this.gesture.type ) === -1 ) {
5324
5325				var ev = pointers[i],
5326					// .pointerId for pointer events, .indentifier for touch events
5327					id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier;
5328
5329				if ( !this.getPointer( id ) ) {
5330
5331					this.pointers[this.pointers.length] = {
5332						id : id,
5333						// clientX/clientY are rounded because "pointer" event return float values
5334						// prevent from inaccurate calculations for the direction on W3C devices capable
5335						x  : Math.round( ev.clientX ),
5336						y  : Math.round( ev.clientY )
5337					};
5338
5339				}
5340
5341			}
5342
5343		}
5344
5345	};
5346
5347	/**
5348	 * Update custom pointer for current event
5349	 * @param {Object} event
5350	 */
5351	proto.updatePointer = function( event ) {
5352
5353		var pointers = this.mapPointer( event );
5354
5355		for ( var i = 0; i < pointers.length; i++ ) {
5356
5357			var ev = pointers[i],
5358				id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier,
5359				pt = this.getPointer( id );
5360
5361			if ( pt ) {
5362
5363				pt.x = Math.round( ev.clientX );
5364				pt.y = Math.round( ev.clientY );
5365
5366			}
5367
5368		}
5369
5370	};
5371
5372	/**
5373	 * Delete custom pointer for current event
5374	 * @param {Object} event
5375	 */
5376	proto.deletePointer = function( event ) {
5377
5378		var pointers = this.mapPointer( event );
5379
5380		for ( var i = 0; i < pointers.length; i++ ) {
5381
5382			var ev = pointers[i],
5383				id = ev.pointerId !== undefined ? ev.pointerId : ev.identifier;
5384
5385			for ( var p = 0; p < this.pointers.length; p++ ) {
5386
5387				if ( this.pointers[p].id === id ) {
5388					this.pointers.splice( p, 1 );
5389				}
5390
5391			}
5392
5393		}
5394
5395	};
5396
5397	/**
5398	 * Check if current pointer was record for the current gesture
5399	 * @param {number} id
5400	 * @return {Object}
5401	 */
5402	proto.getPointer = function( id ) {
5403
5404		for ( var k in this.pointers ) {
5405
5406			if ( this.pointers[k].id === id ) {
5407				return this.pointers[k];
5408			}
5409
5410		}
5411
5412		return null;
5413
5414	};
5415
5416	/**
5417	 * Calculate distances from pointers
5418	 * @param {string} type (start, move)
5419	 */
5420	proto.gestures = function( type ) {
5421
5422		var g = this.gesture,
5423			pointers = this.pointers,
5424			distance;
5425
5426		// if no pointer found return immediately
5427		if ( !pointers.length ) {
5428			return;
5429		}
5430
5431		// set x direction
5432		g.direction = g.x ? pointers[0].x > g.x ? 1 : -1 : 0;
5433
5434		// get current coordinates (1st pointer)
5435		g.x = pointers[0].x;
5436		g.y = pointers[0].y;
5437
5438		if ( pointers.length === 2 ) {
5439
5440			// get current coordinates (2nd pointer)
5441			var x2 = pointers[1].x,
5442				y2 = pointers[1].y;
5443
5444			// calculate current distance between 2 pointers
5445			distance = this.getDistance( [g.x, g.y], [x2, y2] );
5446
5447			// get center coordinates (between 2 pointers)
5448			g.x = g.x - ( g.x - x2 ) / 2;
5449			g.y = g.y - ( g.y - y2 ) / 2;
5450
5451		}
5452
5453		// on touchStart only
5454		if ( type === 'start' ) {
5455
5456			// store initial coordinates
5457			g.dx = 0;
5458			g.dy = 0;
5459			g.sx = g.x;
5460			g.sy = g.y;
5461
5462			// get initial distance (between 2 pointers)
5463			g.distance = distance ? distance : 0;
5464
5465		} else {
5466
5467			// calculate distance in x & y when 1st pointer move
5468			g.dx = g.x - g.sx;
5469			g.dy = g.y - g.sy;
5470
5471			// calculate scale factor (between 2 pointers)
5472			g.scale = distance && g.distance ? distance / g.distance : 1;
5473
5474		}
5475
5476	};
5477
5478	/**
5479	 * Calculate distance between 2 pointers
5480	 * @param {Array} p1 - pointer 1
5481	 * @param {Array} p2 - pointer 2
5482	 * @return {number}
5483	 */
5484	proto.getDistance = function( p1, p2 ) {
5485
5486		var x = p2[0] - p1[0],
5487			y = p2[1] - p1[1];
5488
5489		return Math.sqrt( ( x * x ) + ( y * y ) );
5490
5491	};
5492
5493	// ----- handle gesture ----- //
5494
5495	/**
5496	 * Handle drag gesture in Y axis (1 pointer)
5497	 */
5498	proto.panY = function() {
5499
5500		var moving = this.getCell();
5501
5502		moving.startAnimate();
5503		moving.updateDrag({
5504			x : moving.position.x,
5505			y : moving.start.y + this.gesture.dy,
5506			s : moving.position.s
5507		});
5508
5509	};
5510
5511	/**
5512	 * Handle drag gesture in Y axis (1 pointer) end event
5513	 */
5514	proto.panYEnd = function() {
5515
5516		var posY    = 0,
5517			moving  = this.getCell(),
5518			height  = this.slider.height,
5519			resting = moving.resting.y;
5520
5521		if ( 1 - Math.abs( resting ) / ( height * 0.5 ) < 0.8 ) {
5522
5523			posY = Math.abs( resting ) < height * 0.5 ? Math.abs( resting ) / resting * height * 0.5 : resting;
5524			this.close();
5525
5526			moving.animateTo({
5527				x : 0,
5528				y : posY,
5529				s : posY ? moving.resting.s : 1
5530			});
5531
5532			moving.startAnimate();
5533			moving.releaseDrag();
5534
5535		}
5536
5537	};
5538
5539	/**
5540	 * Handle panZoom gesture (2 pointers only)
5541	 */
5542	proto.panZoom = function() {
5543
5544		var moving  = this.getCell(),
5545			gesture = this.gesture,
5546			bound   = this.mediaViewport( moving.position.s ),
5547			minZoom = this.options.pinchToClose && gesture.canClose ? 0.1 : 0.6,
5548			scale   = Math.min( this.options.maxZoom * 1.5, Math.max( minZoom, moving.start.s * gesture.scale ) ),
5549			posX    = moving.start.x + gesture.dx,
5550			posY    = moving.start.y + gesture.dy,
5551			centerX = gesture.sx - this.slider.width * 0.5,
5552			centerY = gesture.sy - this.slider.height * 0.5;
5553
5554		// prevent to zoom in if can't zoom but pinch to close
5555		if ( !gesture.canZoom || ( !this.options.pinchToZoom && !this.states.zoom ) ) {
5556			scale = Math.min( 1, scale );
5557		}
5558
5559		// prevent to zoom in if disabled (pinch) and keep current scale to pan
5560		if ( !this.options.pinchToZoom && this.states.zoom ) {
5561			scale = moving.position.s;
5562		}
5563
5564		// if pan only, outside viewport, and just started animation
5565		// remove previous friction added outside viewport
5566		if ( !gesture.move && this.pointers.length === 1 ) {
5567
5568			moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0;
5569			moving.start.y += posY > bound.bottom ? posY - bound.bottom : posY < bound.top ? posY - bound.top : 0;
5570
5571		}
5572
5573		// calculate center between 1 or 2 pointers if zoom
5574		posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s );
5575		posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s );
5576
5577		// add friction if pan (1 pointer) and outside viewport
5578		if ( this.pointers.length === 1 ) {
5579
5580			posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX;
5581			posY = posY > bound.bottom ? ( posY + bound.bottom ) * 0.5 : posY < bound.top ? ( posY + bound.top ) * 0.5 : posY;
5582
5583		}
5584
5585		moving.startAnimate();
5586		moving.updateDrag({
5587			x : posX,
5588			y : posY,
5589			s : scale
5590		});
5591
5592		this.updateZoom( scale );
5593
5594	};
5595
5596	/**
5597	 * Handle panZoom gesture (2 pointers only) end event
5598	 */
5599	proto.panZoomEnd = function() {
5600
5601		// if the resting scale value is near at 1 (under 1.1) force unzoom (allow faster gesture recognition)
5602		var moving  = this.getCell(),
5603			gesture = this.gesture,
5604			scale   = moving.resting.s > this.options.maxZoom ? this.options.maxZoom : moving.resting.s < 1 ? 1 : moving.resting.s,
5605			bound   = this.mediaViewport( scale ),
5606			posX, posY;
5607
5608		// if scaled more than authorized then recalculate position to max authorized scale
5609		if ( Math.round( moving.resting.s * 10 ) / 10 > this.options.maxZoom ) {
5610
5611			var centerX = gesture.distance ? gesture.sx - this.slider.width * 0.5 : 0;
5612			var centerY = gesture.distance ? gesture.sy - this.slider.height * 0.5 : 0;
5613
5614			posX = gesture.dx + centerX + ( moving.start.x - centerX ) * ( scale / moving.start.s );
5615			posY = gesture.dy + centerY + ( moving.start.y - centerY ) * ( scale / moving.start.s );
5616			posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX;
5617			posY = posY > bound.bottom ? bound.bottom : posY < bound.top ? bound.top : posY;
5618
5619		} else {
5620
5621			posX = moving.resting.x > bound.left ? bound.left : moving.resting.x < bound.right ? bound.right : undefined;
5622			posY = moving.resting.y > bound.bottom ? bound.bottom : moving.resting.y < bound.top ? bound.top : undefined;
5623
5624		}
5625
5626		// add resting to scale to animate when closing
5627		if ( this.options.pinchToClose && moving.resting.s < 0.8 && gesture.canClose ) {
5628
5629			scale = moving.resting.s < 0.3 ? moving.resting.s : 0.15;
5630			posX = moving.resting.x;
5631			posY = moving.resting.y;
5632			this.close();
5633
5634		}
5635
5636		moving.animateTo({
5637			x : posX,
5638			y : posY,
5639			s : scale !== moving.resting.s ? scale : undefined
5640		});
5641		moving.startAnimate();
5642		moving.releaseDrag();
5643
5644		this.updateZoom( moving.resting.s );
5645
5646	};
5647
5648	/**
5649	 * Handle drag gesture in X axis (1 pointer)
5650	 */
5651	proto.dragThumbs = function() {
5652
5653		var moving = this.thumbs,
5654			bound  = moving.bound,
5655			posX   = moving.start.x + this.gesture.dx;
5656
5657		// add resistance on ends
5658		if ( !this.gesture.move ) {
5659
5660			moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0;
5661			posX = moving.start.x + this.gesture.dx;
5662
5663		}
5664
5665		posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX;
5666
5667		moving.startAnimate();
5668		// to add freescroll
5669		moving.attraction.x = undefined;
5670		moving.updateDrag({
5671			x : posX
5672		});
5673
5674	};
5675
5676	/**
5677	 * Handle drag gesture in X axis (1 pointer)
5678	 */
5679	proto.dragThumbsEnd = function() {
5680
5681		var moving = this.thumbs,
5682			bound  = moving.bound,
5683			posX   = moving.resting.x;
5684
5685		posX = posX > bound.left ? bound.left : posX < bound.right ? bound.right : posX;
5686
5687		// if resting reach bounds/ends
5688		if ( posX !== moving.resting.x ) {
5689
5690			moving.animateTo({
5691				x: posX
5692			});
5693
5694		}
5695		moving.startAnimate();
5696		moving.releaseDrag();
5697
5698	};
5699
5700	/**
5701	 * Handle drag gesture in X axis (1 pointer)
5702	 */
5703	proto.dragSlider = function() {
5704
5705		// prevent dragging slider if one media
5706		if ( this.gallery.length === 1 ) {
5707			return;
5708		}
5709
5710		var moving = this.slider,
5711			posX   = moving.start.x + this.gesture.dx;
5712
5713		// add resistance if no loop
5714		if ( !this.states.loop ) {
5715
5716			var bound = moving.bound;
5717
5718			if ( !this.gesture.move ) {
5719
5720				moving.start.x += posX > bound.left ? posX - bound.left : posX < bound.right ? posX - bound.right : 0;
5721				posX = moving.start.x + this.gesture.dx;
5722
5723			}
5724
5725			posX = posX > bound.left ? ( posX + bound.left ) * 0.5 : posX < bound.right ? ( posX + bound.right ) * 0.5 : posX;
5726
5727		}
5728
5729		moving.startAnimate();
5730		moving.updateDrag({
5731			x : posX
5732		});
5733
5734	};
5735
5736	/**
5737	 * Handle drag gesture in X axis (1 pointer) end event
5738	 */
5739	proto.dragSliderEnd = function() {
5740
5741		// prevent dragging slider if one media
5742		if ( this.gallery.length === 1 ) {
5743			return;
5744		}
5745
5746		var moving    = this.slider,
5747			slides    = this.slides,
5748			oldIndex  = slides.index,
5749			RTL       = this.isRTL(),
5750			restingX  = moving.resting.x,
5751			positionX = moving.position.x;
5752
5753		this.getRestingIndex( positionX, restingX );
5754
5755		if ( oldIndex !== slides.index ) {
5756			this.updateMediaInfo();
5757		}
5758
5759		this.slider.animateTo({
5760			x : - RTL * slides.index * slides.width,
5761			y : undefined,
5762			s : undefined
5763		});
5764
5765		moving.startAnimate();
5766		moving.releaseDrag();
5767
5768	};
5769
5770	/**
5771	 * Calculate new index based on velocity and slider position
5772	 * @param {number} positionX
5773	 * @param {number} restingX
5774	 */
5775	proto.getRestingIndex = function( positionX, restingX ) {
5776
5777		var direction = this.gesture.direction,
5778			gallery   = this.gallery,
5779			slides    = this.slides,
5780			deltaX    = this.gesture.dx,
5781			RTL       = this.isRTL(),
5782			index     = Math.round( - RTL * positionX / slides.width ),
5783			moved     = Math.abs( restingX - positionX );
5784
5785		if ( Math.abs( deltaX ) < slides.width * 0.5 && moved ) {
5786
5787			if ( deltaX > 0 && direction > 0 ) {
5788				index -= 1 * RTL;
5789			} else if ( deltaX < 0 && direction < 0 ) {
5790				index += 1 * RTL;
5791			}
5792
5793		}
5794
5795		// constrain to slide one slide by one slide
5796		var gap = Math.max( -1, Math.min( 1, index - slides.index ) );
5797
5798		// prevent going outside limit if no loop
5799		if ( !this.states.loop &&
5800			( ( gallery.index + gap < 0 ) ||
5801			( gallery.index + gap > gallery.length - 1 ) ) ) {
5802
5803			return;
5804
5805		}
5806
5807		slides.index += gap;
5808
5809	};
5810
5811	/**
5812	 * Shift slide around when slider render
5813	 */
5814	proto.shiftSlides = function() {
5815
5816		var slides  = this.slides,
5817			gallery = this.gallery,
5818			loop    = this.states.loop,
5819			RTL     = this.isRTL(),
5820			from    = RTL * Math.round( - this.slider.position.x / slides.width ) - 2,
5821			to      = from + 5;
5822
5823		if ( !loop && to > gallery.initialLength - 1 ) {
5824			from = gallery.initialLength - 5;
5825			to   = from + 5;
5826		}
5827
5828		if ( !loop && from < 0 ) {
5829			from = 0;
5830			to   = 5;
5831		}
5832
5833		for ( var i = from; i < to; i++ ) {
5834
5835			var position    = RTL * i * slides.width,
5836				slide_index = utils.modulo( slides.length, i ),
5837				slide       = slides[slide_index];
5838
5839			if ( slide.index !== i || slide.position !== position ) {
5840
5841				// set slide virtual index to get the media index
5842				slide.index      = i;
5843				// set position (to resize) if slide width change
5844				slide.position   = position;
5845				// apply new position
5846				slide.style.left = position + 'px';
5847
5848			}
5849
5850		}
5851
5852		// if open only because need to be set after sizing
5853		if ( this.states.open ) {
5854
5855			// load only 3 media to prevent appending one or several DOM while dragging/realasing
5856			// before a touchMove media are set (5), so no reflow/repaint happen during an animation
5857			// improve performance on touch (mobile) devices
5858			this.setMedia( 3 );
5859
5860		}
5861
5862	};
5863
5864	/**
5865	 * Shift thumbnails and load thumbs img
5866	 * @param {Object} thumbs
5867	 */
5868	proto.shiftThumbs = function( thumbs ) {
5869
5870		var child   = this.DOM.thumbsInner.children,
5871			slider  = this.slider,
5872			gallery = this.gallery,
5873			RTL     = this.isRTL(),
5874			length  = child.length,
5875			index   = utils.modulo( gallery.initialLength, gallery.index ),
5876			width   = thumbs.size * ( length ) * 0.5,
5877			center  = Math.round( ( - RTL * thumbs.position.x + RTL * width * 0.5 ) / thumbs.size ),
5878			from    = Math.max( 0, center - Math.floor( length / 2 ) ),
5879			to      = from + length;
5880
5881		// current thumbnail viewport
5882		var tolerance  = slider.width * 0.5,
5883			boundLeft  = thumbs.position.x + tolerance,
5884			boundRight = thumbs.position.x - slider.width - tolerance;
5885
5886		if ( to > gallery.initialLength ) {
5887
5888			to   = gallery.initialLength;
5889			from = to - length;
5890
5891		}
5892
5893		// make sure we are looping through all thumbs
5894		if ( to === gallery.initialLength - 1 && from - to < length ) {
5895
5896			from = gallery.initialLength - length;
5897
5898		}
5899
5900		for ( var i = from; i < to; i++ ) {
5901
5902			var thumb     = child[utils.modulo( length, i )],
5903				position  = RTL * i * thumbs.size + thumbs.gutter * 0.5,
5904				className = this.pre + '-active-thumb',
5905				isActive  = utils.hasClass( thumb, className );
5906
5907			// set thumbnail position
5908			if ( thumb.index !== i || thumb.position !== position ) {
5909
5910				thumb.index      = i;
5911				thumb.position   = position;
5912				thumb.style.left = position + 'px';
5913
5914			}
5915
5916			// set thumbnail size
5917			this.setThumbSize( thumb, thumbs );
5918
5919			// if thumbnail is in the viewport
5920			if ( - thumb.position <= boundLeft && - thumb.position >= boundRight && thumb.media !== i ) {
5921				this.loadThumb( thumb, i );
5922			}
5923
5924			// add active class
5925			if ( isActive && index !== i ) {
5926				utils.removeClass( thumb, className );
5927			} else if ( !isActive && index === i ) {
5928				utils.addClass( thumb, className );
5929			}
5930
5931		}
5932
5933	};
5934
5935	/**
5936	 * Set thumbnail size
5937	 * @param {Object} thumb
5938	 * @param {Object} thumbs
5939	 */
5940	proto.setThumbSize = function( thumb, thumbs ) {
5941
5942		// set thumbnail size if not already assigned
5943		if ( thumb.width !== thumbs.width ||
5944			thumb.height !== thumbs.height ||
5945			thumb.gutter !== thumbs.gutter ) {
5946
5947			// store sizes
5948			thumb.width  = thumbs.width;
5949			thumb.height = thumbs.height;
5950			thumb.gutter = thumbs.gutter;
5951
5952			// set thumbnail size
5953			thumb.style.width  = thumbs.width + 'px';
5954			thumb.style.height = thumbs.height + 'px';
5955
5956		}
5957
5958	};
5959
5960	/**
5961	 * Handle close gesture
5962	 * @param {Object} cell
5963	 */
5964	proto.willClose = function( cell ) {
5965
5966		var opacity      = this.DOM.overlay.style.opacity,
5967			canClose     = this.gesture.canClose,
5968			gestureType  = this.gesture.type,
5969			gestureClose = this.gesture.closeBy,
5970			pinchToClose = gestureType === 'panZoom' || gestureClose === 'panZoom',
5971			dragYToClose = gestureType === 'panY' || gestureClose === 'panY';
5972
5973		// check if the lightbox can be closed with a pinch
5974		if ( cell.position.s > 1.1 && typeof canClose === 'undefined' ) {
5975			this.gesture.canClose = false;
5976		} else if ( cell.position.s < 1 && typeof canClose === 'undefined' ) {
5977			this.gesture.canClose = true;
5978		}
5979
5980		// apply opacity if the user try to close the lightbox
5981		if ( this.options.pinchToClose && pinchToClose && this.gesture.canClose ) {
5982
5983			opacity = cell.position.s;
5984			this.gesture.closeBy = 'panZoom';
5985
5986		} else if ( dragYToClose ) {
5987
5988			opacity = 1 - Math.abs( cell.position.y ) / ( this.slider.height * 0.5 );
5989			this.gesture.closeBy = 'panY';
5990
5991		} else if ( opacity && opacity < 1 ) {
5992
5993			opacity = 1;
5994			this.gesture.closeBy = false;
5995
5996		}
5997
5998		// normalize opacity value
5999		opacity = !opacity ? 1 : Math.max( 0, Math.min( 1, opacity ) );
6000
6001		// hide ui if the user try to close the lightbox
6002		// only if the opacity factor is enough to close (0.8)
6003		var method = opacity <= 0.8 || !opacity ? 'add' : 'remove';
6004		utils[method + 'Class']( this.DOM.holder, this.pre + '-will-close' );
6005
6006		this.DOM.overlay.style.opacity = opacity;
6007
6008	};
6009
6010	// ----- Slide Navigation ----- //
6011
6012	/**
6013	 * Go to prev slide
6014	 * Throttle function to reduce sliding speed
6015	 */
6016	proto.prev = utils.throttle( function() {
6017
6018		if ( !this.gesture.move ) {
6019
6020			this.slideTo( this.slides.index - 1 * this.isRTL() );
6021
6022		}
6023
6024	}, 120 );
6025
6026	/**
6027	 * Go to next slide
6028	 * Throttle function to reduce sliding speed
6029	 */
6030	proto.next = utils.throttle( function() {
6031
6032		if ( !this.gesture.move ) {
6033
6034			this.slideTo( this.slides.index + 1 * this.isRTL() );
6035
6036		}
6037
6038	}, 120 );
6039
6040	/**
6041	 * Slide to gallery index
6042	 * @param {number} to
6043	 * @param {boolean} slideShow
6044	 */
6045	proto.slideTo = function( to, slideShow ) {
6046
6047		var slides     = this.slides,
6048			gallery    = this.gallery,
6049			holder     = this.DOM.slider,
6050			RTL        = this.isRTL(),
6051			length     = gallery.initialLength,
6052			moduloTo   = utils.modulo( length, to ),
6053			moduloFrom = utils.modulo( length, gallery.index ),
6054			slideBy    = moduloTo - moduloFrom,
6055			fromEnds   = length - Math.abs( slideBy );
6056
6057		// if no loop and outside gallery ends
6058		if ( !this.states.loop && ( to < 0 || to > this.gallery.initialLength - 1 ) ) {
6059			return;
6060		}
6061
6062		// if we are closed to a slider end, then go directly
6063		if ( this.states.loop && fromEnds < 3 && fromEnds * 2 < length ) {
6064			slideBy = slideBy < 0 ? fromEnds : -fromEnds;
6065		}
6066
6067		// if same slide but different media index
6068		// necessary in loop and thumb click or slideTo triggered manually
6069		if ( moduloTo === to ) {
6070			to = slides.index + slideBy;
6071		}
6072
6073		// number of slides to animate
6074		slideBy = to - slides.index;
6075
6076		// if same slide selected
6077		if ( !slideBy ) {
6078			return;
6079		}
6080
6081		// unset zoom of current cell
6082		if ( this.states.zoom ) {
6083			this.zoom();
6084		}
6085
6086		// pause video
6087		this.pauseVideo();
6088
6089		// remove share tooltip
6090		this.share();
6091
6092		// if not triggered by the slideshow
6093		if ( !slideShow ) {
6094			this.stopSlideShow();
6095		}
6096
6097		// set new index
6098		slides.index = to;
6099
6100		// prepare slider
6101		var slider = this.slider;
6102
6103		// if the slide will move more than 2 slides
6104		// fake animation
6105		if ( Math.abs( slideBy ) > 2 ) {
6106
6107			// hide slider with css
6108			utils.addClass( holder, this.pre + '-hide' );
6109
6110			// set slider position to selected media
6111			this.setSliderPosition( slider, slides );
6112			this.setSlidesPositions( slides );
6113
6114			// set slider position 2 slide before/after selected media
6115			var moveBy = RTL * slides.width * Math.min( 2, Math.abs( slideBy ) ) * Math.abs( slideBy ) / slideBy;
6116			slider.position.x   =
6117			slider.attraction.x = slider.position.x + moveBy;
6118			utils.translate( holder, slider.position.x, 0 );
6119
6120			// trigger reflow for css transitions.
6121			holder.getClientRects();
6122
6123		}
6124
6125		// update media info with new index
6126		this.updateMediaInfo();
6127
6128		// reveal slider with css transition
6129		utils.removeClass( holder, this.pre + '-hide' );
6130
6131		slider.startAnimate();
6132		slider.releaseDrag();
6133		slider.animateTo({
6134			x : - RTL * to * slides.width,
6135			y : 0,
6136			s : undefined
6137		});
6138
6139	};
6140
6141	/**
6142	 * Handle keydown event
6143	 * @param {Object} event
6144	 */
6145	proto.keyDown = function( event ) {
6146
6147		var key = event.keyCode,
6148			opt = this.options;
6149
6150		if ( opt.prevNextKey ) {
6151
6152			if ( key === 37 ) {
6153				this.prev( event );
6154			} else if ( key === 39 ) {
6155				this.next( event );
6156			}
6157
6158		}
6159
6160		if ( key === 27 && opt.escapeToClose ) {
6161			this.close();
6162		}
6163
6164		// disable scroll on keydown
6165		// Space: 32, Page Up: 33, Page Down: 34, End: 35, Home: 36, Up arrow: 38, Down arrow: 40
6166		if ( !opt.mouseWheel && [32, 33, 34, 35, 36, 38, 40].indexOf( key ) > -1 ) {
6167
6168			event.preventDefault();
6169			return false;
6170
6171		}
6172
6173	};
6174
6175	// ----- Zoom functionality ----- //
6176
6177	/**
6178	 * Handle zoom on icon click
6179	 */
6180	proto.zoom = function() {
6181
6182		this.zoomTo();
6183
6184	};
6185
6186	/**
6187	 * Zoom to a position (x & y) and scale factor on image
6188	 * @param {number} x
6189	 * @param {number} y
6190	 * @param {number} scale
6191	 */
6192	proto.zoomTo = function( x, y, scale ) {
6193
6194		if ( !this.isSliderSettle() || ( !this.isZoomable() && scale > 1 ) ) {
6195			return;
6196		}
6197
6198		this.gesture.closeBy = false;
6199
6200		var media = this.getMedia();
6201		scale = scale ? scale : this.states.zoom ? 1 : media.dom.size.scale;
6202
6203		var cell  = this.getCell(),
6204			bound = this.mediaViewport( scale ),
6205			centX = x ? x - this.slider.width * 0.5 : 0,
6206			centY = y ? y - this.slider.height * 0.5 : 0,
6207			PosX  = scale > 1 ? Math.ceil( centX + ( cell.position.x - centX ) * ( scale / cell.position.s ) ) : 0,
6208			PosY  = scale > 1 ? Math.ceil( centY + ( cell.position.y - centY ) * ( scale / cell.position.s ) ) : 0;
6209
6210		cell.startAnimate();
6211		cell.releaseDrag();
6212		cell.animateTo({
6213			x : PosX > bound.left ? bound.left : PosX < bound.right ? bound.right : PosX,
6214			y : PosY > bound.bottom ? bound.bottom : PosY < bound.top ? bound.top : PosY,
6215			s : scale
6216		});
6217
6218		this.updateZoom( scale );
6219
6220	};
6221
6222	/**
6223	 * Update zoom state when zoomTo
6224	 * @param {number} scale
6225	 */
6226	proto.updateZoom = function( scale ) {
6227
6228		this.states.zoom = scale > 1 ? true : false;
6229		utils[this.states.zoom ? 'addClass' : 'removeClass']( this.DOM.holder, this.pre + '-panzoom' );
6230
6231	};
6232
6233	/**
6234	 * Destroy method
6235	 */
6236	proto.destroy = function() {
6237
6238		// if no instance
6239		if ( !this.GUID ) {
6240			return;
6241		}
6242
6243		// Close only if destroy method is called when opened
6244		if ( this.states.open ) {
6245			this.close();
6246		}
6247
6248		// setup sources
6249		var selectors = this.options.mediaSelector,
6250			sources   = '';
6251
6252		// get all media attached to Modulobox
6253		try {
6254			sources = document.querySelectorAll( selectors );
6255		} catch (error) {}
6256
6257		// remove all events attached to media
6258		for ( var i = 0, l = sources.length; i < l; i++ ) {
6259
6260			var source = sources[i];
6261
6262			if ( source.mobxListener ) {
6263				source.removeEventListener( 'click', source.mobxListener, false );
6264			}
6265
6266		}
6267
6268		// unbind all events
6269		this.bindEvents( false );
6270
6271		// reset slider animation
6272		this.slider.resetAnimate();
6273
6274		// reset cells animations
6275		for ( i = 0; i < this.slides.length; i++ ) {
6276			this.cells[i].resetAnimate();
6277		}
6278
6279		// reset thumbs animation
6280		if ( this.thumbs ) {
6281			this.thumbs.resetAnimate();
6282		}
6283
6284		// remove main holders
6285		this.DOM.holder.parentNode.removeChild( this.DOM.holder );
6286		this.DOM.comment.parentNode.removeChild( this.DOM.comment );
6287
6288		// delete instance;
6289		delete instances[this.GUID];
6290		delete this.GUID;
6291
6292	};
6293
6294	/**
6295	 * jQuery plugin
6296	 */
6297	if ( typeof jQuery !== 'undefined' ) {
6298
6299		( function( $ ) {
6300
6301			$.ModuloBox = function( options ) {
6302
6303				return new ModuloBox( options );
6304
6305			};
6306
6307		})( jQuery );
6308
6309	}
6310
6311	return ModuloBox;
6312
6313}));

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.