PageSourceSearch

https://chakakhan.com/wp-content/plugins/musico-elementor/assets/js/modulobox.js?ver=7.1.2

js chakakhan.com collected 2026-09-24 17:21:27 UTC 154,630 bytes, 6,323 lines download raw bytes

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

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.