PageSourceSearch

https://beaconresort.com.my/wp-content/plugins/hoteller-elementor/assets/js/modulobox.js?ver=7.0.6

js beaconresort.com.my collected 2026-10-05 00:45:48 UTC 186,306 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|webp)/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 >
5590 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.