PageSourceSearch

https://irishaustinsevenclub.ie/wp-content/plugins/nextgen-gallery…/Galleria/galleria-1.6.1.js?ver=1.6.1

js irishaustinsevenclub.ie collected 2026-09-28 17:02:38 UTC 218,797 bytes, 6,991 lines download raw bytes

1/**
2 * Galleria v1.6.1
3 *
4 * Copyright (c) 2010 - 2019 worse is better UG
5 * Licensed under the MIT license
6 *
7 */
8
9( function( window, factory ) {
10    if ( typeof define == 'function' && define.amd ) {
11        define( [ 'jquery' ], function( jQuery ) {
12            return factory( window, jQuery );
13        });
14    } else if ( typeof module == 'object' && module.exports ) {
15        module.exports = factory(
16            window,
17            require('jquery')
18        );
19    } else {
20        // browser global
21        window.Galleria = factory(
22            window,
23            window.jQuery
24        );
25    }
26
27}( window, function factory( window, $, Galleria, undef ) {
28
29/*global jQuery, navigator, Image, module, define */
30
31// some references
32var doc    = window.document,
33    $doc   = $( doc ),
34    $win   = $( window ),
35    jQuery = $,
36
37// native prototypes
38    protoArray = Array.prototype,
39
40// internal constants
41    VERSION = 1.61,
42    DEBUG = fals
vendor: 207,307 bytes, lines 42-6630
42e,
43    TIMEOUT = 30000,
44    DUMMY = false,
45    NAV = navigator.userAgent.toLowerCase(),
46    HASH = window.location.hash.replace(/#\//, ''),
47    PROT = window.location.protocol == "file:" ? "http:" : window.location.protocol,
48    M = Math,
49    F = function(){},
50    FALSE = function() { return false; },
51    MOBILE = !(
52        ( window.screen.width > 1279 && window.devicePixelRatio == 1 ) || // there are not so many mobile devices with more than 1280px and pixelRatio equal to 1 (i.e. retina displays are equal to 2...)
53        ( window.screen.width > 1000 && window.innerWidth < (window.screen.width * .9) ) // this checks in the end if a user is using a resized browser window which is not common on mobile devices
54    ),
55    IE = (function() {
56
57        var v = 3,
58            div = doc.createElement( 'div' ),
59            all = div.getElementsByTagName( 'i' );
60
61        do {
62            div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
63        } while ( all[0] );
64
65        return v > 4 ? v : doc.documentMode || undef;
66
67    }() ),
68    DOM = function() {
69        return {
70            html:  doc.documentElement,
71            body:  doc.body,
72            head:  doc.getElementsByTagName('head')[0],
73            title: doc.title
74        };
75    },
76    IFRAME = window.parent !== window.self,
77
78    // list of Galleria events
79    _eventlist = 'data ready thumbnail loadstart loadfinish image play pause progress ' +
80                 'fullscreen_enter fullscreen_exit idle_enter idle_exit rescale ' +
81                 'lightbox_open lightbox_close lightbox_image',
82
83    _events = (function() {
84
85        var evs = [];
86
87        $.each( _eventlist.split(' '), function( i, ev ) {
88            evs.push( ev );
89
90            // legacy events
91            if ( /_/.test( ev ) ) {
92                evs.push( ev.replace( /_/g, '' ) );
93            }
94        });
95
96        return evs;
97
98    }()),
99
100    // legacy options
101    // allows the old my_setting syntax and converts it to camel case
102
103    _legacyOptions = function( options ) {
104
105        var n;
106
107        if ( typeof options !== 'object' ) {
108
109            // return whatever it was...
110            return options;
111        }
112
113        $.each( options, function( key, value ) {
114            if ( /^[a-z]+_/.test( key ) ) {
115                n = '';
116                $.each( key.split('_'), function( i, k ) {
117                    n += i > 0 ? k.substr( 0, 1 ).toUpperCase() + k.substr( 1 ) : k;
118                });
119                options[ n ] = value;
120                delete options[ key ];
121            }
122        });
123
124        return options;
125    },
126
127    _patchEvent = function( type ) {
128
129        // allow 'image' instead of Galleria.IMAGE
130        if ( $.inArray( type, _events ) > -1 ) {
131            return Galleria[ type.toUpperCase() ];
132        }
133
134        return type;
135    },
136
137    // video providers
138    _video = {
139        youtube: {
140            reg: /https?:\/\/(?:[a-zA_Z]{2,3}.)?(?:youtube\.com\/watch\?)((?:[\w\d\-\_\=]+&amp;(?:amp;)?)*v(?:&lt;[A-Z]+&gt;)?=([0-9a-zA-Z\-\_]+))/i,
141            embed: function() {
142                return PROT + '//www.youtube.com/embed/' + this.id;
143            },
144            get_thumb: function( data ) {
145                return PROT + '//img.youtube.com/vi/'+this.id+'/default.jpg';
146            },
147            get_image: function( data ) {
148                return PROT + '//img.youtube.com/vi/'+this.id+'/maxresdefault.jpg';            }
149        },
150        vimeo: {
151            reg: /https?:\/\/(?:www\.)?(vimeo\.com)\/(?:hd#)?([0-9]+)/i,
152            embed: function() {
153                return PROT + '//player.vimeo.com/video/' + this.id;
154            },
155            getUrl: function() {
156                return PROT + '//vimeo.com/api/v2/video/' + this.id + '.json?callback=?';
157            },
158            get_thumb: function( data ) {
159                return data[0].thumbnail_medium;
160            },
161            get_image: function( data ) {
162                return data[0].thumbnail_large;
163            }
164        },
165        dailymotion: {
166            reg: /https?:\/\/(?:www\.)?(dailymotion\.com)\/video\/([^_]+)/,
167            embed: function() {
168                return PROT + '//www.dailymotion.com/embed/video/' + this.id;
169            },
170            getUrl: function() {
171                return 'https://api.dailymotion.com/video/' + this.id + '?fields=thumbnail_240_url,thumbnail_720_url&callback=?';
172            },
173            get_thumb: function( data ) {
174                return data.thumbnail_240_url;
175            },
176            get_image: function( data ) {
177                return data.thumbnail_720_url;
178            }
179        },
180        _inst: []
181    },
182    Video = function( type, id ) {
183
184        for( var i=0; i<_video._inst.length; i++ ) {
185            if ( _video._inst[i].id === id && _video._inst[i].type == type ) {
186                return _video._inst[i];
187            }
188        }
189
190        this.type = type;
191        this.id = id;
192        this.readys = [];
193
194        _video._inst.push(this);
195
196        var self = this;
197
198        $.extend( this, _video[type] );
199
200        _videoThumbs = function(data) {
201            self.data = data;
202            $.each( self.readys, function( i, fn ) {
203                fn( self.data );
204            });
205            self.readys = [];
206        };
207
208        if ( this.hasOwnProperty('getUrl') ) {
209            $.getJSON( this.getUrl(), _videoThumbs);
210        } else {
211            window.setTimeout(_videoThumbs, 400);
212        }
213
214        this.getMedia = function( type, callback, fail ) {
215            fail = fail || F;
216            var self = this;
217            var success = function( data ) {
218                callback( self['get_'+type]( data ) );
219            };
220            try {
221                if ( self.data ) {
222                    success( self.data );
223                } else {
224                    self.readys.push( success );
225                }
226            } catch(e) {
227                fail();
228            }
229        };
230    },
231
232    // utility for testing the video URL and getting the video ID
233    _videoTest = function( url ) {
234        var match;
235        for ( var v in _video ) {
236            match = url && _video[v].reg && url.match( _video[v].reg );
237            if( match && match.length ) {
238                return {
239                    id: match[2],
240                    provider: v
241                };
242            }
243        }
244        return false;
245    },
246
247    // native fullscreen handler
248    _nativeFullscreen = {
249
250        support: (function() {
251            var html = DOM().html;
252            return !IFRAME && ( html.requestFullscreen || html.msRequestFullscreen || html.mozRequestFullScreen || html.webkitRequestFullScreen );
253        }()),
254
255        callback: F,
256
257        enter: function( instance, callback, elem ) {
258
259            this.instance = instance;
260
261            this.callback = callback || F;
262
263            elem = elem || DOM().html;
264            if ( elem.requestFullscreen ) {
265                elem.requestFullscreen();
266            }
267            else if ( elem.msRequestFullscreen ) {
268                elem.msRequestFullscreen();
269            }
270            else if ( elem.mozRequestFullScreen ) {
271                elem.mozRequestFullScreen();
272            }
273            else if ( elem.webkitRequestFullScreen ) {
274                elem.webkitRequestFullScreen();
275            }
276        },
277
278        exit: function( callback ) {
279
280            this.callback = callback || F;
281
282            if ( doc.exitFullscreen ) {
283                doc.exitFullscreen();
284            }
285            else if ( doc.msExitFullscreen ) {
286                doc.msExitFullscreen();
287            }
288            else if ( doc.mozCancelFullScreen ) {
289                doc.mozCancelFullScreen();
290            }
291            else if ( doc.webkitCancelFullScreen ) {
292                doc.webkitCancelFullScreen();
293            }
294        },
295
296        instance: null,
297
298        listen: function() {
299
300            if ( !this.support ) {
301                return;
302            }
303
304            var handler = function() {
305
306                if ( !_nativeFullscreen.instance ) {
307                    return;
308                }
309                var fs = _nativeFullscreen.instance._fullscreen;
310
311                if ( doc.fullscreen || doc.mozFullScreen || doc.webkitIsFullScreen || ( doc.msFullscreenElement && doc.msFullscreenElement !== null ) ) {
312                    fs._enter( _nativeFullscreen.callback );
313                } else {
314                    fs._exit( _nativeFullscreen.callback );
315                }
316            };
317            doc.addEventListener( 'fullscreenchange', handler, false );
318            doc.addEventListener( 'MSFullscreenChange', handler, false );
319            doc.addEventListener( 'mozfullscreenchange', handler, false );
320            doc.addEventListener( 'webkitfullscreenchange', handler, false );
321        }
322    },
323
324    // the internal gallery holder
325    _galleries = [],
326
327    // the internal instance holder
328    _instances = [],
329
330    // flag for errors
331    _hasError = false,
332
333    // canvas holder
334    _canvas = false,
335
336    // instance pool, holds the galleries until themeLoad is triggered
337    _pool = [],
338
339    // Run galleries from theme trigger
340    _loadedThemes = [],
341    _themeLoad = function( theme ) {
342
343        _loadedThemes.push(theme);
344
345        // run the instances we have in the pool
346        // and apply the last theme if not specified
347        $.each( _pool, function( i, instance ) {
348            if ( instance._options.theme == theme.name || (!instance._initialized && !instance._options.theme) ) {
349                instance.theme = theme;
350                instance._init.call( instance );
351            }
352        });
353    },
354
355    // the Utils singleton
356    Utils = (function() {
357
358        return {
359
360            // legacy support for clearTimer
361            clearTimer: function( id ) {
362                $.each( Galleria.get(), function() {
363                    this.clearTimer( id );
364                });
365            },
366
367            // legacy support for addTimer
368            addTimer: function( id ) {
369                $.each( Galleria.get(), function() {
370                    this.addTimer( id );
371                });
372            },
373
374            array : function( obj ) {
375                return protoArray.slice.call(obj, 0);
376            },
377
378            create : function( className, nodeName ) {
379                nodeName = nodeName || 'div';
380                var elem = doc.createElement( nodeName );
381                elem.className = className;
382                return elem;
383            },
384
385            removeFromArray : function( arr, elem ) {
386                $.each(arr, function(i, el) {
387                    if ( el == elem ) {
388                        arr.splice(i, 1);
389                        return false;
390                    }
391                });
392                return arr;
393            },
394
395            getScriptPath : function( src ) {
396
397                // the currently executing script is always the last
398                src = src || $('script:last').attr('src');
399                var slices = src.split('/');
400
401                if (slices.length == 1) {
402                    return '';
403                }
404
405                slices.pop();
406
407                return slices.join('/') + '/';
408            },
409
410            // CSS3 transitions, added in 1.2.4
411            animate : (function() {
412
413                // detect transition
414                var transition = (function( style ) {
415                    var props = 'transition WebkitTransition MozTransition OTransition'.split(' '),
416                        i;
417
418                    // disable css3 animations in opera until stable
419                    if ( window.opera ) {
420                        return false;
421                    }
422
423                    for ( i = 0; props[i]; i++ ) {
424                        if ( typeof style[ props[ i ] ] !== 'undefined' ) {
425                            return props[ i ];
426                        }
427                    }
428                    return false;
429                }(( doc.body || doc.documentElement).style ));
430
431                // map transitionend event
432                var endEvent = {
433                    MozTransition: 'transitionend',
434                    OTransition: 'oTransitionEnd',
435                    WebkitTransition: 'webkitTransitionEnd',
436                    transition: 'transitionend'
437                }[ transition ];
438
439                // map bezier easing conversions
440                var easings = {
441                    _default: [0.25, 0.1, 0.25, 1],
442                    galleria: [0.645, 0.045, 0.355, 1],
443                    galleriaIn: [0.55, 0.085, 0.68, 0.53],
444                    galleriaOut: [0.25, 0.46, 0.45, 0.94],
445                    ease: [0.25, 0, 0.25, 1],
446                    linear: [0.25, 0.25, 0.75, 0.75],
447                    'ease-in': [0.42, 0, 1, 1],
448                    'ease-out': [0, 0, 0.58, 1],
449                    'ease-in-out': [0.42, 0, 0.58, 1]
450                };
451
452                // function for setting transition css for all browsers
453                var setStyle = function( elem, value, suffix ) {
454                    var css = {};
455                    suffix = suffix || 'transition';
456                    $.each( 'webkit moz ms o'.split(' '), function() {
457                        css[ '-' + this + '-' + suffix ] = value;
458                    });
459                    elem.css( css );
460                };
461
462                // clear styles
463                var clearStyle = function( elem ) {
464                    setStyle( elem, 'none', 'transition' );
465                    if ( Galleria.WEBKIT && Galleria.TOUCH ) {
466                        setStyle( elem, 'translate3d(0,0,0)', 'transform' );
467                        if ( elem.data('revert') ) {
468                            elem.css( elem.data('revert') );
469                            elem.data('revert', null);
470                        }
471                    }
472                };
473
474                // various variables
475                var change, strings, easing, syntax, revert, form, css;
476
477                // the actual animation method
478                return function( elem, to, options ) {
479
480                    // extend defaults
481                    options = $.extend({
482                        duration: 400,
483                        complete: F,
484                        stop: false
485                    }, options);
486
487                    // cache jQuery instance
488                    elem = $( elem );
489
490                    if ( !options.duration ) {
491                        elem.css( to );
492                        options.complete.call( elem[0] );
493                        return;
494                    }
495
496                    // fallback to jQuery's animate if transition is not supported
497                    if ( !transition ) {
498                        elem.animate(to, options);
499                        return;
500                    }
501
502                    // stop
503                    if ( options.stop ) {
504                        // clear the animation
505                        elem.off( endEvent );
506                        clearStyle( elem );
507                    }
508
509                    // see if there is a change
510                    change = false;
511                    $.each( to, function( key, val ) {
512                        css = elem.css( key );
513                        if ( Utils.parseValue( css ) != Utils.parseValue( val ) ) {
514                            change = true;
515                        }
516                        // also add computed styles for FF
517                        elem.css( key, css );
518                    });
519                    if ( !change ) {
520                        window.setTimeout( function() {
521                            options.complete.call( elem[0] );
522                        }, options.duration );
523                        return;
524                    }
525
526                    // the css strings to be applied
527                    strings = [];
528
529                    // the easing bezier
530                    easing = options.easing in easings ? easings[ options.easing ] : easings._default;
531
532                    // the syntax
533                    syntax = ' ' + options.duration + 'ms' + ' cubic-bezier('  + easing.join(',') + ')';
534
535                    // add a tiny timeout so that the browsers catches any css changes before animating
536                    window.setTimeout( (function(elem, endEvent, to, syntax) {
537                        return function() {
538
539                            // attach the end event
540                            elem.one(endEvent, (function( elem ) {
541                                return function() {
542
543                                    // clear the animation
544                                    clearStyle(elem);
545
546                                    // run the complete method
547                                    options.complete.call(elem[0]);
548                                };
549                            }( elem )));
550
551                            // do the webkit translate3d for better performance on iOS
552                            if( Galleria.WEBKIT && Galleria.TOUCH ) {
553
554                                revert = {};
555                                form = [0,0,0];
556
557                                $.each( ['left', 'top'], function(i, m) {
558                                    if ( m in to ) {
559                                        form[ i ] = ( Utils.parseValue( to[ m ] ) - Utils.parseValue(elem.css( m )) ) + 'px';
560                                        revert[ m ] = to[ m ];
561                                        delete to[ m ];
562                                    }
563                                });
564
565                                if ( form[0] || form[1]) {
566
567                                    elem.data('revert', revert);
568
569                                    strings.push('-webkit-transform' + syntax);
570
571                                    // 3d animate
572                                    setStyle( elem, 'translate3d(' + form.join(',') + ')', 'transform');
573                                }
574                            }
575
576                            // push the animation props
577                            $.each(to, function( p, val ) {
578                                strings.push(p + syntax);
579                            });
580
581                            // set the animation styles
582                            setStyle( elem, strings.join(',') );
583
584                            // animate
585                            elem.css( to );
586
587                        };
588                    }(elem, endEvent, to, syntax)), 2);
589                };
590            }()),
591
592            removeAlpha : function( elem ) {
593                if ( elem instanceof jQuery ) {
594                    elem = elem[0];
595                }
596                if ( IE < 9 && elem ) {
597
598                    var style = elem.style,
599                        currentStyle = elem.currentStyle,
600                        filter = currentStyle && currentStyle.filter || style.filter || "";
601
602                    if ( /alpha/.test( filter ) ) {
603                        style.filter = filter.replace( /alpha\([^)]*\)/i, '' );
604                    }
605                }
606            },
607
608            forceStyles : function( elem, styles ) {
609                elem = $(elem);
610                if ( elem.attr( 'style' ) ) {
611                    elem.data( 'styles', elem.attr( 'style' ) ).removeAttr( 'style' );
612                }
613                elem.css( styles );
614            },
615
616            revertStyles : function() {
617                $.each( Utils.array( arguments ), function( i, elem ) {
618
619                    elem = $( elem );
620                    elem.removeAttr( 'style' );
621
622                    elem.attr('style',''); // "fixes" webkit bug
623
624                    if ( elem.data( 'styles' ) ) {
625                        elem.attr( 'style', elem.data('styles') ).data( 'styles', null );
626                    }
627                });
628            },
629
630            moveOut : function( elem ) {
631                Utils.forceStyles( elem, {
632                    position: 'absolute',
633                    left: -10000
634                });
635            },
636
637            moveIn : function() {
638                Utils.revertStyles.apply( Utils, Utils.array( arguments ) );
639            },
640
641            hide : function( elem, speed, callback ) {
642
643                callback = callback || F;
644
645                var $elem = $(elem);
646                elem = $elem[0];
647
648                // save the value if not exist
649                if (! $elem.data('opacity') ) {
650                    $elem.data('opacity', $elem.css('opacity') );
651                }
652
653                // always hide
654                var style = { opacity: 0 };
655
656                if (speed) {
657
658                    var complete = IE < 9 && elem ? function() {
659                        Utils.removeAlpha( elem );
660                        elem.style.visibility = 'hidden';
661                        callback.call( elem );
662                    } : callback;
663
664                    Utils.animate( elem, style, {
665                        duration: speed,
666                        complete: complete,
667                        stop: true
668                    });
669                } else {
670                    if ( IE < 9 && elem ) {
671                        Utils.removeAlpha( elem );
672                        elem.style.visibility = 'hidden';
673                    } else {
674                        $elem.css( style );
675                    }
676                }
677            },
678
679            show : function( elem, speed, callback ) {
680
681                callback = callback || F;
682
683                var $elem = $(elem);
684                elem = $elem[0];
685
686                // bring back saved opacity
687                var saved = parseFloat( $elem.data('opacity') ) || 1,
688                    style = { opacity: saved };
689
690                // animate or toggle
691                if (speed) {
692
693                    if ( IE < 9 ) {
694                        $elem.css('opacity', 0);
695                        elem.style.visibility = 'visible';
696                    }
697
698                    var complete = IE < 9 && elem ? function() {
699                        if ( style.opacity == 1 ) {
700                            Utils.removeAlpha( elem );
701                        }
702                        callback.call( elem );
703                    } : callback;
704
705                    Utils.animate( elem, style, {
706                        duration: speed,
707                        complete: complete,
708                        stop: true
709                    });
710                } else {
711                    if ( IE < 9 && style.opacity == 1 && elem ) {
712                        Utils.removeAlpha( elem );
713                        elem.style.visibility = 'visible';
714                    } else {
715                        $elem.css( style );
716                    }
717                }
718            },
719
720            wait : function(options) {
721
722                Galleria._waiters = Galleria._waiters || [];
723
724                options = $.extend({
725                    until : FALSE,
726                    success : F,
727                    error : function() { Galleria.raise('Could not complete wait function.'); },
728                    timeout: 3000
729                }, options);
730
731                var start = Utils.timestamp(),
732                    elapsed,
733                    now,
734                    tid,
735                    fn = function() {
736                        now = Utils.timestamp();
737                        elapsed = now - start;
738                        Utils.removeFromArray( Galleria._waiters, tid );
739                        if ( options.until( elapsed ) ) {
740                            options.success();
741                            return false;
742                        }
743                        if (typeof options.timeout == 'number' && now >= start + options.timeout) {
744                            options.error();
745                            return false;
746                        }
747                        Galleria._waiters.push( tid = window.setTimeout(fn, 10) );
748                    };
749                Galleria._waiters.push( tid = window.setTimeout(fn, 10) );
750            },
751
752            toggleQuality : function( img, force ) {
753
754                if ( ( IE !== 7 && IE !== 8 ) || !img || img.nodeName.toUpperCase() != 'IMG' ) {
755                    return;
756                }
757
758                if ( typeof force === 'undefined' ) {
759                    force = img.style.msInterpolationMode === 'nearest-neighbor';
760                }
761
762                img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
763            },
764
765            insertStyleTag : function( styles, id ) {
766
767                if ( id && $( '#'+id ).length ) {
768                    return;
769                }
770
771                var style = doc.createElement( 'style' );
772                if ( id ) {
773                    style.id = id;
774                }
775
776                DOM().head.appendChild( style );
777
778                if ( style.styleSheet ) { // IE
779                    style.styleSheet.cssText = styles;
780                } else {
781                    var cssText = doc.createTextNode( styles );
782                    style.appendChild( cssText );
783                }
784            },
785
786            // a loadscript method that works for local scripts
787            loadScript: function( url, callback ) {
788
789                var done = false,
790                    script = $('<scr'+'ipt>').attr({
791                        src: url,
792                        async: true
793                    }).get(0);
794
795               // Attach handlers for all browsers
796               script.onload = script.onreadystatechange = function() {
797                   if ( !done && (!this.readyState ||
798                       this.readyState === 'loaded' || this.readyState === 'complete') ) {
799
800                       done = true;
801
802                       // Handle memory leak in IE
803                       script.onload = script.onreadystatechange = null;
804
805                       if (typeof callback === 'function') {
806                           callback.call( this, this );
807                       }
808                   }
809               };
810
811               DOM().head.appendChild( script );
812            },
813
814            // parse anything into a number
815            parseValue: function( val ) {
816                if (typeof val === 'number') {
817                    return val;
818                } else if (typeof val === 'string') {
819                    var arr = val.match(/\-?\d|\./g);
820                    return arr && arr.constructor === Array ? arr.join('')*1 : 0;
821                } else {
822                    return 0;
823                }
824            },
825
826            // timestamp abstraction
827            timestamp: function() {
828                return new Date().getTime();
829            },
830
831            loadCSS : function( href, id, callback ) {
832
833                var link,
834                    length;
835
836                // look for manual css
837                $('link[rel=stylesheet]').each(function() {
838                    if ( new RegExp( href ).test( this.href ) ) {
839                        link = this;
840                        return false;
841                    }
842                });
843
844                if ( typeof id === 'function' ) {
845                    callback = id;
846                    id = undef;
847                }
848
849                callback = callback || F; // dirty
850
851                // if already present, return
852                if ( link ) {
853                    callback.call( link, link );
854                    return link;
855                }
856
857                // save the length of stylesheets to check against
858                length = doc.styleSheets.length;
859
860                // check for existing id
861                if( $( '#' + id ).length ) {
862
863                    $( '#' + id ).attr( 'href', href );
864                    length--;
865
866                } else {
867                    link = $( '<link>' ).attr({
868                        rel: 'stylesheet',
869                        href: href,
870                        id: id
871                    }).get(0);
872
873                    var styles = $('link[rel="stylesheet"], style');
874                    if ( styles.length ) {
875                        styles.get(0).parentNode.insertBefore( link, styles[0] );
876                    } else {
877                        DOM().head.appendChild( link );
878                    }
879
880                    if ( IE && length >= 31 ) {
881                        Galleria.raise( 'You have reached the browser stylesheet limit (31)', true );
882                        return;
883                    }
884                }
885
886                if ( typeof callback === 'function' ) {
887
888                    // First check for dummy element (new in 1.2.8)
889                    var $loader = $('<s>').attr( 'id', 'galleria-loader' ).hide().appendTo( DOM().body );
890
891                    Utils.wait({
892                        until: function() {
893                            return $loader.height() > 0;
894                        },
895                        success: function() {
896                            $loader.remove();
897                            callback.call( link, link );
898                        },
899                        error: function() {
900                            $loader.remove();
901
902                            // If failed, tell the dev to download the latest theme
903                            Galleria.raise( 'Theme CSS could not load after 20 sec. ' + ( Galleria.QUIRK ?
904                                'Your browser is in Quirks Mode, please add a correct doctype.' :
905                                'Please download the latest theme.' ), true );
906                        },
907                        timeout: 5000
908                    });
909                }
910                return link;
911            }
912        };
913    }()),
914
915    // play icon
916    _playIcon = function( container ) {
917
918        var css = '.galleria-videoicon{width:60px;height:60px;position:absolute;top:50%;left:50%;z-index:1;' +
919                  'margin:-30px 0 0 -30px;cursor:pointer;background:#000;background:rgba(0,0,0,.8);border-radius:3px;-webkit-transition:all 150ms}' +
920                  '.galleria-videoicon i{width:0px;height:0px;border-style:solid;border-width:10px 0 10px 16px;display:block;' +
921                  'border-color:transparent transparent transparent #ffffff;margin:20px 0 0 22px}.galleria-image:hover .galleria-videoicon{background:#000}';
922
923        Utils.insertStyleTag( css, 'galleria-videoicon' );
924
925        return $( Utils.create( 'galleria-videoicon' ) ).html( '<i></i>' ).appendTo( container )
926            .click( function() { $( this ).siblings( 'img' ).mouseup(); });
927    },
928
929    // the transitions holder
930    _transitions = (function() {
931
932        var _slide = function(params, complete, fade, door) {
933
934            var easing = this.getOptions('easing'),
935                distance = this.getStageWidth(),
936                from = { left: distance * ( params.rewind ? -1 : 1 ) },
937                to = { left: 0 };
938
939            if ( fade ) {
940                from.opacity = 0;
941                to.opacity = 1;
942            } else {
943                from.opacity = 1;
944            }
945
946            $(params.next).css(from);
947
948            Utils.animate(params.next, to, {
949                duration: params.speed,
950                complete: (function( elems ) {
951                    return function() {
952                        complete();
953                        elems.css({
954                            left: 0
955                        });
956                    };
957                }( $( params.next ).add( params.prev ) )),
958                queue: false,
959                easing: easing
960            });
961
962            if (door) {
963                params.rewind = !params.rewind;
964            }
965
966            if (params.prev) {
967
968                from = { left: 0 };
969                to = { left: distance * ( params.rewind ? 1 : -1 ) };
970
971                if ( fade ) {
972                    from.opacity = 1;
973                    to.opacity = 0;
974                }
975
976                $(params.prev).css(from);
977                Utils.animate(params.prev, to, {
978                    duration: params.speed,
979                    queue: false,
980                    easing: easing,
981                    complete: function() {
982                        $(this).css('opacity', 0);
983                    }
984                });
985            }
986        };
987
988        return {
989
990            active: false,
991
992            init: function( effect, params, complete ) {
993                if ( _transitions.effects.hasOwnProperty( effect ) ) {
994                    _transitions.effects[ effect ].call( this, params, complete );
995                }
996            },
997
998            effects: {
999
1000                fade: function(params, complete) {
1001                    $(params.next).css({
1002                        opacity: 0,
1003                        left: 0
1004                    });
1005                    Utils.animate(params.next, {
1006                        opacity: 1
1007                    },{
1008                        duration: params.speed,
1009                        complete: complete
1010                    });
1011                    if (params.prev) {
1012                        $(params.prev).css('opacity',1).show();
1013                        Utils.animate(params.prev, {
1014                            opacity: 0
1015                        },{
1016                            duration: params.speed
1017                        });
1018                    }
1019                },
1020
1021                flash: function(params, complete) {
1022                    $(params.next).css({
1023                        opacity: 0,
1024                        left: 0
1025                    });
1026                    if (params.prev) {
1027                        Utils.animate( params.prev, {
1028                            opacity: 0
1029                        },{
1030                            duration: params.speed/2,
1031                            complete: function() {
1032                                Utils.animate( params.next, {
1033                                    opacity:1
1034                                },{
1035                                    duration: params.speed,
1036                                    complete: complete
1037                                });
1038                            }
1039                        });
1040                    } else {
1041                        Utils.animate( params.next, {
1042                            opacity: 1
1043                        },{
1044                            duration: params.speed,
1045                            complete: complete
1046                        });
1047                    }
1048                },
1049
1050                pulse: function(params, complete) {
1051                    if (params.prev) {
1052                        $(params.prev).hide();
1053                    }
1054                    $(params.next).css({
1055                        opacity: 0,
1056                        left: 0
1057                    }).show();
1058                    Utils.animate(params.next, {
1059                        opacity:1
1060                    },{
1061                        duration: params.speed,
1062                        complete: complete
1063                    });
1064                },
1065
1066                slide: function(params, complete) {
1067                    _slide.apply( this, Utils.array( arguments ) );
1068                },
1069
1070                fadeslide: function(params, complete) {
1071                    _slide.apply( this, Utils.array( arguments ).concat( [true] ) );
1072                },
1073
1074                doorslide: function(params, complete) {
1075                    _slide.apply( this, Utils.array( arguments ).concat( [false, true] ) );
1076                }
1077            }
1078        };
1079    }());
1080
1081// listen to fullscreen
1082_nativeFullscreen.listen();
1083
1084// create special click:fast event for fast touch interaction
1085$.event.special['click:fast'] = {
1086    propagate: true,
1087    add: function(handleObj) {
1088
1089        var getCoords = function(e) {
1090            if ( e.touches && e.touches.length ) {
1091                var touch = e.touches[0];
1092                return {
1093                    x: touch.pageX,
1094                    y: touch.pageY
1095                };
1096            }
1097        };
1098
1099        var def = {
1100            touched: false,
1101            touchdown: false,
1102            coords: { x:0, y:0 },
1103            evObj: {}
1104        };
1105
1106        $(this).data({
1107            clickstate: def,
1108            timer: 0
1109        }).on('touchstart.fast', function(e) {
1110            window.clearTimeout($(this).data('timer'));
1111            $(this).data('clickstate', {
1112                touched: true,
1113                touchdown: true,
1114                coords: getCoords(e.originalEvent),
1115                evObj: e
1116            });
1117        }).on('touchmove.fast', function(e) {
1118            var coords = getCoords(e.originalEvent),
1119                state = $(this).data('clickstate'),
1120                distance = Math.max(
1121                    Math.abs(state.coords.x - coords.x),
1122                    Math.abs(state.coords.y - coords.y)
1123                );
1124            if ( distance > 6 ) {
1125                $(this).data('clickstate', $.extend(state, {
1126                    touchdown: false
1127                }));
1128            }
1129        }).on('touchend.fast', function(e) {
1130            var $this = $(this),
1131                state = $this.data('clickstate');
1132            if(state.touchdown) {
1133              handleObj.handler.call(this, e);
1134            }
1135            $this.data('timer', window.setTimeout(function() {
1136                $this.data('clickstate', def);
1137            }, 400));
1138        }).on('click.fast', function(e) {
1139            var state = $(this).data('clickstate');
1140            if ( state.touched ) {
1141                return false;
1142            }
1143            $(this).data('clickstate', def);
1144            handleObj.handler.call(this, e);
1145        });
1146    },
1147    remove: function() {
1148        $(this).off('touchstart.fast touchmove.fast touchend.fast click.fast');
1149    }
1150};
1151
1152// trigger resize on orientationchange (IOS7)
1153$win.on( 'orientationchange', function() {
1154    $(this).resize();
1155});
1156
1157/**
1158    The main Galleria class
1159
1160    @class
1161    @constructor
1162
1163    @example var gallery = new Galleria();
1164
1165    @author http://wib.io
1166
1167    @requires jQuery
1168
1169*/
1170
1171Galleria = function() {
1172
1173    var self = this;
1174
1175    // internal options
1176    this._options = {};
1177
1178    // flag for controlling play/pause
1179    this._playing = false;
1180
1181    // internal interval for slideshow
1182    this._playtime = 5000;
1183
1184    // internal variable for the currently active image
1185    this._active = null;
1186
1187    // the internal queue, arrayified
1188    this._queue = { length: 0 };
1189
1190    // the internal data array
1191    this._data = [];
1192
1193    // the internal dom collection
1194    this._dom = {};
1195
1196    // the internal thumbnails array
1197    this._thumbnails = [];
1198
1199    // the internal layers array
1200    this._layers = [];
1201
1202    // internal init flag
1203    this._initialized = false;
1204
1205    // internal firstrun flag
1206    this._firstrun = false;
1207
1208    // global stagewidth/height
1209    this._stageWidth = 0;
1210    this._stageHeight = 0;
1211
1212    // target holder
1213    this._target = undef;
1214
1215    // bind hashes
1216    this._binds = [];
1217
1218    // instance id
1219    this._id = parseInt(M.random()*10000, 10);
1220
1221    // add some elements
1222    var divs =  'container stage images image-nav image-nav-left image-nav-right ' +
1223                'info info-text info-title info-description ' +
1224                'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
1225                'loader counter tooltip',
1226        spans = 'current total';
1227
1228    $.each( divs.split(' '), function( i, elemId ) {
1229        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId );
1230    });
1231
1232    $.each( spans.split(' '), function( i, elemId ) {
1233        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId, 'span' );
1234    });
1235
1236    // the internal keyboard object
1237    // keeps reference of the keybinds and provides helper methods for binding keys
1238    var keyboard = this._keyboard = {
1239
1240        keys : {
1241            'UP': 38,
1242            'DOWN': 40,
1243            'LEFT': 37,
1244            'RIGHT': 39,
1245            'RETURN': 13,
1246            'ESCAPE': 27,
1247            'BACKSPACE': 8,
1248            'SPACE': 32
1249        },
1250
1251        map : {},
1252
1253        bound: false,
1254
1255        press: function(e) {
1256            var key = e.keyCode || e.which;
1257            if ( key in keyboard.map && typeof keyboard.map[key] === 'function' ) {
1258                keyboard.map[key].call(self, e);
1259            }
1260        },
1261
1262        attach: function(map) {
1263
1264            var key, up;
1265
1266            for( key in map ) {
1267                if ( map.hasOwnProperty( key ) ) {
1268                    up = key.toUpperCase();
1269                    if ( up in keyboard.keys ) {
1270                        keyboard.map[ keyboard.keys[up] ] = map[key];
1271                    } else {
1272                        keyboard.map[ up ] = map[key];
1273                    }
1274                }
1275            }
1276            if ( !keyboard.bound ) {
1277                keyboard.bound = true;
1278                $doc.on('keydown', keyboard.press);
1279            }
1280        },
1281
1282        detach: function() {
1283            keyboard.bound = false;
1284            keyboard.map = {};
1285            $doc.off('keydown', keyboard.press);
1286        }
1287    };
1288
1289    // internal controls for keeping track of active / inactive images
1290    var controls = this._controls = {
1291
1292        0: undef,
1293
1294        1: undef,
1295
1296        active : 0,
1297
1298        swap : function() {
1299            controls.active = controls.active ? 0 : 1;
1300        },
1301
1302        getActive : function() {
1303            return self._options.swipe ? controls.slides[ self._active ] : controls[ controls.active ];
1304        },
1305
1306        getNext : function() {
1307            return self._options.swipe ? controls.slides[ self.getNext( self._active ) ] : controls[ 1 - controls.active ];
1308        },
1309
1310        slides : [],
1311
1312        frames: [],
1313
1314        layers: []
1315    };
1316
1317    // internal carousel object
1318    var carousel = this._carousel = {
1319
1320        // shortcuts
1321        next: self.$('thumb-nav-right'),
1322        prev: self.$('thumb-nav-left'),
1323
1324        // cache the width
1325        width: 0,
1326
1327        // track the current position
1328        current: 0,
1329
1330        // cache max value
1331        max: 0,
1332
1333        // save all hooks for each width in an array
1334        hooks: [],
1335
1336        // update the carousel
1337        // you can run this method anytime, f.ex on window.resize
1338        update: function() {
1339            var w = 0,
1340                h = 0,
1341                hooks = [0];
1342
1343            $.each( self._thumbnails, function( i, thumb ) {
1344                if ( thumb.ready ) {
1345                    w += thumb.outerWidth || $( thumb.container ).outerWidth( true );
1346                    // Due to a bug in jquery, outerwidth() returns the floor of the actual outerwidth,
1347                    // if the browser is zoom to a value other than 100%. height() returns the floating point value.
1348                    var containerWidth = $( thumb.container).width();
1349                    w += containerWidth - M.floor(containerWidth);
1350
1351                    hooks[ i+1 ] = w;
1352                    h = M.max( h, thumb.outerHeight || $( thumb.container).outerHeight( true ) );
1353                }
1354            });
1355
1356            self.$( 'thumbnails' ).css({
1357                width: w,
1358                height: h
1359            });
1360
1361            carousel.max = w;
1362            carousel.hooks = hooks;
1363            carousel.width = self.$( 'thumbnails-list' ).width();
1364            carousel.setClasses();
1365
1366            self.$( 'thumbnails-container' ).toggleClass( 'galleria-carousel', w > carousel.width );
1367
1368            // one extra calculation
1369            carousel.width = self.$( 'thumbnails-list' ).width();
1370
1371            // todo: fix so the carousel moves to the left
1372        },
1373
1374        bindControls: function() {
1375
1376            var i;
1377
1378            carousel.next.on( 'click:fast', function(e) {
1379                e.preventDefault();
1380
1381                if ( self._options.carouselSteps === 'auto' ) {
1382
1383                    for ( i = carousel.current; i < carousel.hooks.length; i++ ) {
1384                        if ( carousel.hooks[i] - carousel.hooks[ carousel.current ] > carousel.width ) {
1385                            carousel.set(i - 2);
1386                            break;
1387                        }
1388                    }
1389
1390                } else {
1391                    carousel.set( carousel.current + self._options.carouselSteps);
1392                }
1393            });
1394
1395            carousel.prev.on( 'click:fast', function(e) {
1396                e.preventDefault();
1397
1398                if ( self._options.carouselSteps === 'auto' ) {
1399
1400                    for ( i = carousel.current; i >= 0; i-- ) {
1401                        if ( carousel.hooks[ carousel.current ] - carousel.hooks[i] > carousel.width ) {
1402                            carousel.set( i + 2 );
1403                            break;
1404                        } else if ( i === 0 ) {
1405                            carousel.set( 0 );
1406                            break;
1407                        }
1408                    }
1409                } else {
1410                    carousel.set( carousel.current - self._options.carouselSteps );
1411                }
1412            });
1413        },
1414
1415        // calculate and set positions
1416        set: function( i ) {
1417            i = M.max( i, 0 );
1418            while ( carousel.hooks[i - 1] + carousel.width >= carousel.max && i >= 0 ) {
1419                i--;
1420            }
1421            carousel.current = i;
1422            carousel.animate();
1423        },
1424
1425        // get the last position
1426        getLast: function(i) {
1427            return ( i || carousel.current ) - 1;
1428        },
1429
1430        // follow the active image
1431        follow: function(i) {
1432
1433            //don't follow if position fits
1434            if ( i === 0 || i === carousel.hooks.length - 2 ) {
1435                carousel.set( i );
1436                return;
1437            }
1438
1439            // calculate last position
1440            var last = carousel.current;
1441            while( carousel.hooks[last] - carousel.hooks[ carousel.current ] <
1442                   carousel.width && last <= carousel.hooks.length ) {
1443                last ++;
1444            }
1445
1446            // set position
1447            if ( i - 1 < carousel.current ) {
1448                carousel.set( i - 1 );
1449            } else if ( i + 2 > last) {
1450                carousel.set( i - last + carousel.current + 2 );
1451            }
1452        },
1453
1454        // helper for setting disabled classes
1455        setClasses: function() {
1456            carousel.prev.toggleClass( 'disabled', !carousel.current );
1457            carousel.next.toggleClass( 'disabled', carousel.hooks[ carousel.current ] + carousel.width >= carousel.max );
1458        },
1459
1460        // the animation method
1461        animate: function(to) {
1462            carousel.setClasses();
1463            var num = carousel.hooks[ carousel.current ] * -1;
1464
1465            if ( isNaN( num ) ) {
1466                return;
1467            }
1468
1469            // FF 24 bug
1470            self.$( 'thumbnails' ).css('left', function() {
1471                return $(this).css('left');
1472            });
1473
1474            Utils.animate(self.get( 'thumbnails' ), {
1475                left: num
1476            },{
1477                duration: self._options.carouselSpeed,
1478                easing: self._options.easing,
1479                queue: false
1480            });
1481        }
1482    };
1483
1484    // tooltip control
1485    // added in 1.2
1486    var tooltip = this._tooltip = {
1487
1488        initialized : false,
1489
1490        open: false,
1491
1492        timer: 'tooltip' + self._id,
1493
1494        swapTimer: 'swap' + self._id,
1495
1496        init: function() {
1497
1498            tooltip.initialized = true;
1499
1500            var css = '.galleria-tooltip{padding:3px 8px;max-width:50%;background:#ffe;color:#000;z-index:3;position:absolute;font-size:11px;line-height:1.3;' +
1501                      'opacity:0;box-shadow:0 0 2px rgba(0,0,0,.4);-moz-box-shadow:0 0 2px rgba(0,0,0,.4);-webkit-box-shadow:0 0 2px rgba(0,0,0,.4);}';
1502
1503            Utils.insertStyleTag( css, 'galleria-tooltip' );
1504
1505            self.$( 'tooltip' ).css({
1506                opacity: 0.8,
1507                visibility: 'visible',
1508                display: 'none'
1509            });
1510
1511        },
1512
1513        // move handler
1514        move: function( e ) {
1515            var mouseX = self.getMousePosition(e).x,
1516                mouseY = self.getMousePosition(e).y,
1517                $elem = self.$( 'tooltip' ),
1518                x = mouseX,
1519                y = mouseY,
1520                height = $elem.outerHeight( true ) + 1,
1521                width = $elem.outerWidth( true ),
1522                limitY = height + 15;
1523
1524            var maxX = self.$( 'container' ).width() - width - 2,
1525                maxY = self.$( 'container' ).height() - height - 2;
1526
1527            if ( !isNaN(x) && !isNaN(y) ) {
1528
1529                x += 10;
1530                y -= ( height+8 );
1531
1532                x = M.max( 0, M.min( maxX, x ) );
1533                y = M.max( 0, M.min( maxY, y ) );
1534
1535                if( mouseY < limitY ) {
1536                    y = limitY;
1537                }
1538
1539                $elem.css({ left: x, top: y });
1540            }
1541        },
1542
1543        // bind elements to the tooltip
1544        // you can bind multiple elementIDs using { elemID : function } or { elemID : string }
1545        // you can also bind single DOM elements using bind(elem, string)
1546        bind: function( elem, value ) {
1547
1548            // todo: revise if alternative tooltip is needed for mobile devices
1549            if (Galleria.TOUCH) {
1550                return;
1551            }
1552
1553            if (! tooltip.initialized ) {
1554                tooltip.init();
1555            }
1556
1557            var mouseout = function() {
1558                self.$( 'container' ).off( 'mousemove', tooltip.move );
1559                self.clearTimer( tooltip.timer );
1560
1561                self.$( 'tooltip' ).stop().animate({
1562                    opacity: 0
1563                }, 200, function() {
1564
1565                    self.$( 'tooltip' ).hide();
1566
1567                    self.addTimer( tooltip.swapTimer, function() {
1568                        tooltip.open = false;
1569                    }, 1000);
1570                });
1571            };
1572
1573            var hover = function( elem, value) {
1574
1575                tooltip.define( elem, value );
1576
1577                $( elem ).hover(function() {
1578
1579                    self.clearTimer( tooltip.swapTimer );
1580                    self.$('container').off( 'mousemove', tooltip.move ).on( 'mousemove', tooltip.move ).trigger( 'mousemove' );
1581                    tooltip.show( elem );
1582
1583                    self.addTimer( tooltip.timer, function() {
1584                        self.$( 'tooltip' ).stop().show().animate({
1585                            opacity: 1
1586                        });
1587                        tooltip.open = true;
1588
1589                    }, tooltip.open ? 0 : 500);
1590
1591                }, mouseout).click(mouseout);
1592            };
1593
1594            if ( typeof value === 'string' ) {
1595                hover( ( elem in self._dom ? self.get( elem ) : elem ), value );
1596            } else {
1597                // asume elemID here
1598                $.each( elem, function( elemID, val ) {
1599                    hover( self.get(elemID), val );
1600                });
1601            }
1602        },
1603
1604        show: function( elem ) {
1605
1606            elem = $( elem in self._dom ? self.get(elem) : elem );
1607
1608            var text = elem.data( 'tt' ),
1609                mouseup = function( e ) {
1610
1611                    // attach a tiny settimeout to make sure the new tooltip is filled
1612                    window.setTimeout( (function( ev ) {
1613                        return function() {
1614                            tooltip.move( ev );
1615                        };
1616                    }( e )), 10);
1617
1618                    elem.off( 'mouseup', mouseup );
1619
1620                };
1621
1622            text = typeof text === 'function' ? text() : text;
1623
1624            if ( ! text ) {
1625                return;
1626            }
1627
1628            self.$( 'tooltip' ).html( text.replace(/\s/, '&#160;') );
1629
1630            // trigger mousemove on mouseup in case of click
1631            elem.on( 'mouseup', mouseup );
1632        },
1633
1634        define: function( elem, value ) {
1635
1636            // we store functions, not strings
1637            if (typeof value !== 'function') {
1638                var s = value;
1639                value = function() {
1640                    return s;
1641                };
1642            }
1643
1644            elem = $( elem in self._dom ? self.get(elem) : elem ).data('tt', value);
1645
1646            tooltip.show( elem );
1647
1648        }
1649    };
1650
1651    // internal fullscreen control
1652    var fullscreen = this._fullscreen = {
1653
1654        scrolled: 0,
1655
1656        crop: undef,
1657
1658        active: false,
1659
1660        prev: $(),
1661
1662        beforeEnter: function(fn){ fn(); },
1663        beforeExit:  function(fn){ fn(); },
1664
1665        keymap: self._keyboard.map,
1666
1667        parseCallback: function( callback, enter ) {
1668
1669            return _transitions.active ? function() {
1670                if ( typeof callback == 'function' ) {
1671                    callback.call(self);
1672                }
1673                var active = self._controls.getActive(),
1674                    next = self._controls.getNext();
1675
1676                self._scaleImage( next );
1677                self._scaleImage( active );
1678
1679                if ( enter && self._options.trueFullscreen ) {
1680                    // Firefox bug, revise later
1681                    $( active.container ).add( next.container ).trigger( 'transitionend' );
1682                }
1683
1684            } : callback;
1685
1686        },
1687
1688        enter: function( callback ) {
1689
1690            fullscreen.beforeEnter(function() {
1691
1692                callback = fullscreen.parseCallback( callback, true );
1693
1694                if ( self._options.trueFullscreen && _nativeFullscreen.support ) {
1695
1696                    // do some stuff prior animation for wmoother transitions
1697
1698                    fullscreen.active = true;
1699
1700                    Utils.forceStyles( self.get('container'), {
1701                        width: '100%',
1702                        height: '100%'
1703                    });
1704
1705                    self.rescale();
1706
1707                    if ( Galleria.MAC ) {
1708                        if ( !( Galleria.SAFARI && /version\/[1-5]/.test(NAV)) ) {
1709                            self.$('container').css('opacity', 0).addClass('fullscreen');
1710                            window.setTimeout(function() {
1711                                fullscreen.scale();
1712                                self.$('container').css('opacity', 1);
1713                            }, 50);
1714                        } else {
1715                            self.$('stage').css('opacity', 0);
1716                            window.setTimeout(function() {
1717                                fullscreen.scale();
1718                                self.$('stage').css('opacity', 1);
1719                            },4);
1720                        }
1721                    } else {
1722                        self.$('container').addClass('fullscreen');
1723                    }
1724
1725                    $win.resize( fullscreen.scale );
1726
1727                    _nativeFullscreen.enter( self, callback, self.get('container') );
1728
1729                } else {
1730
1731                    fullscreen.scrolled = $win.scrollTop();
1732                    if( !Galleria.TOUCH ) {
1733                        window.scrollTo(0, 0);
1734                    }
1735
1736                    fullscreen._enter( callback );
1737                }
1738            });
1739
1740        },
1741
1742        _enter: function( callback ) {
1743
1744            fullscreen.active = true;
1745
1746            if ( IFRAME ) {
1747
1748                fullscreen.iframe = (function() {
1749
1750                    var elem,
1751                        refer = doc.referrer,
1752                        test = doc.createElement('a'),
1753                        loc = window.location;
1754
1755                    test.href = refer;
1756
1757                    if( test.protocol != loc.protocol ||
1758                        test.hostname != loc.hostname ||
1759                        test.port != loc.port ) {
1760                            Galleria.raise('Parent fullscreen not available. Iframe protocol, domains and ports must match.');
1761                            return false;
1762                        }
1763
1764                    fullscreen.pd = window.parent.document;
1765
1766                    $( fullscreen.pd ).find('iframe').each(function() {
1767                        var idoc = this.contentDocument || this.contentWindow.document;
1768                        if ( idoc === doc ) {
1769                            elem = this;
1770                            return false;
1771                        }
1772                    });
1773
1774                    return elem;
1775                }());
1776
1777            }
1778
1779            // hide the image until rescale is complete
1780            Utils.hide( self.getActiveImage() );
1781
1782            if ( IFRAME && fullscreen.iframe ) {
1783                fullscreen.iframe.scrolled = $( window.parent ).scrollTop();
1784                window.parent.scrollTo(0, 0);
1785            }
1786
1787            var data = self.getData(),
1788                options = self._options,
1789                inBrowser = !self._options.trueFullscreen || !_nativeFullscreen.support,
1790                htmlbody = {
1791                    height: '100%',
1792                    overflow: 'hidden',
1793                    margin:0,
1794                    padding:0
1795                };
1796
1797            if (inBrowser) {
1798
1799                self.$('container').addClass('fullscreen');
1800                fullscreen.prev = self.$('container').prev();
1801
1802                if ( !fullscreen.prev.length ) {
1803                    fullscreen.parent = self.$( 'container' ).parent();
1804                }
1805
1806                // move
1807                self.$( 'container' ).appendTo( 'body' );
1808
1809                // begin styleforce
1810
1811                Utils.forceStyles(self.get('container'), {
1812                    position: Galleria.TOUCH ? 'absolute' : 'fixed',
1813                    top: 0,
1814                    left: 0,
1815                    width: '100%',
1816                    height: '100%',
1817                    zIndex: 10000
1818                });
1819                Utils.forceStyles( DOM().html, htmlbody );
1820                Utils.forceStyles( DOM().body, htmlbody );
1821            }
1822
1823            if ( IFRAME && fullscreen.iframe ) {
1824                Utils.forceStyles( fullscreen.pd.documentElement, htmlbody );
1825                Utils.forceStyles( fullscreen.pd.body, htmlbody );
1826                Utils.forceStyles( fullscreen.iframe, $.extend( htmlbody, {
1827                    width: '100%',
1828                    height: '100%',
1829                    top: 0,
1830                    left: 0,
1831                    position: 'fixed',
1832                    zIndex: 10000,
1833                    border: 'none'
1834                }));
1835            }
1836
1837            // temporarily attach some keys
1838            // save the old ones first in a cloned object
1839            fullscreen.keymap = $.extend({}, self._keyboard.map);
1840
1841            self.attachKeyboard({
1842                escape: self.exitFullscreen,
1843                right: self.next,
1844                left: self.prev
1845            });
1846
1847            // temporarily save the crop
1848            fullscreen.crop = options.imageCrop;
1849
1850            // set fullscreen options
1851            if ( options.fullscreenCrop != undef ) {
1852                options.imageCrop = options.fullscreenCrop;
1853            }
1854
1855            // swap to big image if it's different from the display image
1856            if ( data && data.big && data.image !== data.big ) {
1857                var big    = new Galleria.Picture(),
1858                    cached = big.isCached( data.big ),
1859                    index  = self.getIndex(),
1860                    thumb  = self._thumbnails[ index ];
1861
1862                self.trigger( {
1863                    type: Galleria.LOADSTART,
1864                    cached: cached,
1865                    rewind: false,
1866                    index: index,
1867                    imageTarget: self.getActiveImage(),
1868                    thumbTarget: thumb,
1869                    galleriaData: data
1870                });
1871
1872                big.load( data.big, function( big ) {
1873                    self._scaleImage( big, {
1874                        complete: function( big ) {
1875                            self.trigger({
1876                                type: Galleria.LOADFINISH,
1877                                cached: cached,
1878                                index: index,
1879                                rewind: false,
1880                                imageTarget: big.image,
1881                                thumbTarget: thumb
1882                            });
1883                            var image = self._controls.getActive().image;
1884                            if ( image ) {
1885                                $( image ).width( big.image.width ).height( big.image.height )
1886                                    .attr( 'style', $( big.image ).attr('style') );
1887                                if (big.image.src.srcset) {
1888                                    $( image ).attr( 'srcset', big.image.src.srcset );
1889                                }
1890                                if (big.image.src.sizes) {
1891                                    $( image ).attr( 'sizes', big.image.src.sizes );
1892                                }
1893                                $( image ).attr( 'src', big.image.src );
1894                            }
1895                        }
1896                    });
1897                });
1898
1899                var n = self.getNext(index),
1900                    p = new Galleria.Picture(),
1901                    ndata = self.getData( n );
1902                p.preload( self.isFullscreen() && ndata.big ? ndata.big : ndata.image );
1903            }
1904
1905            // init the first rescale and attach callbacks
1906
1907            self.rescale(function() {
1908
1909                self.addTimer(false, function() {
1910                    // show the image after 50 ms
1911                    if ( inBrowser ) {
1912                        Utils.show( self.getActiveImage() );
1913                    }
1914
1915                    if (typeof callback === 'function') {
1916                        callback.call( self );
1917                    }
1918                    self.rescale();
1919
1920                }, 100);
1921
1922                self.trigger( Galleria.FULLSCREEN_ENTER );
1923            });
1924
1925            if ( !inBrowser ) {
1926                Utils.show( self.getActiveImage() );
1927            } else {
1928                $win.resize( fullscreen.scale );
1929            }
1930
1931        },
1932
1933        scale : function() {
1934            self.rescale();
1935        },
1936
1937        exit: function( callback ) {
1938
1939            fullscreen.beforeExit(function() {
1940
1941                callback = fullscreen.parseCallback( callback );
1942
1943                if ( self._options.trueFullscreen && _nativeFullscreen.support ) {
1944                    _nativeFullscreen.exit( callback );
1945                } else {
1946                    fullscreen._exit( callback );
1947                }
1948            });
1949        },
1950
1951        _exit: function( callback ) {
1952
1953            fullscreen.active = false;
1954
1955            var inBrowser = !self._options.trueFullscreen || !_nativeFullscreen.support,
1956                $container = self.$( 'container' ).removeClass( 'fullscreen' );
1957
1958            // move back
1959            if ( fullscreen.parent ) {
1960                fullscreen.parent.prepend( $container );
1961            } else {
1962                $container.insertAfter( fullscreen.prev );
1963            }
1964
1965            if ( inBrowser ) {
1966                Utils.hide( self.getActiveImage() );
1967
1968                // revert all styles
1969                Utils.revertStyles( self.get('container'), DOM().html, DOM().body );
1970
1971                // scroll back
1972                if( !Galleria.TOUCH ) {
1973                    window.scrollTo(0, fullscreen.scrolled);
1974                }
1975
1976                // reload iframe src manually
1977                var frame = self._controls.frames[ self._controls.active ];
1978                if ( frame && frame.image ) {
1979                    frame.image.src = frame.image.src;
1980                }
1981            }
1982
1983            if ( IFRAME && fullscreen.iframe ) {
1984                Utils.revertStyles( fullscreen.pd.documentElement, fullscreen.pd.body, fullscreen.iframe );
1985                if ( fullscreen.iframe.scrolled ) {
1986                    window.parent.scrollTo(0, fullscreen.iframe.scrolled );
1987                }
1988            }
1989
1990            // detach all keyboard events and apply the old keymap
1991            self.detachKeyboard();
1992            self.attachKeyboard( fullscreen.keymap );
1993
1994            // bring back cached options
1995            self._options.imageCrop = fullscreen.crop;
1996
1997            // return to original image
1998            var big = self.getData().big,
1999                image = self._controls.getActive().image;
2000
2001            if ( !self.getData().iframe && image && big && big == image.src ) {
2002
2003                window.setTimeout(function(src) {
2004                    return function() {
2005                        image.src = src;
2006                    };
2007                }( self.getData().image ), 1 );
2008
2009            }
2010
2011            self.rescale(function() {
2012                self.addTimer(false, function() {
2013
2014                    // show the image after 50 ms
2015                    if ( inBrowser ) {
2016                        Utils.show( self.getActiveImage() );
2017                    }
2018
2019                    if ( typeof callback === 'function' ) {
2020                        callback.call( self );
2021                    }
2022
2023                    $win.trigger( 'resize' );
2024
2025                }, 50);
2026                self.trigger( Galleria.FULLSCREEN_EXIT );
2027            });
2028
2029            $win.off('resize', fullscreen.scale);
2030        }
2031    };
2032
2033    // the internal idle object for controlling idle states
2034    var idle = this._idle = {
2035
2036        trunk: [],
2037
2038        bound: false,
2039
2040        active: false,
2041
2042        add: function(elem, to, from, hide) {
2043            if ( !elem || Galleria.TOUCH ) {
2044                return;
2045            }
2046            if (!idle.bound) {
2047                idle.addEvent();
2048            }
2049            elem = $(elem);
2050
2051            if ( typeof from == 'boolean' ) {
2052                hide = from;
2053                from = {};
2054            }
2055
2056            from = from || {};
2057
2058            var extract = {},
2059                style;
2060
2061            for ( style in to ) {
2062                if ( to.hasOwnProperty( style ) ) {
2063                    extract[ style ] = elem.css( style );
2064                }
2065            }
2066
2067            elem.data('idle', {
2068                from: $.extend( extract, from ),
2069                to: to,
2070                complete: true,
2071                busy: false
2072            });
2073
2074            if ( !hide ) {
2075                idle.addTimer();
2076            } else {
2077                elem.css( to );
2078            }
2079            idle.trunk.push(elem);
2080        },
2081
2082        remove: function(elem) {
2083
2084            elem = $(elem);
2085
2086            $.each(idle.trunk, function(i, el) {
2087                if ( el && el.length && !el.not(elem).length ) {
2088                    elem.css( elem.data( 'idle' ).from );
2089                    idle.trunk.splice(i, 1);
2090                }
2091            });
2092
2093            if (!idle.trunk.length) {
2094                idle.removeEvent();
2095                self.clearTimer( idle.timer );
2096            }
2097        },
2098
2099        addEvent : function() {
2100            idle.bound = true;
2101            self.$('container').on( 'mousemove click', idle.showAll );
2102            if ( self._options.idleMode == 'hover' ) {
2103                self.$('container').on( 'mouseleave', idle.hide );
2104            }
2105        },
2106
2107        removeEvent : function() {
2108            idle.bound = false;
2109            self.$('container').on( 'mousemove click', idle.showAll );
2110            if ( self._options.idleMode == 'hover' ) {
2111                self.$('container').off( 'mouseleave', idle.hide );
2112            }
2113        },
2114
2115        addTimer : function() {
2116            if( self._options.idleMode == 'hover' ) {
2117                return;
2118            }
2119            self.addTimer( 'idle', function() {
2120                idle.hide();
2121            }, self._options.idleTime );
2122        },
2123
2124        hide : function() {
2125
2126            if ( !self._options.idleMode || self.getIndex() === false ) {
2127                return;
2128            }
2129
2130            self.trigger( Galleria.IDLE_ENTER );
2131
2132            var len = idle.trunk.length;
2133
2134            $.each( idle.trunk, function(i, elem) {
2135
2136                var data = elem.data('idle');
2137
2138                if (! data) {
2139                    return;
2140                }
2141
2142                elem.data('idle').complete = false;
2143
2144                Utils.animate( elem, data.to, {
2145                    duration: self._options.idleSpeed,
2146                    complete: function() {
2147                        if ( i == len-1 ) {
2148                            idle.active = false;
2149                        }
2150                    }
2151                });
2152            });
2153        },
2154
2155        showAll : function() {
2156
2157            self.clearTimer( 'idle' );
2158
2159            $.each( idle.trunk, function( i, elem ) {
2160                idle.show( elem );
2161            });
2162        },
2163
2164        show: function(elem) {
2165
2166            var data = elem.data('idle');
2167
2168            if ( !idle.active || ( !data.busy && !data.complete ) ) {
2169
2170                data.busy = true;
2171
2172                self.trigger( Galleria.IDLE_EXIT );
2173
2174                self.clearTimer( 'idle' );
2175
2176                Utils.animate( elem, data.from, {
2177                    duration: self._options.idleSpeed/2,
2178                    complete: function() {
2179                        idle.active = true;
2180                        $(elem).data('idle').busy = false;
2181                        $(elem).data('idle').complete = true;
2182                    }
2183                });
2184
2185            }
2186            idle.addTimer();
2187        }
2188    };
2189
2190    // internal lightbox object
2191    // creates a predesigned lightbox for simple popups of images in galleria
2192    var lightbox = this._lightbox = {
2193
2194        width : 0,
2195
2196        height : 0,
2197
2198        initialized : false,
2199
2200        active : null,
2201
2202        image : null,
2203
2204        elems : {},
2205
2206        keymap: false,
2207
2208        init : function() {
2209
2210            if ( lightbox.initialized ) {
2211                return;
2212            }
2213            lightbox.initialized = true;
2214
2215            // create some elements to work with
2216            var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
2217                el = {},
2218                op = self._options,
2219                css = '',
2220                abs = 'position:absolute;',
2221                prefix = 'lightbox-',
2222                cssMap = {
2223                    overlay:    'position:fixed;display:none;opacity:'+op.overlayOpacity+';filter:alpha(opacity='+(op.overlayOpacity*100)+
2224                                ');top:0;left:0;width:100%;height:100%;background:'+op.overlayBackground+';z-index:99990',
2225                    box:        'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
2226                    shadow:     abs+'background:#000;width:100%;height:100%;',
2227                    content:    abs+'background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
2228                    info:       abs+'bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
2229                    close:      abs+'top:10px;right:10px;height:20px;width:20px;background:#fff;text-align:center;cursor:pointer;color:#444;font:16px/22px arial,sans-serif;z-index:99999',
2230                    image:      abs+'top:10px;left:10px;right:10px;bottom:30px;overflow:hidden;display:block;',
2231                    prevholder: abs+'width:50%;top:0;bottom:40px;cursor:pointer;',
2232                    nextholder: abs+'width:50%;top:0;bottom:40px;right:-1px;cursor:pointer;',
2233                    prev:       abs+'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;text-align:center;color:#000;font:bold 16px/36px arial,sans-serif',
2234                    next:       abs+'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;right:20px;left:auto;display:none;font:bold 16px/36px arial,sans-serif;text-align:center;color:#000',
2235                    title:      'float:left',
2236                    counter:    'float:right;margin-left:8px;'
2237                },
2238                hover = function(elem) {
2239                    return elem.hover(
2240                        function() { $(this).css( 'color', '#bbb' ); },
2241                        function() { $(this).css( 'color', '#444' ); }
2242                    );
2243                },
2244                appends = {};
2245
2246            // fix for navigation hovers transparent background event "feature"
2247            var exs = '';
2248            if ( IE > 7 ) {
2249                exs = IE < 9 ? 'background:#000;filter:alpha(opacity=0);' : 'background:rgba(0,0,0,0);';
2250            } else {
2251                exs = 'z-index:99999';
2252            }
2253
2254            cssMap.nextholder += exs;
2255            cssMap.prevholder += exs;
2256
2257            // create and insert CSS
2258            $.each(cssMap, function( key, value ) {
2259                css += '.galleria-'+prefix+key+'{'+value+'}';
2260            });
2261
2262            css += '.galleria-'+prefix+'box.iframe .galleria-'+prefix+'prevholder,'+
2263                   '.galleria-'+prefix+'box.iframe .galleria-'+prefix+'nextholder{'+
2264                   'width:100px;height:100px;top:50%;margin-top:-70px}';
2265
2266            Utils.insertStyleTag( css, 'galleria-lightbox' );
2267
2268            // create the elements
2269            $.each(elems.split(' '), function( i, elemId ) {
2270                self.addElement( 'lightbox-' + elemId );
2271                el[ elemId ] = lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
2272            });
2273
2274            // initiate the image
2275            lightbox.image = new Galleria.Picture();
2276
2277            // append the elements
2278            $.each({
2279                    box: 'shadow content close prevholder nextholder',
2280                    info: 'title counter',
2281                    content: 'info image',
2282                    prevholder: 'prev',
2283                    nextholder: 'next'
2284                }, function( key, val ) {
2285                    var arr = [];
2286                    $.each( val.split(' '), function( i, prop ) {
2287                        arr.push( prefix + prop );
2288                    });
2289                    appends[ prefix+key ] = arr;
2290            });
2291
2292            self.append( appends );
2293
2294            $( el.image ).append( lightbox.image.container );
2295
2296            $( DOM().body ).append( el.overlay, el.box );
2297
2298            // add the prev/next nav and bind some controls
2299
2300            hover( $( el.close ).on( 'click:fast', lightbox.hide ).html('&#215;') );
2301
2302            $.each( ['Prev','Next'], function(i, dir) {
2303
2304                var $d = $( el[ dir.toLowerCase() ] ).html( /v/.test( dir ) ? '&#8249;&#160;' : '&#160;&#8250;' ),
2305                    $e = $( el[ dir.toLowerCase()+'holder'] );
2306
2307                $e.on( 'click:fast', function() {
2308                    lightbox[ 'show' + dir ]();
2309                });
2310
2311                // IE7 and touch devices will simply show the nav
2312                if ( IE < 8 || Galleria.TOUCH ) {
2313                    $d.show();
2314                    return;
2315                }
2316
2317                $e.hover( function() {
2318                    $d.show();
2319                }, function(e) {
2320                    $d.stop().fadeOut( 200 );
2321                });
2322
2323            });
2324            $( el.overlay ).on( 'click:fast', lightbox.hide );
2325
2326            // the lightbox animation is slow on ipad
2327            if ( Galleria.IPAD ) {
2328                self._options.lightboxTransitionSpeed = 0;
2329            }
2330
2331        },
2332
2333        rescale: function(event) {
2334
2335            // calculate
2336             var width = M.min( $win.width()-40, lightbox.width ),
2337                height = M.min( $win.height()-60, lightbox.height ),
2338                ratio = M.min( width / lightbox.width, height / lightbox.height ),
2339                destWidth = M.round( lightbox.width * ratio ) + 40,
2340                destHeight = M.round( lightbox.height * ratio ) + 60,
2341                to = {
2342                    width: destWidth,
2343                    height: destHeight,
2344                    'margin-top': M.ceil( destHeight / 2 ) *- 1,
2345                    'margin-left': M.ceil( destWidth / 2 ) *- 1
2346                };
2347
2348            // if rescale event, don't animate
2349            if ( event ) {
2350                $( lightbox.elems.box ).css( to );
2351            } else {
2352                $( lightbox.elems.box ).animate( to, {
2353                    duration: self._options.lightboxTransitionSpeed,
2354                    easing: self._options.easing,
2355                    complete: function() {
2356                        var image = lightbox.image,
2357                            speed = self._options.lightboxFadeSpeed;
2358
2359                        self.trigger({
2360                            type: Galleria.LIGHTBOX_IMAGE,
2361                            imageTarget: image.image
2362                        });
2363
2364                        $( image.container ).show();
2365
2366                        $( image.image ).animate({ opacity: 1 }, speed);
2367                        Utils.show( lightbox.elems.info, speed );
2368                    }
2369                });
2370            }
2371        },
2372
2373        hide: function() {
2374
2375            // remove the image
2376            lightbox.image.image = null;
2377
2378            $win.off('resize', lightbox.rescale);
2379
2380            $( lightbox.elems.box ).hide().find( 'iframe' ).remove();
2381
2382            Utils.hide( lightbox.elems.info );
2383
2384            self.detachKeyboard();
2385            self.attachKeyboard( lightbox.keymap );
2386
2387            lightbox.keymap = false;
2388
2389            Utils.hide( lightbox.elems.overlay, 200, function() {
2390                $( this ).hide().css( 'opacity', self._options.overlayOpacity );
2391                self.trigger( Galleria.LIGHTBOX_CLOSE );
2392            });
2393        },
2394
2395        showNext: function() {
2396            lightbox.show( self.getNext( lightbox.active ) );
2397        },
2398
2399        showPrev: function() {
2400            lightbox.show( self.getPrev( lightbox.active ) );
2401        },
2402
2403        show: function(index) {
2404
2405            lightbox.active = index = typeof index === 'number' ? index : self.getIndex() || 0;
2406
2407            if ( !lightbox.initialized ) {
2408                lightbox.init();
2409            }
2410
2411            // trigger the event
2412            self.trigger( Galleria.LIGHTBOX_OPEN );
2413
2414            // temporarily attach some keys
2415            // save the old ones first in a cloned object
2416            if ( !lightbox.keymap ) {
2417
2418                lightbox.keymap = $.extend({}, self._keyboard.map);
2419
2420                self.attachKeyboard({
2421                    escape: lightbox.hide,
2422                    right: lightbox.showNext,
2423                    left: lightbox.showPrev
2424                });
2425            }
2426
2427            $win.off('resize', lightbox.rescale );
2428
2429            var data = self.getData(index),
2430                total = self.getDataLength(),
2431                n = self.getNext( index ),
2432                ndata, p, i;
2433
2434            Utils.hide( lightbox.elems.info );
2435
2436            try {
2437                for ( i = self._options.preload; i > 0; i-- ) {
2438                    p = new Galleria.Picture();
2439                    ndata = self.getData( n );
2440                    p.preload( ndata.big ? ndata.big : ndata.image );
2441                    n = self.getNext( n );
2442                }
2443            } catch(e) {}
2444
2445            lightbox.image.isIframe = ( data.iframe && !data.image );
2446
2447            $( lightbox.elems.box ).toggleClass( 'iframe', lightbox.image.isIframe );
2448
2449            $( lightbox.image.container ).find( '.galleria-videoicon' ).remove();
2450
2451            lightbox.image.load( data.big || data.image || data.iframe, function( image ) {
2452
2453                if ( image.isIframe ) {
2454
2455                    var cw = $(window).width(),
2456                        ch = $(window).height();
2457
2458                    if ( image.video && self._options.maxVideoSize ) {
2459                        var r = M.min( self._options.maxVideoSize/cw, self._options.maxVideoSize/ch );
2460                        if ( r < 1 ) {
2461                            cw *= r;
2462                            ch *= r;
2463                        }
2464                    }
2465                    lightbox.width = cw;
2466                    lightbox.height = ch;
2467
2468                } else {
2469                    lightbox.width = image.original.width;
2470                    lightbox.height = image.original.height;
2471                }
2472
2473                $( image.image ).css({
2474                    width: image.isIframe ? '100%' : '100.1%',
2475                    height: image.isIframe ? '100%' : '100.1%',
2476                    top: 0,
2477                    bottom: 0,
2478                    zIndex: 99998,
2479                    opacity: 0,
2480                    visibility: 'visible'
2481                }).parent().height('100%');
2482
2483                lightbox.elems.title.innerHTML = data.title || '';
2484                lightbox.elems.counter.innerHTML = (index + 1) + ' / ' + total;
2485                $win.resize( lightbox.rescale );
2486                lightbox.rescale();
2487
2488                if( data.image && data.iframe ) {
2489
2490                    $( lightbox.elems.box ).addClass('iframe');
2491
2492                    if ( data.video ) {
2493                        var $icon = _playIcon( image.container ).hide();
2494                        window.setTimeout(function() {
2495                            $icon.fadeIn(200);
2496                        }, 200);
2497                    }
2498
2499                    $( image.image ).css( 'cursor', 'pointer' ).mouseup((function(data, image) {
2500                        return function(e) {
2501                            $( lightbox.image.container ).find( '.galleria-videoicon' ).remove();
2502                            e.preventDefault();
2503                            image.isIframe = true;
2504                            image.load( data.iframe + ( data.video ? '&autoplay=1' : '' ), {
2505                                width: '100%',
2506                                height: IE < 8 ? $( lightbox.image.container ).height() : '100%'
2507                            });
2508                        };
2509                    }(data, image)));
2510                }
2511            });
2512
2513            $( lightbox.elems.overlay ).show().css( 'visibility', 'visible' );
2514            $( lightbox.elems.box ).show();
2515        }
2516    };
2517
2518    // the internal timeouts object
2519    // provides helper methods for controlling timeouts
2520
2521    var _timer = this._timer = {
2522
2523        trunk: {},
2524
2525        add: function( id, fn, delay, loop ) {
2526            id = id || new Date().getTime();
2527            loop = loop || false;
2528            this.clear( id );
2529            if ( loop ) {
2530                var old = fn;
2531                fn = function() {
2532                    old();
2533                    _timer.add( id, fn, delay );
2534                };
2535            }
2536            this.trunk[ id ] = window.setTimeout( fn, delay );
2537        },
2538
2539        clear: function( id ) {
2540
2541            var del = function( i ) {
2542                window.clearTimeout( this.trunk[ i ] );
2543                delete this.trunk[ i ];
2544            }, i;
2545
2546            if ( !!id && id in this.trunk ) {
2547                del.call( this, id );
2548
2549            } else if ( typeof id === 'undefined' ) {
2550                for ( i in this.trunk ) {
2551                    if ( this.trunk.hasOwnProperty( i ) ) {
2552                        del.call( this, i );
2553                    }
2554                }
2555            }
2556        }
2557    };
2558
2559    return this;
2560};
2561
2562// end Galleria constructor
2563
2564Galleria.prototype = {
2565
2566    // bring back the constructor reference
2567
2568    constructor: Galleria,
2569
2570    /**
2571        Use this function to initialize the gallery and start loading.
2572        Should only be called once per instance.
2573
2574        @param {HTMLElement} target The target element
2575        @param {Object} options The gallery options
2576
2577        @returns Instance
2578    */
2579
2580    init: function( target, options ) {
2581
2582        options = _legacyOptions( options );
2583
2584        // save the original ingredients
2585        this._original = {
2586            target: target,
2587            options: options,
2588            data: null
2589        };
2590
2591        // save the target here
2592        this._target = this._dom.target = target.nodeName ? target : $( target ).get(0);
2593
2594        // save the original content for destruction
2595        this._original.html = this._target.innerHTML;
2596
2597        // push the instance
2598        _instances.push( this );
2599
2600        // raise error if no target is detected
2601        if ( !this._target ) {
2602             Galleria.raise('Target not found', true);
2603             return;
2604        }
2605
2606        // apply options
2607        this._options = {
2608            autoplay: false,
2609            carousel: true,
2610            carouselFollow: true, // legacy, deprecate at 1.3
2611            carouselSpeed: 400,
2612            carouselSteps: 'auto',
2613            clicknext: false,
2614            dailymotion: {
2615                foreground: '%23EEEEEE',
2616                highlight: '%235BCEC5',
2617                background: '%23222222',
2618                logo: 0,
2619                hideInfos: 1
2620            },
2621            dataConfig : function( elem ) { return {}; },
2622            dataSelector: 'img',
2623            dataSort: false,
2624            dataSource: this._target,
2625            debug: undef,
2626            dummy: undef, // 1.2.5
2627            easing: 'galleria',
2628            extend: function(options) {},
2629            fullscreenCrop: undef, // 1.2.5
2630            fullscreenDoubleTap: true, // 1.2.4 toggles fullscreen on double-tap for touch devices
2631            fullscreenTransition: undef, // 1.2.6
2632            height: 0,
2633            idleMode: true, // 1.2.4 toggles idleMode
2634            idleTime: 3000,
2635            idleSpeed: 200,
2636            imageCrop: false,
2637            imageMargin: 0,
2638            imagePan: false,
2639            imagePanSmoothness: 12,
2640            imagePosition: '50%',
2641            imageTimeout: undef, // 1.2.5
2642            initialTransition: undef, // 1.2.4, replaces transitionInitial
2643            keepSource: false,
2644            layerFollow: true, // 1.2.5
2645            lightbox: false, // 1.2.3
2646            lightboxFadeSpeed: 200,
2647            lightboxTransitionSpeed: 200,
2648            linkSourceImages: true,
2649            maxScaleRatio: undef,
2650            maxVideoSize: undef, // 1.2.9
2651            minScaleRatio: undef, // deprecated in 1.2.9
2652            overlayOpacity: 0.85,
2653            overlayBackground: '#0b0b0b',
2654            pauseOnInteraction: true,
2655            popupLinks: false,
2656            preload: 2,
2657            queue: true,
2658            responsive: true,
2659            show: 0,
2660            showInfo: true,
2661            showCounter: true,
2662            showImagenav: true,
2663            swipe: 'auto', // 1.2.4 -> revised in 1.3 -> changed type in 1.3.5
2664            theme: null,
2665            thumbCrop: true,
2666            thumbEventType: 'click:fast',
2667            thumbMargin: 0,
2668            thumbQuality: 'auto',
2669            thumbDisplayOrder: true, // 1.2.8
2670            thumbPosition: '50%', // 1.3
2671            thumbnails: true,
2672            touchTransition: undef, // 1.2.6
2673            transition: 'fade',
2674            transitionInitial: undef, // legacy, deprecate in 1.3. Use initialTransition instead.
2675            transitionSpeed: 400,
2676            trueFullscreen: true, // 1.2.7
2677            useCanvas: false, // 1.2.4
2678            variation: '', // 1.3.2
2679            videoPoster: true, // 1.3
2680            vimeo: {
2681                title: 0,
2682                byline: 0,
2683                portrait: 0,
2684                color: 'aaaaaa'
2685            },
2686            wait: 5000, // 1.2.7
2687            width: 'auto',
2688            youtube: {
2689                modestbranding: 1,
2690                autohide: 1,
2691                color: 'white',
2692                hd: 1,
2693                rel: 0,
2694                showinfo: 0
2695            }
2696        };
2697
2698        // legacy support for transitionInitial
2699        this._options.initialTransition = this._options.initialTransition || this._options.transitionInitial;
2700
2701        if ( options ) {
2702
2703            // turn off debug
2704            if ( options.debug === false ) {
2705                DEBUG = false;
2706            }
2707
2708            // set timeout
2709            if ( typeof options.imageTimeout === 'number' ) {
2710                TIMEOUT = options.imageTimeout;
2711            }
2712
2713            // set dummy
2714            if ( typeof options.dummy === 'string' ) {
2715                DUMMY = options.dummy;
2716            }
2717
2718            // set theme
2719            if ( typeof options.theme == 'string' ) {
2720                this._options.theme = options.theme;
2721            }
2722        }
2723
2724        // hide all content
2725        $( this._target ).children().hide();
2726
2727        // Warn for quirks mode
2728        if ( Galleria.QUIRK ) {
2729            Galleria.raise('Your page is in Quirks mode, Galleria may not render correctly. Please validate your HTML and add a correct doctype.');
2730        }
2731
2732        // now we just have to wait for the theme...
2733        // first check if it has already loaded
2734        if ( _loadedThemes.length ) {
2735            if ( this._options.theme ) {
2736                for ( var i=0; i<_loadedThemes.length; i++ ) {
2737                    if( this._options.theme === _loadedThemes[i].name ) {
2738                        this.theme = _loadedThemes[i];
2739                        break;
2740                    }
2741                }
2742            } else {
2743                // if no theme sepcified, apply the first loaded theme
2744                this.theme = _loadedThemes[0];
2745            }
2746        }
2747
2748        if ( typeof this.theme == 'object' ) {
2749            this._init();
2750        } else {
2751            // if no theme is loaded yet, push the instance into a pool and run it when the theme is ready
2752            _pool.push( this );
2753        }
2754
2755        return this;
2756    },
2757
2758    // this method should only be called once per instance
2759    // for manipulation of data, use the .load method
2760
2761    _init: function() {
2762
2763        var self = this,
2764            options = this._options;
2765
2766        if ( this._initialized ) {
2767            Galleria.raise( 'Init failed: Gallery instance already initialized.' );
2768            return this;
2769        }
2770
2771        this._initialized = true;
2772
2773        if ( !this.theme ) {
2774            Galleria.raise( 'Init failed: No theme found.', true );
2775            return this;
2776        }
2777
2778        // merge the theme & caller options
2779        $.extend( true, options, this.theme.defaults, this._original.options, Galleria.configure.options );
2780
2781        // internally we use boolean for swipe
2782        options.swipe = (function(s) {
2783
2784            if ( s == 'enforced' ) { return true; }
2785
2786            // legacy patch
2787            if( s === false || s == 'disabled' ) { return false; }
2788
2789            return !!Galleria.TOUCH;
2790
2791        }( options.swipe ));
2792
2793        // disable options that arent compatible with swipe
2794        if ( options.swipe ) {
2795            options.clicknext = false;
2796            options.imagePan = false;
2797        }
2798
2799        // check for canvas support
2800        (function( can ) {
2801            if ( !( 'getContext' in can ) ) {
2802                can = null;
2803                return;
2804            }
2805            _canvas = _canvas || {
2806                elem: can,
2807                context: can.getContext( '2d' ),
2808                cache: {},
2809                length: 0
2810            };
2811        }( doc.createElement( 'canvas' ) ) );
2812
2813        // bind the gallery to run when data is ready
2814        this.bind( Galleria.DATA, function() {
2815
2816            // remove big if total pixels are less than 1024 (most phones)
2817            if ( window.screen && window.screen.width && Array.prototype.forEach ) {
2818
2819                this._data.forEach(function(data) {
2820
2821                    var density = 'devicePixelRatio' in window ? window.devicePixelRatio : 1,
2822                        m = M.max( window.screen.width, window.screen.height );
2823
2824                    if ( m*density < 1024 ) {
2825                        data.big = data.image;
2826                    }
2827                });
2828            }
2829
2830            // save the new data
2831            this._original.data = this._data;
2832
2833            // lets show the counter here
2834            this.get('total').innerHTML = this.getDataLength();
2835
2836            // cache the container
2837            var $container = this.$( 'container' );
2838
2839            // set ratio if height is < 2
2840            if ( self._options.height < 2 ) {
2841                self._userRatio = self._ratio = self._options.height;
2842            }
2843
2844            // the gallery is ready, let's just wait for the css
2845            var num = { width: 0, height: 0 };
2846            var testHeight = function() {
2847                return self.$( 'stage' ).height();
2848            };
2849
2850            // check container and thumbnail height
2851            Utils.wait({
2852                until: function() {
2853
2854                    // keep trying to get the value
2855                    num = self._getWH();
2856                    $container.width( num.width ).height( num.height );
2857                    return testHeight() && num.width && num.height > 50;
2858
2859                },
2860                success: function() {
2861
2862                    self._width = num.width;
2863                    self._height = num.height;
2864                    self._ratio = self._ratio || num.height/num.width;
2865
2866                    // for some strange reason, webkit needs a single setTimeout to play ball
2867                    if ( Galleria.WEBKIT ) {
2868                        window.setTimeout( function() {
2869                            self._run();
2870                        }, 1);
2871                    } else {
2872                        self._run();
2873                    }
2874                },
2875                error: function() {
2876
2877                    // Height was probably not set, raise hard errors
2878
2879                    if ( testHeight() ) {
2880                        Galleria.raise('Could not extract sufficient width/height of the gallery container. Traced measures: width:' + num.width + 'px, height: ' + num.height + 'px.', true);
2881                    } else {
2882                        Galleria.raise('Could not extract a stage height from the CSS. Traced height: ' + testHeight() + 'px.', true);
2883                    }
2884                },
2885                timeout: typeof this._options.wait == 'number' ? this._options.wait : false
2886            });
2887        });
2888
2889        // build the gallery frame
2890        this.append({
2891            'info-text' :
2892                ['info-title', 'info-description'],
2893            'info' :
2894                ['info-text'],
2895            'image-nav' :
2896                ['image-nav-right', 'image-nav-left'],
2897            'stage' :
2898                ['images', 'loader', 'counter', 'image-nav'],
2899            'thumbnails-list' :
2900                ['thumbnails'],
2901            'thumbnails-container' :
2902                ['thumb-nav-left', 'thumbnails-list', 'thumb-nav-right'],
2903            'container' :
2904                ['stage', 'thumbnails-container', 'info', 'tooltip']
2905        });
2906
2907        Utils.hide( this.$( 'counter' ).append(
2908            this.get( 'current' ),
2909            doc.createTextNode(' / '),
2910            this.get( 'total' )
2911        ) );
2912
2913        this.setCounter('&#8211;');
2914
2915        Utils.hide( self.get('tooltip') );
2916
2917        // add a notouch class on the container to prevent unwanted :hovers on touch devices
2918        this.$( 'container' ).addClass([
2919            ( Galleria.TOUCH ? 'touch' : 'notouch' ),
2920            this._options.variation,
2921            'galleria-theme-'+this.theme.name
2922        ].join(' '));
2923
2924        // add images to the controls
2925        if ( !this._options.swipe ) {
2926            $.each( new Array(2), function( i ) {
2927
2928                // create a new Picture instance
2929                var image = new Galleria.Picture();
2930
2931                // apply some styles, create & prepend overlay
2932                $( image.container ).css({
2933                    position: 'absolute',
2934                    top: 0,
2935                    left: 0
2936                }).prepend( self._layers[i] = $( Utils.create('galleria-layer') ).css({
2937                    position: 'absolute',
2938                    top:0, left:0, right:0, bottom:0,
2939                    zIndex:2
2940                })[0] );
2941
2942                // append the image
2943                self.$( 'images' ).append( image.container );
2944
2945                // reload the controls
2946                self._controls[i] = image;
2947
2948                // build a frame
2949                var frame = new Galleria.Picture();
2950                frame.isIframe = true;
2951
2952                $( frame.container ).attr('class', 'galleria-frame').css({
2953                    position: 'absolute',
2954                    top: 0,
2955                    left: 0,
2956                    zIndex: 4,
2957                    background: '#000',
2958                    display: 'none'
2959                }).appendTo( image.container );
2960
2961                self._controls.frames[i] = frame;
2962
2963            });
2964        }
2965
2966        // some forced generic styling
2967        this.$( 'images' ).css({
2968            position: 'relative',
2969            top: 0,
2970            left: 0,
2971            width: '100%',
2972            height: '100%'
2973        });
2974
2975        if ( options.swipe ) {
2976            this.$( 'images' ).css({
2977                position: 'absolute',
2978                top: 0,
2979                left: 0,
2980                width: 0,
2981                height: '100%'
2982            });
2983            this.finger = new Galleria.Finger(this.get('stage'), {
2984                onchange: function(page) {
2985                    self.pause().show(page);
2986                },
2987                oncomplete: function(page) {
2988
2989                    var index = M.max( 0, M.min( parseInt( page, 10 ), self.getDataLength() - 1 ) ),
2990                        data = self.getData(index);
2991
2992                    $( self._thumbnails[ index ].container )
2993                        .addClass( 'active' )
2994                        .siblings( '.active' )
2995                        .removeClass( 'active' );
2996
2997                    if ( !data ) {
2998                       return;
2999                    }
3000
3001                    // remove video iframes
3002                    self.$( 'images' ).find( '.galleria-frame' ).css('opacity', 0).hide().find( 'iframe' ).remove();
3003
3004                    if ( self._options.carousel && self._options.carouselFollow ) {
3005                        self._carousel.follow( index );
3006                    }
3007                }
3008            });
3009            this.bind( Galleria.RESCALE, function() {
3010                this.finger.setup();
3011            });
3012            this.$('stage').on('click', function(e) {
3013                var data = self.getData();
3014                if ( !data ) {
3015                    return;
3016                }
3017                if ( data.iframe ) {
3018
3019                    if ( self.isPlaying() ) {
3020                        self.pause();
3021                    }
3022                    var frame = self._controls.frames[ self._active ],
3023                        w = self._stageWidth,
3024                        h = self._stageHeight;
3025
3026                    if ( $( frame.container ).find( 'iframe' ).length ) {
3027                        return;
3028                    }
3029
3030                    $( frame.container ).css({
3031                        width: w,
3032                        height: h,
3033                        opacity: 0
3034                    }).show().animate({
3035                        opacity: 1
3036                    }, 200);
3037
3038                    window.setTimeout(function() {
3039                        frame.load( data.iframe + ( data.video ? '&autoplay=1' : '' ), {
3040                            width: w,
3041                            height: h
3042                        }, function( frame ) {
3043                            self.$( 'container' ).addClass( 'videoplay' );
3044                            frame.scale({
3045                                width: self._stageWidth,
3046                                height: self._stageHeight,
3047                                iframelimit: data.video ? self._options.maxVideoSize : undef
3048                            });
3049                        });
3050                    }, 100);
3051
3052                    return;
3053                }
3054
3055                if ( data.link ) {
3056                    if ( self._options.popupLinks ) {
3057                        var win = window.open( data.link, '_blank' );
3058                    } else {
3059                        window.location.href = data.link;
3060                    }
3061                    return;
3062                }
3063            });
3064            this.bind( Galleria.IMAGE, function(e) {
3065
3066                self.setCounter( e.index );
3067                self.setInfo( e.index );
3068
3069                var next = this.getNext(),
3070                    prev = this.getPrev();
3071
3072                var preloads = [prev,next];
3073                preloads.push(this.getNext(next), this.getPrev(prev), self._controls.slides.length-1);
3074
3075                var filtered = [];
3076
3077                $.each(preloads, function(i, val) {
3078                    if ( $.inArray(val, filtered) == -1 ) {
3079                        filtered.push(val);
3080                    }
3081                });
3082
3083                $.each(filtered, function(i, loadme) {
3084                    var d = self.getData(loadme),
3085                        img = self._controls.slides[loadme],
3086                        src = self.isFullscreen() && d.big ? d.big : ( d.image || d.iframe );
3087
3088                    if ( d.iframe && !d.image ) {
3089                        img.isIframe = true;
3090                    }
3091
3092                    if ( !img.ready ) {
3093                        self._controls.slides[loadme].load(src, function(img) {
3094                            if ( !img.isIframe ) {
3095                                $(img.image).css('visibility', 'hidden');
3096                            }
3097                            self._scaleImage(img, {
3098                                complete: function(img) {
3099                                    if ( !img.isIframe ) {
3100                                        $(img.image).css({
3101                                            opacity: 0,
3102                                            visibility: 'visible'
3103                                        }).animate({
3104                                            opacity: 1
3105                                        }, 200);
3106                                    }
3107                                }
3108                            });
3109                        });
3110                    }
3111                });
3112            });
3113        }
3114
3115        this.$( 'thumbnails, thumbnails-list' ).css({
3116            overflow: 'hidden',
3117            position: 'relative'
3118        });
3119
3120        // bind image navigation arrows
3121        this.$( 'image-nav-right, image-nav-left' ).on( 'click:fast', function(e) {
3122
3123            // pause if options is set
3124            if ( options.pauseOnInteraction ) {
3125                self.pause();
3126            }
3127
3128            // navigate
3129            var fn = /right/.test( this.className ) ? 'next' : 'prev';
3130            self[ fn ]();
3131
3132        }).on('click', function(e) {
3133
3134            e.preventDefault();
3135
3136            // tune the clicknext option
3137            if ( options.clicknext || options.swipe ) {
3138                e.stopPropagation();
3139            }
3140        });
3141
3142        // hide controls if chosen to
3143        $.each( ['info','counter','image-nav'], function( i, el ) {
3144            if ( options[ 'show' + el.substr(0,1).toUpperCase() + el.substr(1).replace(/-/,'') ] === false ) {
3145                Utils.moveOut( self.get( el.toLowerCase() ) );
3146            }
3147        });
3148
3149        // load up target content
3150        this.load();
3151
3152        // now it's usually safe to remove the content
3153        // IE will never stop loading if we remove it, so let's keep it hidden for IE (it's usually fast enough anyway)
3154        if ( !options.keepSource && !IE ) {
3155            this._target.innerHTML = '';
3156        }
3157
3158        // re-append the errors, if they happened before clearing
3159        if ( this.get( 'errors' ) ) {
3160            this.appendChild( 'target', 'errors' );
3161        }
3162
3163        // append the gallery frame
3164        this.appendChild( 'target', 'container' );
3165
3166        // parse the carousel on each thumb load
3167        if ( options.carousel ) {
3168            var count = 0,
3169                show = options.show;
3170            this.bind( Galleria.THUMBNAIL, function() {
3171                this.updateCarousel();
3172                if ( ++count == this.getDataLength() && typeof show == 'number' && show > 0 ) {
3173                    this._carousel.follow( show );
3174                }
3175            });
3176        }
3177
3178        // bind window resize for responsiveness
3179        if ( options.responsive ) {
3180            $win.on( 'resize', function() {
3181                if ( !self.isFullscreen() ) {
3182                    self.resize();
3183                }
3184            });
3185        }
3186
3187        // double-tap/click fullscreen toggle
3188
3189        if ( options.fullscreenDoubleTap ) {
3190
3191            this.$( 'stage' ).on( 'touchstart', (function() {
3192                var last, cx, cy, lx, ly, now,
3193                    getData = function(e) {
3194                        return e.originalEvent.touches ? e.originalEvent.touches[0] : e;
3195                    };
3196                self.$( 'stage' ).on('touchmove', function() {
3197                    last = 0;
3198                });
3199                return function(e) {
3200                    if( /(-left|-right)/.test(e.target.className) ) {
3201                        return;
3202                    }
3203                    now = Utils.timestamp();
3204                    cx = getData(e).pageX;
3205                    cy = getData(e).pageY;
3206                    if ( e.originalEvent.touches.length < 2 && ( now - last < 300 ) && ( cx - lx < 20) && ( cy - ly < 20) ) {
3207                        self.toggleFullscreen();
3208                        e.preventDefault();
3209                        return;
3210                    }
3211                    last = now;
3212                    lx = cx;
3213                    ly = cy;
3214                };
3215            }()));
3216        }
3217
3218        // bind the ons
3219        $.each( Galleria.on.binds, function(i, bind) {
3220            // check if already bound
3221            if ( $.inArray( bind.hash, self._binds ) == -1 ) {
3222                self.bind( bind.type, bind.callback );
3223            }
3224        });
3225
3226        return this;
3227    },
3228
3229    addTimer : function() {
3230        this._timer.add.apply( this._timer, Utils.array( arguments ) );
3231        return this;
3232    },
3233
3234    clearTimer : function() {
3235        this._timer.clear.apply( this._timer, Utils.array( arguments ) );
3236        return this;
3237    },
3238
3239    // parse width & height from CSS or options
3240
3241    _getWH : function() {
3242
3243        var $container = this.$( 'container' ),
3244            $target = this.$( 'target' ),
3245            self = this,
3246            num = {},
3247            arr;
3248
3249        $.each(['width', 'height'], function( i, m ) {
3250
3251            // first check if options is set
3252            if ( self._options[ m ] && typeof self._options[ m ] === 'number') {
3253                num[ m ] = self._options[ m ];
3254            } else {
3255
3256                arr = [
3257                    Utils.parseValue( $container.css( m ) ),         // the container css height
3258                    Utils.parseValue( $target.css( m ) ),            // the target css height
3259                    $container[ m ](),                               // the container jQuery method
3260                    $target[ m ]()                                   // the target jQuery method
3261                ];
3262
3263                // if first time, include the min-width & min-height
3264                if ( !self[ '_'+m ] ) {
3265                    arr.splice(arr.length,
3266                        Utils.parseValue( $container.css( 'min-'+m ) ),
3267                        Utils.parseValue( $target.css( 'min-'+m ) )
3268                    );
3269                }
3270
3271                // else extract the measures from different sources and grab the highest value
3272                num[ m ] = M.max.apply( M, arr );
3273            }
3274        });
3275
3276        // allow setting a height ratio instead of exact value
3277        // useful when doing responsive galleries
3278
3279        if ( self._userRatio ) {
3280            num.height = num.width * self._userRatio;
3281        }
3282
3283        return num;
3284    },
3285
3286    // Creates the thumbnails and carousel
3287    // can be used at any time, f.ex when the data object is manipulated
3288    // push is an optional argument with pushed images
3289
3290    _createThumbnails : function( push ) {
3291
3292        this.get( 'total' ).innerHTML = this.getDataLength();
3293
3294        var src,
3295            thumb,
3296            data,
3297
3298            $container,
3299
3300            self = this,
3301            o = this._options,
3302
3303            i = push ? this._data.length - push.length : 0,
3304            chunk = i,
3305
3306            thumbchunk = [],
3307            loadindex = 0,
3308
3309            gif = IE < 8 ? 'http://upload.wikimedia.org/wikipedia/commons/c/c0/Blank.gif' :
3310                           'data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D',
3311
3312            // get previously active thumbnail, if exists
3313            active = (function() {
3314                var a = self.$('thumbnails').find('.active');
3315                if ( !a.length ) {
3316                    return false;
3317                }
3318                return a.find('img').attr('src');
3319            }()),
3320
3321            // cache the thumbnail option
3322            optval = typeof o.thumbnails === 'string' ? o.thumbnails.toLowerCase() : null,
3323
3324            // move some data into the instance
3325            // for some reason, jQuery cant handle css(property) when zooming in FF, breaking the gallery
3326            // so we resort to getComputedStyle for browsers who support it
3327            getStyle = function( prop ) {
3328                return doc.defaultView && doc.defaultView.getComputedStyle ?
3329                    doc.defaultView.getComputedStyle( thumb.container, null )[ prop ] :
3330                    $container.css( prop );
3331            },
3332
3333            fake = function(image, index, container) {
3334                return function() {
3335                    $( container ).append( image );
3336                    self.trigger({
3337                        type: Galleria.THUMBNAIL,
3338                        thumbTarget: image,
3339                        index: index,
3340                        galleriaData: self.getData( index )
3341                    });
3342                };
3343            },
3344
3345            onThumbEvent = function( e ) {
3346
3347                // pause if option is set
3348                if ( o.pauseOnInteraction ) {
3349                    self.pause();
3350                }
3351
3352                // extract the index from the data
3353                var index = $( e.currentTarget ).data( 'index' );
3354                if ( self.getIndex() !== index ) {
3355                    self.show( index );
3356                }
3357
3358                e.preventDefault();
3359            },
3360
3361            thumbComplete = function( thumb, callback ) {
3362
3363                $( thumb.container ).css( 'visibility', 'visible' );
3364                self.trigger({
3365                    type: Galleria.THUMBNAIL,
3366                    thumbTarget: thumb.image,
3367                    index: thumb.data.order,
3368                    galleriaData: self.getData( thumb.data.order )
3369                });
3370
3371                if ( typeof callback == 'function' ) {
3372                    callback.call( self, thumb );
3373                }
3374            },
3375
3376            onThumbLoad = function( thumb, callback ) {
3377
3378                // scale when ready
3379                thumb.scale({
3380                    width:    thumb.data.width,
3381                    height:   thumb.data.height,
3382                    crop:     o.thumbCrop,
3383                    margin:   o.thumbMargin,
3384                    canvas:   o.useCanvas,
3385                    position: o.thumbPosition,
3386                    complete: function( thumb ) {
3387
3388                        // shrink thumbnails to fit
3389                        var top = ['left', 'top'],
3390                            arr = ['Width', 'Height'],
3391                            m,
3392                            css,
3393                            data = self.getData( thumb.index );
3394
3395                        // calculate shrinked positions
3396                        $.each(arr, function( i, measure ) {
3397                            m = measure.toLowerCase();
3398                            if ( (o.thumbCrop !== true || o.thumbCrop === m ) ) {
3399                                css = {};
3400                                css[ m ] = thumb[ m ];
3401                                $( thumb.container ).css( css );
3402                                css = {};
3403                                css[ top[ i ] ] = 0;
3404                                $( thumb.image ).css( css );
3405                            }
3406
3407                            // cache outer measures
3408                            thumb[ 'outer' + measure ] = $( thumb.container )[ 'outer' + measure ]( true );
3409                        });
3410
3411                        // set high quality if downscale is moderate
3412                        Utils.toggleQuality( thumb.image,
3413                            o.thumbQuality === true ||
3414                            ( o.thumbQuality === 'auto' && thumb.original.width < thumb.width * 3 )
3415                        );
3416
3417                        if ( o.thumbDisplayOrder && !thumb.lazy ) {
3418
3419                            $.each( thumbchunk, function( i, th ) {
3420                                if ( i === loadindex && th.ready && !th.displayed ) {
3421
3422                                    loadindex++;
3423                                    th.displayed = true;
3424
3425                                    thumbComplete( th, callback );
3426
3427                                    return;
3428                                }
3429                            });
3430                        } else {
3431                            thumbComplete( thumb, callback );
3432                        }
3433                    }
3434                });
3435            };
3436
3437        if ( !push ) {
3438            this._thumbnails = [];
3439            this.$( 'thumbnails' ).empty();
3440        }
3441
3442        // loop through data and create thumbnails
3443        for( ; this._data[ i ]; i++ ) {
3444
3445            data = this._data[ i ];
3446
3447            // get source from thumb or image
3448            src = data.thumb || data.image;
3449
3450            if ( ( o.thumbnails === true || optval == 'lazy' ) && ( data.thumb || data.image ) ) {
3451
3452                // add a new Picture instance
3453                thumb = new Galleria.Picture(i);
3454
3455                // save the index
3456                thumb.index = i;
3457
3458                // flag displayed
3459                thumb.displayed = false;
3460
3461                // flag lazy
3462                thumb.lazy = false;
3463
3464                // flag video
3465                thumb.video = false;
3466
3467                // append the thumbnail
3468                this.$( 'thumbnails' ).append( thumb.container );
3469
3470                // cache the container
3471                $container = $( thumb.container );
3472
3473                // hide it
3474                $container.css( 'visibility', 'hidden' );
3475
3476                thumb.data = {
3477                    width  : Utils.parseValue( getStyle( 'width' ) ),
3478                    height : Utils.parseValue( getStyle( 'height' ) ),
3479                    order  : i,
3480                    src    : src
3481                };
3482
3483                // grab & reset size for smoother thumbnail loads
3484                if ( o.thumbCrop !== true ) {
3485                    $container.css( { width: 'auto', height: 'auto' } );
3486                } else {
3487                    $container.css( { width: thumb.data.width, height: thumb.data.height } );
3488                }
3489
3490                // load the thumbnail
3491                if ( optval == 'lazy' ) {
3492
3493                    $container.addClass( 'lazy' );
3494
3495                    thumb.lazy = true;
3496
3497                    thumb.load( gif, {
3498                        height: thumb.data.height,
3499                        width: thumb.data.width
3500                    });
3501
3502                } else {
3503                    thumb.load( src, onThumbLoad );
3504                }
3505
3506                // preload all images here
3507                if ( o.preload === 'all' ) {
3508                    thumb.preload( data.image );
3509                }
3510
3511            // create empty spans if thumbnails is set to 'empty'
3512            } else if ( ( data.iframe && optval !== null ) || optval === 'empty' || optval === 'numbers' ) {
3513                thumb = {
3514                    container: Utils.create( 'galleria-image' ),
3515                    image: Utils.create( 'img', 'span' ),
3516                    ready: true,
3517                    data: {
3518                        order: i
3519                    }
3520                };
3521
3522                // create numbered thumbnails
3523                if ( optval === 'numbers' ) {
3524                    $( thumb.image ).text( i + 1 );
3525                }
3526
3527                if ( data.iframe ) {
3528                    $( thumb.image ).addClass( 'iframe' );
3529                }
3530
3531                this.$( 'thumbnails' ).append( thumb.container );
3532
3533                // we need to "fake" a loading delay before we append and trigger
3534                // 50+ should be enough
3535
3536                window.setTimeout( ( fake )( thumb.image, i, thumb.container ), 50 + ( i*20 ) );
3537
3538            // create null object to silent errors
3539            } else {
3540                thumb = {
3541                    container: null,
3542                    image: null
3543                };
3544            }
3545
3546            // add events for thumbnails
3547            // you can control the event type using thumb_event_type
3548            // we'll add the same event to the source if it's kept
3549
3550            $( thumb.container ).add( o.keepSource && o.linkSourceImages ? data.original : null )
3551                .data('index', i).on( o.thumbEventType, onThumbEvent )
3552                .data('thumbload', onThumbLoad);
3553
3554            if (active === src) {
3555                $( thumb.container ).addClass( 'active' );
3556            }
3557
3558            this._thumbnails.push( thumb );
3559        }
3560
3561        thumbchunk = this._thumbnails.slice( chunk );
3562
3563        return this;
3564    },
3565
3566    /**
3567        Lazy-loads thumbnails.
3568        You can call this method to load lazy thumbnails at run time
3569
3570        @param {Array|Number} index Index or array of indexes of thumbnails to be loaded
3571        @param {Function} complete Callback that is called when all lazy thumbnails have been loaded
3572
3573        @returns Instance
3574    */
3575
3576    lazyLoad: function( index, complete ) {
3577
3578        var arr = index.constructor == Array ? index : [ index ],
3579            self = this,
3580            loaded = 0;
3581
3582        $.each( arr, function(i, ind) {
3583
3584            if ( ind > self._thumbnails.length - 1 ) {
3585                return;
3586            }
3587
3588            var thumb = self._thumbnails[ ind ],
3589                data = thumb.data,
3590                callback = function() {
3591                    if ( ++loaded == arr.length && typeof complete == 'function' ) {
3592                        complete.call( self );
3593                    }
3594                },
3595                thumbload = $( thumb.container ).data( 'thumbload' );
3596            if (thumbload) {
3597              if ( thumb.video ) {
3598                  thumbload.call( self, thumb, callback );
3599              } else {
3600                  thumb.load( data.src , function( thumb ) {
3601                      thumbload.call( self, thumb, callback );
3602                  });
3603              }
3604            }
3605        });
3606
3607        return this;
3608
3609    },
3610
3611    /**
3612        Lazy-loads thumbnails in chunks.
3613        This method automatcally chops up the loading process of many thumbnails into chunks
3614
3615        @param {Number} size Size of each chunk to be loaded
3616        @param {Number} [delay] Delay between each loads
3617
3618        @returns Instance
3619    */
3620
3621    lazyLoadChunks: function( size, delay ) {
3622
3623        var len = this.getDataLength(),
3624            i = 0,
3625            n = 0,
3626            arr = [],
3627            temp = [],
3628            self = this;
3629
3630        delay = delay || 0;
3631
3632        for( ; i<len; i++ ) {
3633            temp.push(i);
3634            if ( ++n == size || i == len-1 ) {
3635                arr.push( temp );
3636                n = 0;
3637                temp = [];
3638            }
3639        }
3640
3641        var init = function( wait ) {
3642            var a = arr.shift();
3643            if ( a ) {
3644                window.setTimeout(function() {
3645                    self.lazyLoad(a, function() {
3646                        init( true );
3647                    });
3648                }, ( delay && wait ) ? delay : 0 );
3649            }
3650        };
3651
3652        init( false );
3653
3654        return this;
3655
3656    },
3657
3658    // the internal _run method should be called after loading data into galleria
3659    // makes sure the gallery has proper measurements before postrun & ready
3660    _run : function() {
3661
3662        var self = this;
3663
3664        self._createThumbnails();
3665
3666        // make sure we have a stageHeight && stageWidth
3667
3668        Utils.wait({
3669
3670            timeout: 10000,
3671
3672            until: function() {
3673
3674                // Opera crap
3675                if ( Galleria.OPERA ) {
3676                    self.$( 'stage' ).css( 'display', 'inline-block' );
3677                }
3678
3679                self._stageWidth  = self.$( 'stage' ).width();
3680                self._stageHeight = self.$( 'stage' ).height();
3681
3682                return( self._stageWidth &&
3683                        self._stageHeight > 50 ); // what is an acceptable height?
3684            },
3685
3686            success: function() {
3687
3688                // save the instance
3689                _galleries.push( self );
3690
3691                // postrun some stuff after the gallery is ready
3692
3693                // create the touch slider
3694                if ( self._options.swipe ) {
3695
3696                    var $images = self.$( 'images' ).width( self.getDataLength() * self._stageWidth );
3697                    $.each( new Array( self.getDataLength() ), function(i) {
3698
3699                        var image = new Galleria.Picture(),
3700                            data = self.getData(i);
3701
3702                        $( image.container ).css({
3703                            position: 'absolute',
3704                            top: 0,
3705                            left: self._stageWidth*i
3706                        }).prepend( self._layers[i] = $( Utils.create('galleria-layer') ).css({
3707                            position: 'absolute',
3708                            top:0, left:0, right:0, bottom:0,
3709                            zIndex:2
3710                        })[0] ).appendTo( $images );
3711
3712                        if( data.video ) {
3713                            _playIcon( image.container );
3714                        }
3715
3716                        self._controls.slides.push(image);
3717
3718                        var frame = new Galleria.Picture();
3719                        frame.isIframe = true;
3720
3721                        $( frame.container ).attr('class', 'galleria-frame').css({
3722                            position: 'absolute',
3723                            top: 0,
3724                            left: 0,
3725                            zIndex: 4,
3726                            background: '#000',
3727                            display: 'none'
3728                        }).appendTo( image.container );
3729
3730                        self._controls.frames.push(frame);
3731                    });
3732
3733                    self.finger.setup();
3734                }
3735
3736                // show counter
3737                Utils.show( self.get('counter') );
3738
3739                // bind carousel nav
3740                if ( self._options.carousel ) {
3741                    self._carousel.bindControls();
3742                }
3743
3744                // start autoplay
3745                if ( self._options.autoplay ) {
3746
3747                    self.pause();
3748
3749                    if ( typeof self._options.autoplay === 'number' ) {
3750                        self._playtime = self._options.autoplay;
3751                    }
3752
3753                    self._playing = true;
3754                }
3755                // if second load, just do the show and return
3756                if ( self._firstrun ) {
3757
3758                    if ( self._options.autoplay ) {
3759                        self.trigger( Galleria.PLAY );
3760                    }
3761
3762                    if ( typeof self._options.show === 'number' ) {
3763                        self.show( self._options.show );
3764                    }
3765                    return;
3766                }
3767
3768                self._firstrun = true;
3769
3770                // initialize the History plugin
3771                if ( Galleria.History ) {
3772
3773                    // bind the show method
3774                    Galleria.History.change(function( value ) {
3775
3776                        // if ID is NaN, the user pressed back from the first image
3777                        // return to previous address
3778                        if ( isNaN( value ) ) {
3779                            window.history.go(-1);
3780
3781                        // else show the image
3782                        } else {
3783                            self.show( value, undef, true );
3784                        }
3785                    });
3786                }
3787
3788                self.trigger( Galleria.READY );
3789
3790                // call the theme init method
3791                self.theme.init.call( self, self._options );
3792
3793                // Trigger Galleria.ready
3794                $.each( Galleria.ready.callbacks, function(i ,fn) {
3795                    if ( typeof fn == 'function' ) {
3796                        fn.call( self, self._options );
3797                    }
3798                });
3799
3800                // call the extend option
3801                self._options.extend.call( self, self._options );
3802
3803                // show the initial image
3804                // first test for permalinks in history
3805                if ( /^[0-9]{1,4}$/.test( HASH ) && Galleria.History ) {
3806                    self.show( HASH, undef, true );
3807
3808                } else if( self._data[ self._options.show ] ) {
3809                    self.show( self._options.show );
3810                }
3811
3812                // play trigger
3813                if ( self._options.autoplay ) {
3814                    self.trigger( Galleria.PLAY );
3815                }
3816            },
3817
3818            error: function() {
3819                Galleria.raise('Stage width or height is too small to show the gallery. Traced measures: width:' + self._stageWidth + 'px, height: ' + self._stageHeight + 'px.', true);
3820            }
3821
3822        });
3823    },
3824
3825    /**
3826        Loads data into the gallery.
3827        You can call this method on an existing gallery to reload the gallery with new data.
3828
3829        @param {Array|string} [source] Optional JSON array of data or selector of where to find data in the document.
3830        Defaults to the Galleria target or dataSource option.
3831
3832        @param {string} [selector] Optional element selector of what elements to parse.
3833        Defaults to 'img'.
3834
3835        @param {Function} [config] Optional function to modify the data extraction proceedure from the selector.
3836        See the dataConfig option for more information.
3837
3838        @returns Instance
3839    */
3840
3841    load : function( source, selector, config ) {
3842
3843        var self = this,
3844            o = this._options;
3845
3846        // empty the data array
3847        this._data = [];
3848
3849        // empty the thumbnails
3850        this._thumbnails = [];
3851        this.$('thumbnails').empty();
3852
3853        // shorten the arguments
3854        if ( typeof selector === 'function' ) {
3855            config = selector;
3856            selector = null;
3857        }
3858
3859        // use the source set by target
3860        source = source || o.dataSource;
3861
3862        // use selector set by option
3863        selector = selector || o.dataSelector;
3864
3865        // use the dataConfig set by option
3866        config = config || o.dataConfig;
3867
3868        // if source is a true object, make it into an array
3869        if( $.isPlainObject( source ) ) {
3870            source = [source];
3871        }
3872
3873        // check if the data is an array already
3874        if ( $.isArray( source ) ) {
3875            if ( this.validate( source ) ) {
3876                this._data = source;
3877            } else {
3878                Galleria.raise( 'Load failed: JSON Array not valid.' );
3879            }
3880        } else {
3881
3882            // add .video and .iframe to the selector (1.2.7)
3883            selector += ',.video,.iframe';
3884
3885            // loop through images and set data
3886            $( source ).find( selector ).each( function( i, elem ) {
3887
3888                elem = $( elem );
3889                var data = {},
3890                    parent = elem.parent(),
3891                    href = parent.attr( 'href' ),
3892                    rel  = parent.attr( 'rel' );
3893
3894                if( href && ( elem[0].nodeName == 'IMG' || elem.hasClass('video') ) && _videoTest( href ) ) {
3895                    data.video = href;
3896                } else if( href && elem.hasClass('iframe') ) {
3897                    data.iframe = href;
3898                } else {
3899                    data.image = href;
3900                }
3901
3902                if ( rel ) {
3903                    data.big = rel;
3904                }
3905
3906                data.imagesrcset = parent.data( 'srcset' );
3907                data.imagesizes = parent.data( 'sizes' );
3908                data.thumbsizes = elem.attr( 'sizes' );
3909                data.thumbsrcset = elem.attr( 'srcset' );
3910
3911                // alternative extraction from HTML5 data attribute
3912                $.each( 'big bigsrcset bigsizes title description link layer image imagesrcset imagesizes'.split(' '), function( i, val ) {
3913                    if ( elem.data(val) ) {
3914                        data[ val ] = elem.data(val).toString();
3915                    }
3916                });
3917
3918                if (elem.data('srcset')) {
3919                    data.imagesrcset = elem.data('srcset');
3920                }
3921
3922                if (elem.data('sizes')) {
3923                    data.imagesizes = elem.data('sizes');
3924                }
3925
3926                if ( !data.big ) {
3927                    data.big = data.image;
3928                    data.bigsrcset = data.imagesrcset;
3929                    data.bigsizes = data.imagesizes;
3930                }
3931
3932                // mix default extractions with the hrefs and config
3933                // and push it into the data array
3934                self._data.push( $.extend({
3935
3936                    title:       elem.attr('title') || '',
3937                    thumb:       elem.attr('src'),
3938                    image:       elem.attr('src'),
3939                    big:         elem.attr('src'),
3940                    description: elem.attr('alt') || '',
3941                    link:        elem.attr('longdesc'),
3942                    original:    elem.get(0) // saved as a reference
3943
3944                }, data, config( elem ) ) );
3945
3946            });
3947        }
3948
3949        if ( typeof o.dataSort == 'function' ) {
3950            protoArray.sort.call( this._data, o.dataSort );
3951        } else if ( o.dataSort == 'random' ) {
3952            this._data.sort( function() {
3953                return M.round(M.random())-0.5;
3954            });
3955        }
3956
3957        // trigger the DATA event and return
3958        if ( this.getDataLength() ) {
3959            this._parseData( function() {
3960                this.trigger( Galleria.DATA );
3961            } );
3962        }
3963        return this;
3964
3965    },
3966
3967    // make sure the data works properly
3968    _parseData : function( callback ) {
3969
3970        var self = this,
3971            current,
3972            ready = false,
3973            onload = function() {
3974                var complete = true;
3975                $.each( self._data, function( i, data ) {
3976                    if ( data.loading ) {
3977                        complete = false;
3978                        return false;
3979                    }
3980                });
3981                if ( complete && !ready ) {
3982                    ready = true;
3983                    callback.call( self );
3984                }
3985            };
3986
3987        $.each( this._data, function( i, data ) {
3988
3989            current = self._data[ i ];
3990
3991            // q&d hack to attach srcset & sizes to src
3992            $.each( 'big image thumb'.split(' '), function( i, val ) {
3993                if ( data[ val] ) {
3994                    data[val] = new String(data[val]);
3995                    data[val].srcset = data [val + 'srcset'];
3996                    data[val].sizes = data [val + 'sizes'];
3997                }
3998            });
3999
4000            // copy image as thumb if no thumb exists
4001            if ( 'thumb' in data === false ) {
4002                current.thumb = data.image;
4003            }
4004            // copy image as big image if no biggie exists
4005            if ( !data.big ) {
4006                current.big = data.image;
4007            }
4008            // parse video
4009            if ( 'video' in data ) {
4010                var result = _videoTest( data.video );
4011
4012                if ( result ) {
4013                    current.iframe = new Video(result.provider, result.id ).embed() + (function() {
4014
4015                        // add options
4016                        if ( typeof self._options[ result.provider ] == 'object' ) {
4017                            var str = '?', arr = [];
4018                            $.each( self._options[ result.provider ], function( key, val ) {
4019                                arr.push( key + '=' + val );
4020                            });
4021
4022                            // small youtube specifics, perhaps move to _video later
4023                            if ( result.provider == 'youtube' ) {
4024                                arr = ['wmode=opaque'].concat(arr);
4025                            }
4026                            return str + arr.join('&');
4027                        }
4028                        return '';
4029                    }());
4030
4031                    // pre-fetch video providers media
4032
4033                    if( !current.thumb || !current.image ) {
4034                        $.each( ['thumb', 'image'], function( i, type ) {
4035                            if ( type == 'image' && !self._options.videoPoster ) {
4036                                current.image = undef;
4037                                return;
4038                            }
4039                            var video = new Video( result.provider, result.id );
4040                            if ( !current[ type ] ) {
4041                                current.loading = true;
4042                                video.getMedia( type, (function(current, type) {
4043                                    return function(src) {
4044                                        current[ type ] = src;
4045                                        if ( type == 'image' && !current.big ) {
4046                                            current.big = current.image;
4047                                        }
4048                                        delete current.loading;
4049                                        onload();
4050                                    };
4051                                }( current, type )));
4052                            }
4053                        });
4054                    }
4055                }
4056            }
4057        });
4058
4059        onload();
4060
4061        return this;
4062    },
4063
4064    /**
4065        Destroy the Galleria instance and recover the original content
4066
4067        @example this.destroy();
4068
4069        @returns Instance
4070    */
4071
4072    destroy : function() {
4073        this.$( 'target' ).data( 'galleria', null );
4074        this.$( 'container' ).off( 'galleria' );
4075        this.get( 'target' ).innerHTML = this._original.html;
4076        this.clearTimer();
4077        Utils.removeFromArray( _instances, this );
4078        Utils.removeFromArray( _galleries, this );
4079        _video._inst = [];
4080        if ( Galleria._waiters !== undefined && Galleria._waiters.length ) {
4081            $.each( Galleria._waiters, function( i, w ) {
4082                if ( w ) window.clearTimeout( w );
4083            });
4084        }
4085        return this;
4086    },
4087
4088    /**
4089        Adds and/or removes images from the gallery
4090        Works just like Array.splice
4091        https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice
4092
4093        @example this.splice( 2, 4 ); // removes 4 images after the second image
4094
4095        @returns Instance
4096    */
4097
4098    splice : function() {
4099        var self = this,
4100            args = Utils.array( arguments );
4101        window.setTimeout(function() {
4102            protoArray.splice.apply( self._data, args );
4103            self._parseData( function() {
4104                self._createThumbnails();
4105            });
4106        },2);
4107        return self;
4108    },
4109
4110    /**
4111        Append images to the gallery
4112        Works just like Array.push
4113        https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push
4114
4115        @example this.push({ image: 'image1.jpg' }); // appends the image to the gallery
4116
4117        @returns Instance
4118    */
4119
4120    push : function() {
4121        var self = this,
4122            args = Utils.array( arguments );
4123
4124        if ( args.length == 1 && args[0].constructor == Array ) {
4125            args = args[0];
4126        }
4127
4128        window.setTimeout(function() {
4129            protoArray.push.apply( self._data, args );
4130            self._parseData( function() {
4131                self._createThumbnails( args );
4132            });
4133        }, 2);
4134        return self;
4135    },
4136
4137    _getActive : function() {
4138        return this._controls.getActive();
4139    },
4140
4141    validate : function( data ) {
4142        // todo: validate a custom data array
4143        return true;
4144    },
4145
4146    /**
4147        Bind any event to Galleria
4148
4149        @param {string} type The Event type to listen for
4150        @param {Function} fn The function to execute when the event is triggered
4151
4152        @example this.bind( 'image', function() { Galleria.log('image shown') });
4153
4154        @returns Instance
4155    */
4156
4157    bind : function(type, fn) {
4158
4159        // allow 'image' instead of Galleria.IMAGE
4160        type = _patchEvent( type );
4161
4162        this.$( 'container' ).on( type, this.proxy(fn) );
4163        return this;
4164    },
4165
4166    /**
4167        Unbind any event to Galleria
4168
4169        @param {string} type The Event type to forget
4170
4171        @returns Instance
4172    */
4173
4174    unbind : function(type) {
4175
4176        type = _patchEvent( type );
4177
4178        this.$( 'container' ).off( type );
4179        return this;
4180    },
4181
4182    /**
4183        Manually trigger a Galleria event
4184
4185        @param {string} type The Event to trigger
4186
4187        @returns Instance
4188    */
4189
4190    trigger : function( type ) {
4191
4192        type = typeof type === 'object' ?
4193            $.extend( type, { scope: this } ) :
4194            { type: _patchEvent( type ), scope: this };
4195
4196        this.$( 'container' ).trigger( type );
4197
4198        return this;
4199    },
4200
4201    /**
4202        Assign an "idle state" to any element.
4203        The idle state will be applied after a certain amount of idle time
4204        Useful to hide f.ex navigation when the gallery is inactive
4205
4206        @param {HTMLElement|string} elem The Dom node or selector to apply the idle state to
4207        @param {Object} styles the CSS styles to apply when in idle mode
4208        @param {Object} [from] the CSS styles to apply when in normal
4209        @param {Boolean} [hide] set to true if you want to hide it first
4210
4211        @example addIdleState( this.get('image-nav'), { opacity: 0 });
4212        @example addIdleState( '.galleria-image-nav', { top: -200 }, true);
4213
4214        @returns Instance
4215    */
4216
4217    addIdleState: function( elem, styles, from, hide ) {
4218        this._idle.add.apply( this._idle, Utils.array( arguments ) );
4219        return this;
4220    },
4221
4222    /**
4223        Removes any idle state previously set using addIdleState()
4224
4225        @param {HTMLElement|string} elem The Dom node or selector to remove the idle state from.
4226
4227        @returns Instance
4228    */
4229
4230    removeIdleState: function( elem ) {
4231        this._idle.remove.apply( this._idle, Utils.array( arguments ) );
4232        return this;
4233    },
4234
4235    /**
4236        Force Galleria to enter idle mode.
4237
4238        @returns Instance
4239    */
4240
4241    enterIdleMode: function() {
4242        this._idle.hide();
4243        return this;
4244    },
4245
4246    /**
4247        Force Galleria to exit idle mode.
4248
4249        @returns Instance
4250    */
4251
4252    exitIdleMode: function() {
4253        this._idle.showAll();
4254        return this;
4255    },
4256
4257    /**
4258        Enter FullScreen mode
4259
4260        @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
4261
4262        @returns Instance
4263    */
4264
4265    enterFullscreen: function( callback ) {
4266        this._fullscreen.enter.apply( this, Utils.array( arguments ) );
4267        return this;
4268    },
4269
4270    /**
4271        Exits FullScreen mode
4272
4273        @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
4274
4275        @returns Instance
4276    */
4277
4278    exitFullscreen: function( callback ) {
4279        this._fullscreen.exit.apply( this, Utils.array( arguments ) );
4280        return this;
4281    },
4282
4283    /**
4284        Toggle FullScreen mode
4285
4286        @param {Function} callback the function to be executed when the fullscreen mode is fully applied or removed.
4287
4288        @returns Instance
4289    */
4290
4291    toggleFullscreen: function( callback ) {
4292        this._fullscreen[ this.isFullscreen() ? 'exit' : 'enter'].apply( this, Utils.array( arguments ) );
4293        return this;
4294    },
4295
4296    /**
4297        Adds a tooltip to any element.
4298        You can also call this method with an object as argument with elemID:value pairs to apply tooltips to (see examples)
4299
4300        @param {HTMLElement} elem The DOM Node to attach the event to
4301        @param {string|Function} value The tooltip message. Can also be a function that returns a string.
4302
4303        @example this.bindTooltip( this.get('thumbnails'), 'My thumbnails');
4304        @example this.bindTooltip( this.get('thumbnails'), function() { return 'My thumbs' });
4305        @example this.bindTooltip( { image_nav: 'Navigation' });
4306
4307        @returns Instance
4308    */
4309
4310    bindTooltip: function( elem, value ) {
4311        this._tooltip.bind.apply( this._tooltip, Utils.array(arguments) );
4312        return this;
4313    },
4314
4315    /**
4316        Note: this method is deprecated. Use refreshTooltip() instead.
4317
4318        Redefine a tooltip.
4319        Use this if you want to re-apply a tooltip value to an already bound tooltip element.
4320
4321        @param {HTMLElement} elem The DOM Node to attach the event to
4322        @param {string|Function} value The tooltip message. Can also be a function that returns a string.
4323
4324        @returns Instance
4325    */
4326
4327    defineTooltip: function( elem, value ) {
4328        this._tooltip.define.apply( this._tooltip, Utils.array(arguments) );
4329        return this;
4330    },
4331
4332    /**
4333        Refresh a tooltip value.
4334        Use this if you want to change the tooltip value at runtime, f.ex if you have a play/pause toggle.
4335
4336        @param {HTMLElement} elem The DOM Node that has a tooltip that should be refreshed
4337
4338        @returns Instance
4339    */
4340
4341    refreshTooltip: function( elem ) {
4342        this._tooltip.show.apply( this._tooltip, Utils.array(arguments) );
4343        return this;
4344    },
4345
4346    /**
4347        Open a pre-designed lightbox with the currently active image.
4348        You can control some visuals using gallery options.
4349
4350        @returns Instance
4351    */
4352
4353    openLightbox: function() {
4354        this._lightbox.show.apply( this._lightbox, Utils.array( arguments ) );
4355        return this;
4356    },
4357
4358    /**
4359        Close the lightbox.
4360
4361        @returns Instance
4362    */
4363
4364    closeLightbox: function() {
4365        this._lightbox.hide.apply( this._lightbox, Utils.array( arguments ) );
4366        return this;
4367    },
4368
4369    /**
4370        Check if a variation exists
4371
4372        @returns {Boolean} If the variation has been applied
4373    */
4374
4375    hasVariation: function( variation ) {
4376        return $.inArray( variation, this._options.variation.split(/\s+/) ) > -1;
4377    },
4378
4379    /**
4380        Get the currently active image element.
4381
4382        @returns {HTMLElement} The image element
4383    */
4384
4385    getActiveImage: function() {
4386        var active = this._getActive();
4387        return active ? active.image : undef;
4388    },
4389
4390    /**
4391        Get the currently active thumbnail element.
4392
4393        @returns {HTMLElement} The thumbnail element
4394    */
4395
4396    getActiveThumb: function() {
4397        return this._thumbnails[ this._active ].image || undef;
4398    },
4399
4400    /**
4401        Get the mouse position relative to the gallery container
4402
4403        @param e The mouse event
4404
4405        @example
4406
4407var gallery = this;
4408$(document).mousemove(function(e) {
4409    console.log( gallery.getMousePosition(e).x );
4410});
4411
4412        @returns {Object} Object with x & y of the relative mouse postion
4413    */
4414
4415    getMousePosition : function(e) {
4416        return {
4417            x: e.pageX - this.$( 'container' ).offset().left,
4418            y: e.pageY - this.$( 'container' ).offset().top
4419        };
4420    },
4421
4422    /**
4423        Adds a panning effect to the image
4424
4425        @param [img] The optional image element. If not specified it takes the currently active image
4426
4427        @returns Instance
4428    */
4429
4430    addPan : function( img ) {
4431
4432        if ( this._options.imageCrop === false ) {
4433            return;
4434        }
4435
4436        img = $( img || this.getActiveImage() );
4437
4438        // define some variables and methods
4439        var self   = this,
4440            x      = img.width() / 2,
4441            y      = img.height() / 2,
4442            destX  = parseInt( img.css( 'left' ), 10 ),
4443            destY  = parseInt( img.css( 'top' ), 10 ),
4444            curX   = destX || 0,
4445            curY   = destY || 0,
4446            distX  = 0,
4447            distY  = 0,
4448            active = false,
4449            ts     = Utils.timestamp(),
4450            cache  = 0,
4451            move   = 0,
4452
4453            // positions the image
4454            position = function( dist, cur, pos ) {
4455                if ( dist > 0 ) {
4456                    move = M.round( M.max( dist * -1, M.min( 0, cur ) ) );
4457                    if ( cache !== move ) {
4458
4459                        cache = move;
4460
4461                        if ( IE === 8 ) { // scroll is faster for IE
4462                            img.parent()[ 'scroll' + pos ]( move * -1 );
4463                        } else {
4464                            var css = {};
4465                            css[ pos.toLowerCase() ] = move;
4466                            img.css(css);
4467                        }
4468                    }
4469                }
4470            },
4471
4472            // calculates mouse position after 50ms
4473            calculate = function(e) {
4474                if (Utils.timestamp() - ts < 50) {
4475                    return;
4476                }
4477                active = true;
4478                x = self.getMousePosition(e).x;
4479                y = self.getMousePosition(e).y;
4480            },
4481
4482            // the main loop to check
4483            loop = function(e) {
4484
4485                if (!active) {
4486                    return;
4487                }
4488
4489                distX = img.width() - self._stageWidth;
4490                distY = img.height() - self._stageHeight;
4491                destX = x / self._stageWidth * distX * -1;
4492                destY = y / self._stageHeight * distY * -1;
4493                curX += ( destX - curX ) / self._options.imagePanSmoothness;
4494                curY += ( destY - curY ) / self._options.imagePanSmoothness;
4495
4496                position( distY, curY, 'Top' );
4497                position( distX, curX, 'Left' );
4498
4499            };
4500
4501        // we need to use scroll in IE8 to speed things up
4502        if ( IE === 8 ) {
4503
4504            img.parent().scrollTop( curY * -1 ).scrollLeft( curX * -1 );
4505            img.css({
4506                top: 0,
4507                left: 0
4508            });
4509
4510        }
4511
4512        // unbind and bind event
4513        this.$( 'stage' ).off( 'mousemove', calculate ).on( 'mousemove', calculate );
4514
4515        // loop the loop
4516        this.addTimer( 'pan' + self._id, loop, 50, true);
4517
4518        return this;
4519    },
4520
4521    /**
4522        Brings the scope into any callback
4523
4524        @param fn The callback to bring the scope into
4525        @param [scope] Optional scope to bring
4526
4527        @example $('#fullscreen').click( this.proxy(function() { this.enterFullscreen(); }) )
4528
4529        @returns {Function} Return the callback with the gallery scope
4530    */
4531
4532    proxy : function( fn, scope ) {
4533        if ( typeof fn !== 'function' ) {
4534            return F;
4535        }
4536        scope = scope || this;
4537        return function() {
4538            return fn.apply( scope, Utils.array( arguments ) );
4539        };
4540    },
4541
4542    /**
4543        Tells you the theme name of the gallery
4544
4545        @returns {String} theme name
4546    */
4547
4548    getThemeName : function() {
4549        return this.theme.name;
4550    },
4551
4552    /**
4553        Removes the panning effect set by addPan()
4554
4555        @returns Instance
4556    */
4557
4558    removePan: function() {
4559
4560        // todo: doublecheck IE8
4561
4562        this.$( 'stage' ).off( 'mousemove' );
4563
4564        this.clearTimer( 'pan' + this._id );
4565
4566        return this;
4567    },
4568
4569    /**
4570        Adds an element to the Galleria DOM array.
4571        When you add an element here, you can access it using element ID in many API calls
4572
4573        @param {string} id The element ID you wish to use. You can add many elements by adding more arguments.
4574
4575        @example addElement('mybutton');
4576        @example addElement('mybutton','mylink');
4577
4578        @returns Instance
4579    */
4580
4581    addElement : function( id ) {
4582
4583        var dom = this._dom;
4584
4585        $.each( Utils.array(arguments), function( i, blueprint ) {
4586           dom[ blueprint ] = Utils.create( 'galleria-' + blueprint );
4587        });
4588
4589        return this;
4590    },
4591
4592    /**
4593        Attach keyboard events to Galleria
4594
4595        @param {Object} map The map object of events.
4596        Possible keys are 'UP', 'DOWN', 'LEFT', 'RIGHT', 'RETURN', 'ESCAPE', 'BACKSPACE', and 'SPACE'.
4597
4598        @example
4599
4600this.attachKeyboard({
4601    right: this.next,
4602    left: this.prev,
4603    up: function() {
4604        console.log( 'up key pressed' )
4605    }
4606});
4607
4608        @returns Instance
4609    */
4610
4611    attachKeyboard : function( map ) {
4612        this._keyboard.attach.apply( this._keyboard, Utils.array( arguments ) );
4613        return this;
4614    },
4615
4616    /**
4617        Detach all keyboard events to Galleria
4618
4619        @returns Instance
4620    */
4621
4622    detachKeyboard : function() {
4623        this._keyboard.detach.apply( this._keyboard, Utils.array( arguments ) );
4624        return this;
4625    },
4626
4627    /**
4628        Fast helper for appending galleria elements that you added using addElement()
4629
4630        @param {string} parentID The parent element ID where the element will be appended
4631        @param {string} childID the element ID that should be appended
4632
4633        @example this.addElement('myElement');
4634        this.appendChild( 'info', 'myElement' );
4635
4636        @returns Instance
4637    */
4638
4639    appendChild : function( parentID, childID ) {
4640        this.$( parentID ).append( this.get( childID ) || childID );
4641        return this;
4642    },
4643
4644    /**
4645        Fast helper for prepending galleria elements that you added using addElement()
4646
4647        @param {string} parentID The parent element ID where the element will be prepended
4648        @param {string} childID the element ID that should be prepended
4649
4650        @example
4651
4652this.addElement('myElement');
4653this.prependChild( 'info', 'myElement' );
4654
4655        @returns Instance
4656    */
4657
4658    prependChild : function( parentID, childID ) {
4659        this.$( parentID ).prepend( this.get( childID ) || childID );
4660        return this;
4661    },
4662
4663    /**
4664        Remove an element by blueprint
4665
4666        @param {string} elemID The element to be removed.
4667        You can remove multiple elements by adding arguments.
4668
4669        @returns Instance
4670    */
4671
4672    remove : function( elemID ) {
4673        this.$( Utils.array( arguments ).join(',') ).remove();
4674        return this;
4675    },
4676
4677    // a fast helper for building dom structures
4678    // leave this out of the API for now
4679
4680    append : function( data ) {
4681        var i, j;
4682        for( i in data ) {
4683            if ( data.hasOwnProperty( i ) ) {
4684                if ( data[i].constructor === Array ) {
4685                    for( j = 0; data[i][j]; j++ ) {
4686                        this.appendChild( i, data[i][j] );
4687                    }
4688                } else {
4689                    this.appendChild( i, data[i] );
4690                }
4691            }
4692        }
4693        return this;
4694    },
4695
4696    // an internal helper for scaling according to options
4697    _scaleImage : function( image, options ) {
4698
4699        image = image || this._controls.getActive();
4700
4701        // janpub (JH) fix:
4702        // image might be unselected yet
4703        // e.g. when external logics rescales the gallery on window resize events
4704        if( !image ) {
4705            return;
4706        }
4707
4708        var complete,
4709
4710            scaleLayer = function( img ) {
4711                $( img.container ).children(':first').css({
4712                    top: M.max(0, Utils.parseValue( img.image.style.top )),
4713                    left: M.max(0, Utils.parseValue( img.image.style.left )),
4714                    width: Utils.parseValue( img.image.width ),
4715                    height: Utils.parseValue( img.image.height )
4716                });
4717            };
4718
4719        options = $.extend({
4720            width:       this._stageWidth,
4721            height:      this._stageHeight,
4722            crop:        this._options.imageCrop,
4723            max:         this._options.maxScaleRatio,
4724            min:         this._options.minScaleRatio,
4725            margin:      this._options.imageMargin,
4726            position:    this._options.imagePosition,
4727            iframelimit: this._options.maxVideoSize
4728        }, options );
4729
4730        if ( this._options.layerFollow && this._options.imageCrop !== true ) {
4731
4732            if ( typeof options.complete == 'function' ) {
4733                complete = options.complete;
4734                options.complete = function() {
4735                    complete.call( image, image );
4736                    scaleLayer( image );
4737                };
4738            } else {
4739                options.complete = scaleLayer;
4740            }
4741
4742        } else {
4743            $( image.container ).children(':first').css({ top: 0, left: 0 });
4744        }
4745
4746        image.scale( options );
4747        return this;
4748    },
4749
4750    /**
4751        Updates the carousel,
4752        useful if you resize the gallery and want to re-check if the carousel nav is needed.
4753
4754        @returns Instance
4755    */
4756
4757    updateCarousel : function() {
4758        this._carousel.update();
4759        return this;
4760    },
4761
4762    /**
4763        Resize the entire gallery container
4764
4765        @param {Object} [measures] Optional object with width/height specified
4766        @param {Function} [complete] The callback to be called when the scaling is complete
4767
4768        @returns Instance
4769    */
4770
4771    resize : function( measures, complete ) {
4772
4773        if ( typeof measures == 'function' ) {
4774            complete = measures;
4775            measures = undef;
4776        }
4777
4778        measures = $.extend( { width:0, height:0 }, measures );
4779
4780        var self = this,
4781            $container = this.$( 'container' );
4782
4783        $.each( measures, function( m, val ) {
4784            if ( !val ) {
4785                $container[ m ]( 'auto' );
4786                measures[ m ] = self._getWH()[ m ];
4787            }
4788        });
4789
4790        $.each( measures, function( m, val ) {
4791            $container[ m ]( val );
4792        });
4793
4794        return this.rescale( complete );
4795
4796    },
4797
4798    /**
4799        Rescales the gallery
4800
4801        @param {number} width The target width
4802        @param {number} height The target height
4803        @param {Function} complete The callback to be called when the scaling is complete
4804
4805        @returns Instance
4806    */
4807
4808    rescale : function( width, height, complete ) {
4809
4810        var self = this;
4811
4812        // allow rescale(fn)
4813        if ( typeof width === 'function' ) {
4814            complete = width;
4815            width = undef;
4816        }
4817
4818        var scale = function() {
4819
4820            // set stagewidth
4821            self._stageWidth = width || self.$( 'stage' ).width();
4822            self._stageHeight = height || self.$( 'stage' ).height();
4823
4824            if ( self._options.swipe ) {
4825                $.each( self._controls.slides, function(i, img) {
4826                    self._scaleImage( img );
4827                    $( img.container ).css('left', self._stageWidth * i);
4828                });
4829                self.$('images').css('width', self._stageWidth * self.getDataLength());
4830            } else {
4831                // scale the active image
4832                self._scaleImage();
4833            }
4834
4835            if ( self._options.carousel ) {
4836                self.updateCarousel();
4837            }
4838
4839            var frame = self._controls.frames[ self._controls.active ];
4840
4841            if (frame) {
4842                self._controls.frames[ self._controls.active ].scale({
4843                    width: self._stageWidth,
4844                    height: self._stageHeight,
4845                    iframelimit: self._options.maxVideoSize
4846                });
4847            }
4848
4849            self.trigger( Galleria.RESCALE );
4850
4851            if ( typeof complete === 'function' ) {
4852                complete.call( self );
4853            }
4854        };
4855
4856        scale.call( self );
4857
4858        return this;
4859    },
4860
4861    /**
4862        Refreshes the gallery.
4863        Useful if you change image options at runtime and want to apply the changes to the active image.
4864
4865        @returns Instance
4866    */
4867
4868    refreshImage : function() {
4869        this._scaleImage();
4870        if ( this._options.imagePan ) {
4871            this.addPan();
4872        }
4873        return this;
4874    },
4875
4876    _preload: function() {
4877        if ( this._options.preload ) {
4878            var p, i,
4879                n = this.getNext(),
4880                ndata;
4881            try {
4882                for ( i = this._options.preload; i > 0; i-- ) {
4883                    p = new Galleria.Picture();
4884                    ndata = this.getData( n );
4885                    p.preload( this.isFullscreen() && ndata.big ? ndata.big : ndata.image );
4886                    n = this.getNext( n );
4887                }
4888            } catch(e) {}
4889        }
4890    },
4891
4892    /**
4893        Shows an image by index
4894
4895        @param {number|boolean} index The index to show
4896        @param {Boolean} rewind A boolean that should be true if you want the transition to go back
4897
4898        @returns Instance
4899    */
4900
4901    show : function( index, rewind, _history ) {
4902
4903        var swipe = this._options.swipe;
4904
4905        // do nothing queue is long || index is false || queue is false and transition is in progress
4906        if ( !swipe &&
4907            ( this._queue.length > 3 || index === false || ( !this._options.queue && this._queue.stalled ) ) ) {
4908            return;
4909        }
4910
4911        index = M.max( 0, M.min( parseInt( index, 10 ), this.getDataLength() - 1 ) );
4912
4913        rewind = typeof rewind !== 'undefined' ? !!rewind : index < this.getIndex();
4914
4915        _history = _history || false;
4916
4917        // do the history thing and return
4918        if ( !_history && Galleria.History ) {
4919            Galleria.History.set( index.toString() );
4920            return;
4921        }
4922
4923        if ( this.finger && index !== this._active ) {
4924            this.finger.to = -( index*this.finger.width );
4925            this.finger.index = index;
4926        }
4927        this._active = index;
4928
4929        // we do things a bit simpler in swipe:
4930        if ( swipe ) {
4931
4932            var data = this.getData(index),
4933                self = this;
4934            if ( !data ) {
4935                return;
4936            }
4937
4938            var src = this.isFullscreen() && data.big ? data.big : ( data.image || data.iframe ),
4939                image = this._controls.slides[index],
4940                cached = image.isCached( src ),
4941                thumb = this._thumbnails[ index ];
4942
4943            var evObj = {
4944                cached: cached,
4945                index: index,
4946                rewind: rewind,
4947                imageTarget: image.image,
4948                thumbTarget: thumb.image,
4949                galleriaData: data
4950            };
4951
4952            this.trigger($.extend(evObj, {
4953                type: Galleria.LOADSTART
4954            }));
4955
4956            self.$('container').removeClass( 'videoplay' );
4957
4958            var complete = function() {
4959
4960                self._layers[index].innerHTML = self.getData().layer || '';
4961
4962                self.trigger($.extend(evObj, {
4963                    type: Galleria.LOADFINISH
4964                }));
4965                self._playCheck();
4966            };
4967
4968            self._preload();
4969
4970            window.setTimeout(function() {
4971
4972                // load if not ready
4973                if ( !image.ready || $(image.image).attr('src') != src ) {
4974                    if ( data.iframe && !data.image ) {
4975                        image.isIframe = true;
4976                    }
4977                    image.load(src, function(image) {
4978                        evObj.imageTarget = image.image;
4979                        self._scaleImage(image, complete).trigger($.extend(evObj, {
4980                            type: Galleria.IMAGE
4981                        }));
4982                        complete();
4983                    });
4984                } else {
4985                    self.trigger($.extend(evObj, {
4986                        type: Galleria.IMAGE
4987                    }));
4988                    complete();
4989                }
4990            }, 100);
4991
4992        } else {
4993            protoArray.push.call( this._queue, {
4994                index : index,
4995                rewind : rewind
4996            });
4997            if ( !this._queue.stalled ) {
4998                this._show();
4999            }
5000        }
5001
5002        return this;
5003    },
5004
5005    // the internal _show method does the actual showing
5006    _show : function() {
5007
5008        // shortcuts
5009        var self = this,
5010            queue = this._queue[ 0 ],
5011            data = this.getData( queue.index );
5012
5013        if ( !data ) {
5014            return;
5015        }
5016
5017        var src = this.isFullscreen() && data.big ? data.big : ( data.image || data.iframe ),
5018            active = this._controls.getActive(),
5019            next = this._controls.getNext(),
5020            cached = next.isCached( src ),
5021            thumb = this._thumbnails[ queue.index ],
5022            mousetrigger = function() {
5023                $( next.image ).trigger( 'mouseup' );
5024            };
5025
5026        self.$('container').toggleClass('iframe', !!data.isIframe).removeClass( 'videoplay' );
5027
5028        // to be fired when loading & transition is complete:
5029        var complete = (function( data, next, active, queue, thumb ) {
5030
5031            return function() {
5032
5033                var win;
5034
5035                _transitions.active = false;
5036
5037                // optimize quality
5038                Utils.toggleQuality( next.image, self._options.imageQuality );
5039
5040                // remove old layer
5041                self._layers[ self._controls.active ].innerHTML = '';
5042
5043                // swap
5044                $( active.container ).css({
5045                    zIndex: 0,
5046                    opacity: 0
5047                }).show();
5048
5049                $( active.container ).find( 'iframe, .galleria-videoicon' ).remove();
5050                $( self._controls.frames[ self._controls.active ].container ).hide();
5051
5052                $( next.container ).css({
5053                    zIndex: 1,
5054                    left: 0,
5055                    top: 0
5056                }).show();
5057
5058                self._controls.swap();
5059
5060                // add pan according to option
5061                if ( self._options.imagePan ) {
5062                    self.addPan( next.image );
5063                }
5064
5065                // make the image clickable
5066                // order of precedence: iframe, link, lightbox, clicknext
5067                if ( ( data.iframe && data.image ) || data.link || self._options.lightbox || self._options.clicknext ) {
5068
5069                    $( next.image ).css({
5070                        cursor: 'pointer'
5071                    }).on( 'mouseup', function( e ) {
5072
5073                        // non-left click
5074                        if ( typeof e.which == 'number' && e.which > 1 ) {
5075                            return;
5076                        }
5077
5078                        // iframe / video
5079                        if ( data.iframe ) {
5080
5081                            if ( self.isPlaying() ) {
5082                                self.pause();
5083                            }
5084                            var frame = self._controls.frames[ self._controls.active ],
5085                                w = self._stageWidth,
5086                                h = self._stageHeight;
5087
5088                            $( frame.container ).css({
5089                                width: w,
5090                                height: h,
5091                                opacity: 0
5092                            }).show().animate({
5093                                opacity: 1
5094                            }, 200);
5095
5096                            window.setTimeout(function() {
5097                                frame.load( data.iframe + ( data.video ? '&autoplay=1' : '' ), {
5098                                    width: w,
5099                                    height: h
5100                                }, function( frame ) {
5101                                    self.$( 'container' ).addClass( 'videoplay' );
5102                                    frame.scale({
5103                                        width: self._stageWidth,
5104                                        height: self._stageHeight,
5105                                        iframelimit: data.video ? self._options.maxVideoSize : undef
5106                                    });
5107                                });
5108                            }, 100);
5109
5110                            return;
5111                        }
5112
5113                        // clicknext
5114                        if ( self._options.clicknext && !Galleria.TOUCH ) {
5115                            if ( self._options.pauseOnInteraction ) {
5116                                self.pause();
5117                            }
5118                            self.next();
5119                            return;
5120                        }
5121
5122                        // popup link
5123                        if ( data.link ) {
5124                            if ( self._options.popupLinks ) {
5125                                win = window.open( data.link, '_blank' );
5126                            } else {
5127                                window.location.href = data.link;
5128                            }
5129                            return;
5130                        }
5131
5132                        if ( self._options.lightbox ) {
5133                            self.openLightbox();
5134                        }
5135
5136                    });
5137                }
5138
5139                // check if we are playing
5140                self._playCheck();
5141
5142                // trigger IMAGE event
5143                self.trigger({
5144                    type: Galleria.IMAGE,
5145                    index: queue.index,
5146                    imageTarget: next.image,
5147                    thumbTarget: thumb.image,
5148                    galleriaData: data
5149                });
5150
5151                // remove the queued image
5152                protoArray.shift.call( self._queue );
5153
5154                // remove stalled
5155                self._queue.stalled = false;
5156
5157                // if we still have images in the queue, show it
5158                if ( self._queue.length ) {
5159                    self._show();
5160                }
5161
5162            };
5163        }( data, next, active, queue, thumb ));
5164
5165        // let the carousel follow
5166        if ( this._options.carousel && this._options.carouselFollow ) {
5167            this._carousel.follow( queue.index );
5168        }
5169
5170        // preload images
5171        self._preload();
5172
5173        // show the next image, just in case
5174        Utils.show( next.container );
5175
5176        next.isIframe = data.iframe && !data.image;
5177
5178        // add active classes
5179        $( self._thumbnails[ queue.index ].container )
5180            .addClass( 'active' )
5181            .siblings( '.active' )
5182            .removeClass( 'active' );
5183
5184        // trigger the LOADSTART event
5185        self.trigger( {
5186            type: Galleria.LOADSTART,
5187            cached: cached,
5188            index: queue.index,
5189            rewind: queue.rewind,
5190            imageTarget: next.image,
5191            thumbTarget: thumb.image,
5192            galleriaData: data
5193        });
5194
5195        // stall the queue
5196        self._queue.stalled = true;
5197
5198        // begin loading the next image
5199        next.load( src, function( next ) {
5200
5201            // add layer HTML
5202            var layer = $( self._layers[ 1-self._controls.active ] ).html( data.layer || '' ).hide();
5203
5204            self._scaleImage( next, {
5205
5206                complete: function( next ) {
5207
5208                    // toggle low quality for IE
5209                    if ( 'image' in active ) {
5210                        Utils.toggleQuality( active.image, false );
5211                    }
5212                    Utils.toggleQuality( next.image, false );
5213
5214                    // remove the image panning, if applied
5215                    // TODO: rethink if this is necessary
5216                    self.removePan();
5217
5218                    // set the captions and counter
5219                    self.setInfo( queue.index );
5220                    self.setCounter( queue.index );
5221
5222                    // show the layer now
5223                    if ( data.layer ) {
5224                        layer.show();
5225                        // inherit click events set on image
5226                        if ( ( data.iframe && data.image ) || data.link || self._options.lightbox || self._options.clicknext ) {
5227                            layer.css( 'cursor', 'pointer' ).off( 'mouseup' ).mouseup( mousetrigger );
5228                        }
5229                    }
5230
5231                    // add play icon
5232                    if( data.video && data.image ) {
5233                        _playIcon( next.container );
5234                    }
5235
5236                    var transition = self._options.transition;
5237
5238                    // can JavaScript loop through objects in order? yes.
5239                    $.each({
5240                        initial: active.image === null,
5241                        touch: Galleria.TOUCH,
5242                        fullscreen: self.isFullscreen()
5243                    }, function( type, arg ) {
5244                        if ( arg && self._options[ type + 'Transition' ] !== undef ) {
5245                            transition = self._options[ type + 'Transition' ];
5246                            return false;
5247                        }
5248                    });
5249
5250                    // validate the transition
5251                    if ( transition in _transitions.effects === false ) {
5252                        complete();
5253                    } else {
5254                        var params = {
5255                            prev: active.container,
5256                            next: next.container,
5257                            rewind: queue.rewind,
5258                            speed: self._options.transitionSpeed || 400
5259                        };
5260
5261                        _transitions.active = true;
5262
5263                        // call the transition function and send some stuff
5264                        _transitions.init.call( self, transition, params, complete );
5265
5266                    }
5267
5268                    // trigger the LOADFINISH event
5269                    self.trigger({
5270                        type: Galleria.LOADFINISH,
5271                        cached: cached,
5272                        index: queue.index,
5273                        rewind: queue.rewind,
5274                        imageTarget: next.image,
5275                        thumbTarget: self._thumbnails[ queue.index ].image,
5276                        galleriaData: self.getData( queue.index )
5277                    });
5278                }
5279            });
5280        });
5281    },
5282
5283    /**
5284        Gets the next index
5285
5286        @param {number} [base] Optional starting point
5287
5288        @returns {number} the next index, or the first if you are at the first (looping)
5289    */
5290
5291    getNext : function( base ) {
5292        base = typeof base === 'number' ? base : this.getIndex();
5293        return base === this.getDataLength() - 1 ? 0 : base + 1;
5294    },
5295
5296    /**
5297        Gets the previous index
5298
5299        @param {number} [base] Optional starting point
5300
5301        @returns {number} the previous index, or the last if you are at the first (looping)
5302    */
5303
5304    getPrev : function( base ) {
5305        base = typeof base === 'number' ? base : this.getIndex();
5306        return base === 0 ? this.getDataLength() - 1 : base - 1;
5307    },
5308
5309    /**
5310        Shows the next image in line
5311
5312        @returns Instance
5313    */
5314
5315    next : function() {
5316        if ( this.getDataLength() > 1 ) {
5317            this.show( this.getNext(), false );
5318        }
5319        return this;
5320    },
5321
5322    /**
5323        Shows the previous image in line
5324
5325        @returns Instance
5326    */
5327
5328    prev : function() {
5329        if ( this.getDataLength() > 1 ) {
5330            this.show( this.getPrev(), true );
5331        }
5332        return this;
5333    },
5334
5335    /**
5336        Retrieve a DOM element by element ID
5337
5338        @param {string} elemId The delement ID to fetch
5339
5340        @returns {HTMLElement} The elements DOM node or null if not found.
5341    */
5342
5343    get : function( elemId ) {
5344        return elemId in this._dom ? this._dom[ elemId ] : null;
5345    },
5346
5347    /**
5348        Retrieve a data object
5349
5350        @param {number} index The data index to retrieve.
5351        If no index specified it will take the currently active image
5352
5353        @returns {Object} The data object
5354    */
5355
5356    getData : function( index ) {
5357        return index in this._data ?
5358            this._data[ index ] : this._data[ this._active ];
5359    },
5360
5361    /**
5362        Retrieve the number of data items
5363
5364        @returns {number} The data length
5365    */
5366    getDataLength : function() {
5367        return this._data.length;
5368    },
5369
5370    /**
5371        Retrieve the currently active index
5372
5373        @returns {number|boolean} The active index or false if none found
5374    */
5375
5376    getIndex : function() {
5377        return typeof this._active === 'number' ? this._active : false;
5378    },
5379
5380    /**
5381        Retrieve the stage height
5382
5383        @returns {number} The stage height
5384    */
5385
5386    getStageHeight : function() {
5387        return this._stageHeight;
5388    },
5389
5390    /**
5391        Retrieve the stage width
5392
5393        @returns {number} The stage width
5394    */
5395
5396    getStageWidth : function() {
5397        return this._stageWidth;
5398    },
5399
5400    /**
5401        Retrieve the option
5402
5403        @param {string} key The option key to retrieve. If no key specified it will return all options in an object.
5404
5405        @returns option or options
5406    */
5407
5408    getOptions : function( key ) {
5409        return typeof key === 'undefined' ? this._options : this._options[ key ];
5410    },
5411
5412    /**
5413        Set options to the instance.
5414        You can set options using a key & value argument or a single object argument (see examples)
5415
5416        @param {string} key The option key
5417        @param {string} value the the options value
5418
5419        @example setOptions( 'autoplay', true )
5420        @example setOptions({ autoplay: true });
5421
5422        @returns Instance
5423    */
5424
5425    setOptions : function( key, value ) {
5426        if ( typeof key === 'object' ) {
5427            $.extend( this._options, key );
5428        } else {
5429            this._options[ key ] = value;
5430        }
5431        return this;
5432    },
5433
5434    /**
5435        Starts playing the slideshow
5436
5437        @param {number} delay Sets the slideshow interval in milliseconds.
5438        If you set it once, you can just call play() and get the same interval the next time.
5439
5440        @returns Instance
5441    */
5442
5443    play : function( delay ) {
5444
5445        this._playing = true;
5446
5447        this._playtime = delay || this._playtime;
5448
5449        this._playCheck();
5450
5451        this.trigger( Galleria.PLAY );
5452
5453        return this;
5454    },
5455
5456    /**
5457        Stops the slideshow if currently playing
5458
5459        @returns Instance
5460    */
5461
5462    pause : function() {
5463
5464        this._playing = false;
5465
5466        this.trigger( Galleria.PAUSE );
5467
5468        return this;
5469    },
5470
5471    /**
5472        Toggle between play and pause events.
5473
5474        @param {number} delay Sets the slideshow interval in milliseconds.
5475
5476        @returns Instance
5477    */
5478
5479    playToggle : function( delay ) {
5480        return ( this._playing ) ? this.pause() : this.play( delay );
5481    },
5482
5483    /**
5484        Checks if the gallery is currently playing
5485
5486        @returns {Boolean}
5487    */
5488
5489    isPlaying : function() {
5490        return this._playing;
5491    },
5492
5493    /**
5494        Checks if the gallery is currently in fullscreen mode
5495
5496        @returns {Boolean}
5497    */
5498
5499    isFullscreen : function() {
5500        return this._fullscreen.active;
5501    },
5502
5503    _playCheck : function() {
5504        var self = this,
5505            played = 0,
5506            interval = 20,
5507            now = Utils.timestamp(),
5508            timer_id = 'play' + this._id;
5509
5510        if ( this._playing ) {
5511
5512            this.clearTimer( timer_id );
5513
5514            var fn = function() {
5515
5516                played = Utils.timestamp() - now;
5517                if ( played >= self._playtime && self._playing ) {
5518                    self.clearTimer( timer_id );
5519                    self.next();
5520                    return;
5521                }
5522                if ( self._playing ) {
5523
5524                    // trigger the PROGRESS event
5525                    self.trigger({
5526                        type:         Galleria.PROGRESS,
5527                        percent:      M.ceil( played / self._playtime * 100 ),
5528                        seconds:      M.floor( played / 1000 ),
5529                        milliseconds: played
5530                    });
5531
5532                    self.addTimer( timer_id, fn, interval );
5533                }
5534            };
5535            self.addTimer( timer_id, fn, interval );
5536        }
5537    },
5538
5539    /**
5540        Modify the slideshow delay
5541
5542        @param {number} delay the number of milliseconds between slides,
5543
5544        @returns Instance
5545    */
5546
5547    setPlaytime: function( delay ) {
5548        this._playtime = delay;
5549        return this;
5550    },
5551
5552    setIndex: function( val ) {
5553        this._active = val;
5554        return this;
5555    },
5556
5557    /**
5558        Manually modify the counter
5559
5560        @param {number} [index] Optional data index to fectch,
5561        if no index found it assumes the currently active index
5562
5563        @returns Instance
5564    */
5565
5566    setCounter: function( index ) {
5567
5568        if ( typeof index === 'number' ) {
5569            index++;
5570        } else if ( typeof index === 'undefined' ) {
5571            index = this.getIndex()+1;
5572        }
5573
5574        this.get( 'current' ).innerHTML = index;
5575
5576        if ( IE ) { // weird IE bug
5577
5578            var count = this.$( 'counter' ),
5579                opacity = count.css( 'opacity' );
5580
5581            if ( parseInt( opacity, 10 ) === 1) {
5582                Utils.removeAlpha( count[0] );
5583            } else {
5584                this.$( 'counter' ).css( 'opacity', opacity );
5585            }
5586
5587        }
5588
5589        return this;
5590    },
5591
5592    /**
5593        Manually set captions
5594
5595        @param {number} [index] Optional data index to fectch and apply as caption,
5596        if no index found it assumes the currently active index
5597
5598        @returns Instance
5599    */
5600
5601    setInfo : function( index ) {
5602
5603        var self = this,
5604            data = this.getData( index );
5605
5606        $.each( ['title','description'], function( i, type ) {
5607
5608            var elem = self.$( 'info-' + type );
5609
5610            if ( !!data[type] ) {
5611                elem[ data[ type ].length ? 'show' : 'hide' ]().html( data[ type ] );
5612            } else {
5613               elem.empty().hide();
5614            }
5615        });
5616
5617        return this;
5618    },
5619
5620    /**
5621        Checks if the data contains any captions
5622
5623        @param {number} [index] Optional data index to fectch,
5624        if no index found it assumes the currently active index.
5625
5626        @returns {boolean}
5627    */
5628
5629    hasInfo : function( index ) {
5630
5631        var check = 'title description'.split(' '),
5632            i;
5633
5634        for ( i = 0; check[i]; i++ ) {
5635            if ( !!this.getData( index )[ check[i] ] ) {
5636                return true;
5637            }
5638        }
5639        return false;
5640
5641    },
5642
5643    jQuery : function( str ) {
5644
5645        var self = this,
5646            ret = [];
5647
5648        $.each( str.split(','), function( i, elemId ) {
5649            elemId = $.trim( elemId );
5650
5651            if ( self.get( elemId ) ) {
5652                ret.push( elemId );
5653            }
5654        });
5655
5656        var jQ = $( self.get( ret.shift() ) );
5657
5658        $.each( ret, function( i, elemId ) {
5659            jQ = jQ.add( self.get( elemId ) );
5660        });
5661
5662        return jQ;
5663
5664    },
5665
5666    /**
5667        Converts element IDs into a jQuery collection
5668        You can call for multiple IDs separated with commas.
5669
5670        @param {string} str One or more element IDs (comma-separated)
5671
5672        @returns jQuery
5673
5674        @example this.$('info,container').hide();
5675    */
5676
5677    $ : function( str ) {
5678        return this.jQuery.apply( this, Utils.array( arguments ) );
5679    }
5680
5681};
5682
5683// End of Galleria prototype
5684
5685// Add events as static variables
5686$.each( _events, function( i, ev ) {
5687
5688    // legacy events
5689    var type = /_/.test( ev ) ? ev.replace( /_/g, '' ) : ev;
5690
5691    Galleria[ ev.toUpperCase() ] = 'galleria.'+type;
5692
5693} );
5694
5695$.extend( Galleria, {
5696
5697    // Browser helpers
5698    IE9:     IE === 9,
5699    IE8:     IE === 8,
5700    IE7:     IE === 7,
5701    IE6:     IE === 6,
5702    IE:      IE,
5703    WEBKIT:  /webkit/.test( NAV ),
5704    CHROME:  /chrome/.test( NAV ),
5705    SAFARI:  /safari/.test( NAV ) && !(/chrome/.test( NAV )),
5706    QUIRK:   ( IE && doc.compatMode && doc.compatMode === "BackCompat" ),
5707    MAC:     /mac/.test( navigator.platform.toLowerCase() ),
5708    OPERA:   !!window.opera,
5709    IPHONE:  /iphone/.test( NAV ),
5710    IPAD:    /ipad/.test( NAV ),
5711    ANDROID: /android/.test( NAV ),
5712    TOUCH:   ( 'ontouchstart' in doc ) && MOBILE // rule out false positives on Win10
5713
5714});
5715
5716// Galleria static methods
5717
5718/**
5719    Adds a theme that you can use for your Gallery
5720
5721    @param {Object} theme Object that should contain all your theme settings.
5722    <ul>
5723        <li>name - name of the theme</li>
5724        <li>author - name of the author</li>
5725        <li>css - css file name (not path)</li>
5726        <li>defaults - default options to apply, including theme-specific options</li>
5727        <li>init - the init function</li>
5728    </ul>
5729
5730    @returns {Object} theme
5731*/
5732
5733Galleria.addTheme = function( theme ) {
5734
5735    // make sure we have a name
5736    if ( !theme.name ) {
5737        Galleria.raise('No theme name specified');
5738    }
5739
5740    // make sure it's compatible
5741    if ( !theme.version || parseInt(Galleria.version*10) > parseInt(theme.version*10) ) {
5742        Galleria.raise('This version of Galleria requires '+theme.name+' theme version '+parseInt(Galleria.version*10)/10+' or later', true);
5743    }
5744
5745    if ( typeof theme.defaults !== 'object' ) {
5746        theme.defaults = {};
5747    } else {
5748        theme.defaults = _legacyOptions( theme.defaults );
5749    }
5750
5751    var css = false,
5752        reg, reg2;
5753
5754    if ( typeof theme.css === 'string' ) {
5755
5756        // look for manually added CSS
5757        $('link').each(function( i, link ) {
5758            reg = new RegExp( theme.css );
5759            if ( reg.test( link.href ) ) {
5760
5761                // we found the css
5762                css = true;
5763
5764                // the themeload trigger
5765                _themeLoad( theme );
5766
5767                return false;
5768            }
5769        });
5770
5771        // else look for the absolute path and load the CSS dynamic
5772        if ( !css ) {
5773
5774
5775            $(function() {
5776                // Try to determine the css-path from the theme script.
5777                // In IE8/9, the script-dom-element seems to be not present
5778                // at once, if galleria itself is inserted into the dom
5779                // dynamically. We therefore try multiple times before raising
5780                // an error.
5781                var retryCount = 0;
5782                var tryLoadCss = function() {
5783                    $('script').each(function (i, script) {
5784                        // look for the theme script
5785                        reg = new RegExp('galleria\\.' + theme.name.toLowerCase() + '\\.');
5786                        reg2 = new RegExp('galleria\\.io\\/theme\\/' + theme.name.toLowerCase() + '\\/(\\d*\\.*)?(\\d*\\.*)?(\\d*\\/)?js');
5787                        if (reg.test(script.src) || reg2.test(script.src)) {
5788                            // we have a match
5789                            css = script.src.replace(/[^\/]*$/, '') + theme.css;
5790
5791                            window.setTimeout(function () {
5792                                Utils.loadCSS(css, 'galleria-theme-'+theme.name, function () {
5793
5794                                    // run galleries with this theme
5795                                    _themeLoad(theme);
5796
5797                                });
5798                            }, 1);
5799                        }
5800                    });
5801                    if (!css) {
5802                        if (retryCount++ > 5) {
5803                            Galleria.raise('No theme CSS loaded');
5804                        } else {
5805                            window.setTimeout(tryLoadCss, 500);
5806                        }
5807                    }
5808                };
5809                tryLoadCss();
5810            });
5811        }
5812
5813    } else {
5814
5815        // pass
5816        _themeLoad( theme );
5817    }
5818    return theme;
5819};
5820
5821/**
5822    loadTheme loads a theme js file and attaches a load event to Galleria
5823
5824    @param {string} src The relative path to the theme source file
5825
5826    @param {Object} [options] Optional options you want to apply
5827
5828    @returns Galleria
5829*/
5830
5831Galleria.loadTheme = function( src, options ) {
5832
5833    // Don't load if theme is already loaded
5834    if( $('script').filter(function() { return $(this).attr('src') == src; }).length ) {
5835        return;
5836    }
5837
5838    var loaded = false,
5839        err;
5840
5841    // start listening for the timeout onload
5842    $( window ).on('load', function() {
5843        if ( !loaded ) {
5844            // give it another 20 seconds
5845            err = window.setTimeout(function() {
5846                if ( !loaded ) {
5847                    Galleria.raise( "Galleria had problems loading theme at " + src + ". Please check theme path or load manually.", true );
5848                }
5849            }, 20000);
5850        }
5851    });
5852
5853    // load the theme
5854    Utils.loadScript( src, function() {
5855        loaded = true;
5856        window.clearTimeout( err );
5857    });
5858
5859    return Galleria;
5860};
5861
5862/**
5863    Retrieves a Galleria instance.
5864
5865    @param {number} [index] Optional index to retrieve.
5866    If no index is supplied, the method will return all instances in an array.
5867
5868    @returns Instance or Array of instances
5869*/
5870
5871Galleria.get = function( index ) {
5872    if ( !!_instances[ index ] ) {
5873        return _instances[ index ];
5874    } else if ( typeof index !== 'number' ) {
5875        return _instances;
5876    } else {
5877        Galleria.raise('Gallery index ' + index + ' not found');
5878    }
5879};
5880
5881/**
5882
5883    Configure Galleria options via a static function.
5884    The options will be applied to all instances
5885
5886    @param {string|object} key The options to apply or a key
5887
5888    @param [value] If key is a string, this is the value
5889
5890    @returns Galleria
5891
5892*/
5893
5894Galleria.configure = function( key, value ) {
5895
5896    var opts = {};
5897
5898    if( typeof key == 'string' && value ) {
5899        opts[key] = value;
5900        key = opts;
5901    } else {
5902        $.extend( opts, key );
5903    }
5904
5905    Galleria.configure.options = opts;
5906
5907    $.each( Galleria.get(), function(i, instance) {
5908        instance.setOptions( opts );
5909    });
5910
5911    return Galleria;
5912};
5913
5914Galleria.configure.options = {};
5915
5916/**
5917
5918    Bind a Galleria event to the gallery
5919
5920    @param {string} type A string representing the galleria event
5921
5922    @param {function} callback The function that should run when the event is triggered
5923
5924    @returns Galleria
5925
5926*/
5927
5928Galleria.on = function( type, callback ) {
5929    if ( !type ) {
5930        return;
5931    }
5932
5933    callback = callback || F;
5934
5935    // hash the bind
5936    var hash = type + callback.toString().replace(/\s/g,'') + Utils.timestamp();
5937
5938    // for existing instances
5939    $.each( Galleria.get(), function(i, instance) {
5940        instance._binds.push( hash );
5941        instance.bind( type, callback );
5942    });
5943
5944    // for future instances
5945    Galleria.on.binds.push({
5946        type: type,
5947        callback: callback,
5948        hash: hash
5949    });
5950
5951    return Galleria;
5952};
5953
5954Galleria.on.binds = [];
5955
5956/**
5957
5958    Run Galleria
5959    Alias for $(selector).galleria(options)
5960
5961    @param {string} selector A selector of element(s) to intialize galleria to
5962
5963    @param {object} options The options to apply
5964
5965    @returns Galleria
5966
5967*/
5968
5969Galleria.run = function( selector, options ) {
5970    if ( $.isFunction( options ) ) {
5971        options = { extend: options };
5972    }
5973    $( selector || '#galleria' ).galleria( options );
5974    return Galleria;
5975};
5976
5977/**
5978    Creates a transition to be used in your gallery
5979
5980    @param {string} name The name of the transition that you will use as an option
5981
5982    @param {Function} fn The function to be executed in the transition.
5983    The function contains two arguments, params and complete.
5984    Use the params Object to integrate the transition, and then call complete when you are done.
5985
5986    @returns Galleria
5987
5988*/
5989
5990Galleria.addTransition = function( name, fn ) {
5991    _transitions.effects[name] = fn;
5992    return Galleria;
5993};
5994
5995/**
5996    The Galleria utilites
5997*/
5998
5999Galleria.utils = Utils;
6000
6001/**
6002    A helper metod for cross-browser logging.
6003    It uses the console log if available otherwise it falls back to alert
6004
6005    @example Galleria.log("hello", document.body, [1,2,3]);
6006*/
6007
6008Galleria.log = function() {
6009    var args = Utils.array( arguments );
6010    if( 'console' in window && 'log' in window.console ) {
6011        try {
6012            return window.console.log.apply( window.console, args );
6013        } catch( e ) {
6014            $.each( args, function() {
6015                window.console.log(this);
6016            });
6017        }
6018    } else {
6019        return window.alert( args.join('<br>') );
6020    }
6021};
6022
6023/**
6024    A ready method for adding callbacks when a gallery is ready
6025    Each method is call before the extend option for every instance
6026
6027    @param {function} callback The function to call
6028
6029    @returns Galleria
6030*/
6031
6032Galleria.ready = function( fn ) {
6033    if ( typeof fn != 'function' ) {
6034        return Galleria;
6035    }
6036    $.each( _galleries, function( i, gallery ) {
6037        fn.call( gallery, gallery._options );
6038    });
6039    Galleria.ready.callbacks.push( fn );
6040    return Galleria;
6041};
6042
6043Galleria.ready.callbacks = [];
6044
6045/**
6046    Method for raising errors
6047
6048    @param {string} msg The message to throw
6049
6050    @param {boolean} [fatal] Set this to true to override debug settings and display a fatal error
6051*/
6052
6053Galleria.raise = function( msg, fatal ) {
6054
6055    var type = fatal ? 'Fatal error' : 'Error',
6056
6057        css = {
6058            color: '#fff',
6059            position: 'absolute',
6060            top: 0,
6061            left: 0,
6062            zIndex: 100000
6063        },
6064
6065        echo = function( msg ) {
6066
6067            var html = '<div style="padding:4px;margin:0 0 2px;background:#' +
6068                ( fatal ? '811' : '222' ) + ';">' +
6069                ( fatal ? '<strong>' + type + ': </strong>' : '' ) +
6070                msg + '</div>';
6071
6072            $.each( _instances, function() {
6073
6074                var cont = this.$( 'errors' ),
6075                    target = this.$( 'target' );
6076
6077                if ( !cont.length ) {
6078
6079                    target.css( 'position', 'relative' );
6080
6081                    cont = this.addElement( 'errors' ).appendChild( 'target', 'errors' ).$( 'errors' ).css(css);
6082                }
6083                cont.append( html );
6084
6085            });
6086
6087            if ( !_instances.length ) {
6088                $('<div>').css( $.extend( css, { position: 'fixed' } ) ).append( html ).appendTo( DOM().body );
6089            }
6090        };
6091
6092    // if debug is on, display errors and throw exception if fatal
6093    if ( DEBUG ) {
6094        echo( msg );
6095        if ( fatal ) {
6096            throw new Error(type + ': ' + msg);
6097        }
6098
6099    // else just echo a silent generic error if fatal
6100    } else if ( fatal ) {
6101        if ( _hasError ) {
6102            return;
6103        }
6104        _hasError = true;
6105        fatal = false;
6106        echo( 'Gallery could not load.' );
6107    }
6108};
6109
6110// Add the version
6111Galleria.version = VERSION;
6112
6113Galleria.getLoadedThemes = function() {
6114    return $.map(_loadedThemes, function(theme) {
6115        return theme.name;
6116    });
6117};
6118
6119/**
6120    A method for checking what version of Galleria the user has installed and throws a readable error if the user needs to upgrade.
6121    Useful when building plugins that requires a certain version to function.
6122
6123    @param {number} version The minimum version required
6124
6125    @param {string} [msg] Optional message to display. If not specified, Galleria will throw a generic error.
6126
6127    @returns Galleria
6128*/
6129
6130Galleria.requires = function( version, msg ) {
6131    msg = msg || 'You need to upgrade Galleria to version ' + version + ' to use one or more components.';
6132    if ( Galleria.version < version ) {
6133        Galleria.raise(msg, true);
6134    }
6135    return Galleria;
6136};
6137
6138/**
6139    Adds preload, cache, scale and crop functionality
6140
6141    @constructor
6142
6143    @requires jQuery
6144
6145    @param {number} [id] Optional id to keep track of instances
6146*/
6147
6148Galleria.Picture = function( id ) {
6149
6150    // save the id
6151    this.id = id || null;
6152
6153    // the image should be null until loaded
6154    this.image = null;
6155
6156    // Create a new container
6157    this.container = Utils.create('galleria-image');
6158
6159    // add container styles
6160    $( this.container ).css({
6161        overflow: 'hidden',
6162        position: 'relative' // for IE Standards mode
6163    });
6164
6165    // saves the original measurements
6166    this.original = {
6167        width: 0,
6168        height: 0
6169    };
6170
6171    // flag when the image is ready
6172    this.ready = false;
6173
6174    // flag for iframe Picture
6175    this.isIframe = false;
6176
6177};
6178
6179Galleria.Picture.prototype = {
6180
6181    // the inherited cache object
6182    cache: {},
6183
6184    // show the image on stage
6185    show: function() {
6186        Utils.show( this.image );
6187    },
6188
6189    // hide the image
6190    hide: function() {
6191        Utils.moveOut( this.image );
6192    },
6193
6194    clear: function() {
6195        this.image = null;
6196    },
6197
6198    /**
6199        Checks if an image is in cache
6200
6201        @param {string} src The image source path, ex '/path/to/img.jpg'
6202
6203        @returns {boolean}
6204    */
6205
6206    isCached: function( src ) {
6207        return !!this.cache[src];
6208    },
6209
6210    /**
6211        Preloads an image into the cache
6212
6213        @param {string} src The image source path, ex '/path/to/img.jpg'
6214
6215        @returns Galleria.Picture
6216    */
6217
6218    preload: function( src ) {
6219        var $image = $( new Image() ).on( 'load', (function(src, cache) {
6220            return function() {
6221                cache[ src ] = src;
6222            };
6223        }( src, this.cache )));
6224
6225        // due to a bug in safari, need to set srcset first
6226        if (src.srcset) {
6227            $image.attr( 'srcset', src.srcset );
6228        }
6229
6230        if (src.sizes) {
6231            $image.attr( 'sizes', src.sizes );
6232        }
6233
6234        $image.attr( 'src', src );
6235    },
6236
6237    /**
6238        Loads an image and call the callback when ready.
6239        Will also add the image to cache.
6240
6241        @param {string} src The image source path, ex '/path/to/img.jpg'
6242        @param {Object} [size] The forced size of the image, defined as an object { width: xx, height:xx }
6243        @param {Function} callback The function to be executed when the image is loaded & scaled
6244
6245        @returns The image container (jQuery object)
6246    */
6247
6248    load: function(src, size, callback) {
6249
6250        if ( typeof size == 'function' ) {
6251            callback = size;
6252            size = null;
6253        }
6254
6255        if( this.isIframe ) {
6256            var id = 'if'+new Date().getTime();
6257
6258            var iframe = this.image = $('<iframe>', {
6259                src: src,
6260                frameborder: 0,
6261                id: id,
6262                allowfullscreen: true,
6263                css: { visibility: 'hidden' }
6264            })[0];
6265
6266            if ( size ) {
6267                $( iframe ).css( size );
6268            }
6269
6270            $( this.container ).find( 'iframe,img' ).remove();
6271
6272            this.container.appendChild( this.image );
6273
6274            $('#'+id).on( 'load', (function( self, callback ) {
6275                return function() {
6276                    window.setTimeout(function() {
6277                        $( self.image ).css( 'visibility', 'visible' );
6278                        if( typeof callback == 'function' ) {
6279                            callback.call( self, self );
6280                        }
6281                    }, 10);
6282                };
6283            }( this, callback )));
6284
6285            return this.container;
6286        }
6287
6288        this.image = new Image();
6289
6290        // IE8 opacity inherit bug
6291        if ( Galleria.IE8 ) {
6292            $( this.image ).css( 'filter', 'inherit' );
6293        }
6294
6295        // FF shaking images bug:
6296        // http://support.galleria.io/discussions/problems/12245-shaking-photos
6297        if ( !Galleria.IE && !Galleria.CHROME && !Galleria.SAFARI ) {
6298            $( this.image ).css( 'image-rendering', 'optimizequality' );
6299        }
6300
6301        var reload = false,
6302            resort = false,
6303
6304            // some jquery cache
6305            $container = $( this.container ),
6306            $image = $( this.image ),
6307
6308            onerror = function() {
6309                if ( !reload ) {
6310                    reload = true;
6311                    // reload the image with a timestamp
6312                    window.setTimeout((function(image, src) {
6313                        return function() {
6314                            image.attr('src', src + (src.indexOf('?') > -1 ? '&' : '?') + Utils.timestamp() );
6315                        };
6316                    }( $(this), src )), 50);
6317                } else {
6318                    // apply the dummy image if it exists
6319                    if ( DUMMY ) {
6320                        $( this ).attr( 'src', DUMMY );
6321                    } else {
6322                        Galleria.raise('Image not found: ' + src);
6323                    }
6324                }
6325            },
6326
6327            // the onload method
6328            onload = (function( self, callback, src ) {
6329
6330                return function() {
6331
6332                    var complete = function() {
6333
6334                        $( this ).off( 'load' );
6335
6336                        // save the original size
6337                        self.original = size || {
6338                            height: this.height,
6339                            width: this.width
6340                        };
6341
6342                        // translate3d if needed
6343                        if ( Galleria.HAS3D ) {
6344                            this.style.MozTransform = this.style.webkitTransform = 'translate3d(0,0,0)';
6345                        }
6346
6347                        $container.append( this );
6348
6349                        self.cache[ src ] = src; // will override old cache
6350
6351                        if (typeof callback == 'function' ) {
6352                            window.setTimeout(function() {
6353                                callback.call( self, self );
6354                            },1);
6355                        }
6356                    };
6357
6358                    // Delay the callback to "fix" the Adblock Bug
6359                    // http://code.google.com/p/adblockforchrome/issues/detail?id=3701
6360                    if ( ( !this.width || !this.height ) ) {
6361                        (function( img ) {
6362                            Utils.wait({
6363                                until: function() {
6364                                    return img.width && img.height;
6365                                },
6366                                success: function() {
6367                                    complete.call( img );
6368                                },
6369                                error: function() {
6370                                    if ( !resort ) {
6371                                        $(new Image()).on( 'load', onload ).attr( 'src', img.src );
6372                                        resort = true;
6373                                    } else {
6374                                        Galleria.raise('Could not extract width/height from image: ' + img.src +
6375                                            '. Traced measures: width:' + img.width + 'px, height: ' + img.height + 'px.');
6376                                    }
6377                                },
6378                                timeout: 100
6379                            });
6380                        }( this ));
6381                    } else {
6382                        complete.call( this );
6383                    }
6384                };
6385            }( this, callback, src ));
6386
6387        // remove any previous images
6388        $container.find( 'iframe,img' ).remove();
6389
6390        // append the image
6391        $image.css( 'display', 'block');
6392
6393        // hide it for now
6394        Utils.hide( this.image );
6395
6396        // remove any max/min scaling
6397        $.each('minWidth minHeight maxWidth maxHeight'.split(' '), function(i, prop) {
6398            $image.css(prop, (/min/.test(prop) ? '0' : 'none'));
6399        });
6400
6401        // begin load and insert in cache when done
6402        $image.on( 'load', onload ).on( 'error', onerror );
6403        if (src.srcset) {
6404            $image.attr( 'srcset', src.srcset );
6405        }
6406        if (src.sizes) {
6407            $image.attr( 'sizes', src.sizes );
6408        }
6409        $image.attr( 'src', src );
6410
6411        // return the container
6412        return this.container;
6413    },
6414
6415    /**
6416        Scales and crops the image
6417
6418        @param {Object} options The method takes an object with a number of options:
6419
6420        <ul>
6421            <li>width - width of the container</li>
6422            <li>height - height of the container</li>
6423            <li>min - minimum scale ratio</li>
6424            <li>max - maximum scale ratio</li>
6425            <li>margin - distance in pixels from the image border to the container</li>
6426            <li>complete - a callback that fires when scaling is complete</li>
6427            <li>position - positions the image, works like the css background-image property.</li>
6428            <li>crop - defines how to crop. Can be true, false, 'width' or 'height'</li>
6429            <li>canvas - set to true to try a canvas-based rescale</li>
6430        </ul>
6431
6432        @returns The image container object (jQuery)
6433    */
6434
6435    scale: function( options ) {
6436
6437        var self = this;
6438
6439        // extend some defaults
6440        options = $.extend({
6441            width: 0,
6442            height: 0,
6443            min: undef,
6444            max: undef,
6445            margin: 0,
6446            complete: F,
6447            position: 'center',
6448            crop: false,
6449            canvas: false,
6450            iframelimit: undef
6451        }, options);
6452
6453        if( this.isIframe ) {
6454
6455            var cw = options.width,
6456                ch = options.height,
6457                nw, nh;
6458            if ( options.iframelimit ) {
6459                var r = M.min( options.iframelimit/cw, options.iframelimit/ch );
6460                if ( r < 1 ) {
6461                    nw = cw * r;
6462                    nh = ch * r;
6463
6464                    $( this.image ).css({
6465                        top: ch/2-nh/2,
6466                        left: cw/2-nw/2,
6467                        position: 'absolute'
6468                    });
6469                } else {
6470                    $( this.image ).css({
6471                        top: 0,
6472                        left: 0
6473                    });
6474                }
6475            }
6476            $( this.image ).width( nw || cw ).height( nh || ch ).removeAttr( 'width' ).removeAttr( 'height' );
6477            $( this.container ).width( cw ).height( ch );
6478            options.complete.call(self, self);
6479            try {
6480                if( this.image.contentWindow ) {
6481                    $( this.image.contentWindow ).trigger('resize');
6482                }
6483            } catch(e) {}
6484            return this.container;
6485
6486        }
6487
6488        // return the element if no image found
6489        if (!this.image) {
6490            return this.container;
6491        }
6492
6493        // store locale variables
6494        var width,
6495            height,
6496            $container = $( self.container ),
6497            data;
6498
6499        // wait for the width/height
6500        Utils.wait({
6501            until: function() {
6502                width  = options.width ||
6503                         $container.width() ||
6504                         Utils.parseValue( $container.css('width') );
6505
6506                height = options.height ||
6507                         $container.height() ||
6508                         Utils.parseValue( $container.css('height') );
6509
6510                return width && height;
6511            },
6512            success: function() {
6513
6514                // calculate some cropping
6515                var newWidth = ( width - options.margin * 2 ) / self.original.width,
6516                    newHeight = ( height - options.margin * 2 ) / self.original.height,
6517                    min = M.min( newWidth, newHeight ),
6518                    max = M.max( newWidth, newHeight ),
6519                    cropMap = {
6520                        'true'  : max,
6521                        'width' : newWidth,
6522                        'height': newHeight,
6523                        'false' : min,
6524                        'landscape': self.original.width > self.original.height ? max : min,
6525                        'portrait': self.original.width < self.original.height ? max : min
6526                    },
6527                    ratio = cropMap[ options.crop.toString() ],
6528                    canvasKey = '';
6529
6530                // allow maxScaleRatio
6531                if ( options.max ) {
6532                    ratio = M.min( options.max, ratio );
6533                }
6534
6535                // allow minScaleRatio
6536                if ( options.min ) {
6537                    ratio = M.max( options.min, ratio );
6538                }
6539
6540                $.each( ['width','height'], function( i, m ) {
6541                    $( self.image )[ m ]( self[ m ] = self.image[ m ] = M.round( self.original[ m ] * ratio ) );
6542                });
6543
6544                $( self.container ).width( width ).height( height );
6545
6546                if ( options.canvas && _canvas ) {
6547
6548                    _canvas.elem.width = self.width;
6549                    _canvas.elem.height = self.height;
6550
6551                    canvasKey = self.image.src + ':' + self.width + 'x' + self.height;
6552
6553                    self.image.src = _canvas.cache[ canvasKey ] || (function( key ) {
6554
6555                        _canvas.context.drawImage(self.image, 0, 0, self.original.width*ratio, self.original.height*ratio);
6556
6557                        try {
6558
6559                            data = _canvas.elem.toDataURL();
6560                            _canvas.length += data.length;
6561                            _canvas.cache[ key ] = data;
6562                            return data;
6563
6564                        } catch( e ) {
6565                            return self.image.src;
6566                        }
6567
6568                    }( canvasKey ) );
6569
6570                }
6571
6572                // calculate image_position
6573                var pos = {},
6574                    mix = {},
6575                    getPosition = function(value, measure, margin) {
6576                        var result = 0;
6577                        if (/\%/.test(value)) {
6578                            var flt = parseInt( value, 10 ) / 100,
6579                                m = self.image[ measure ] || $( self.image )[ measure ]();
6580
6581                            result = M.ceil( m * -1 * flt + margin * flt );
6582                        } else {
6583                            result = Utils.parseValue( value );
6584                        }
6585                        return result;
6586                    },
6587                    positionMap = {
6588                        'top': { top: 0 },
6589                        'left': { left: 0 },
6590                        'right': { left: '100%' },
6591                        'bottom': { top: '100%' }
6592                    };
6593
6594                $.each( options.position.toLowerCase().split(' '), function( i, value ) {
6595                    if ( value === 'center' ) {
6596                        value = '50%';
6597                    }
6598                    pos[i ? 'top' : 'left'] = value;
6599                });
6600
6601                $.each( pos, function( i, value ) {
6602                    if ( positionMap.hasOwnProperty( value ) ) {
6603                        $.extend( mix, positionMap[ value ] );
6604                    }
6605                });
6606
6607                pos = pos.top ? $.extend( pos, mix ) : mix;
6608
6609                pos = $.extend({
6610                    top: '50%',
6611                    left: '50%'
6612                }, pos);
6613
6614                // apply position
6615                $( self.image ).css({
6616                    position : 'absolute',
6617                    top :  getPosition(pos.top, 'height', height),
6618                    left : getPosition(pos.left, 'width', width)
6619                });
6620
6621                // show the image
6622                self.show();
6623
6624                // flag ready and call the callback
6625                self.ready = true;
6626                options.complete.call( self, self );
6627
6628            },
6629            error: function() {
6630                
6630// Edited by Photocrati: silently log this instead of raising an error
6631                if (window.console && console.trace) {
6632                    // Galleria.raise('Could not scale image: '+self.image.src);
6633                    console.trace('Could not scale image: ' + self.image.src);
6634                }
vendor: 10,234 bytes, lines 6634-6991
6634
6635            },
6636            timeout: 1000
6637        });
6638        return this;
6639    }
6640};
6641
6642// our own easings
6643$.extend( $.easing, {
6644
6645    galleria: function (_, t, b, c, d) {
6646        if ((t/=d/2) < 1) {
6647            return c/2*t*t*t + b;
6648        }
6649        return c/2*((t-=2)*t*t + 2) + b;
6650    },
6651
6652    galleriaIn: function (_, t, b, c, d) {
6653        return c*(t/=d)*t + b;
6654    },
6655
6656    galleriaOut: function (_, t, b, c, d) {
6657        return -c *(t/=d)*(t-2) + b;
6658    }
6659
6660});
6661
6662
6663// Forked version of Ainos Finger.js for native-style touch
6664
6665Galleria.Finger = (function() {
6666
6667    var abs = M.abs;
6668
6669    // test for translate3d support
6670    var has3d = Galleria.HAS3D = (function() {
6671
6672        var el = doc.createElement('p'),
6673            has3d,
6674            t = ['webkit','O','ms','Moz',''],
6675            s,
6676            i=0,
6677            a = 'transform';
6678
6679        DOM().html.insertBefore(el, null);
6680
6681        for (; t[i]; i++) {
6682            s = t[i] ? t[i]+'Transform' : a;
6683            if (el.style[s] !== undefined) {
6684                el.style[s] = "translate3d(1px,1px,1px)";
6685                has3d = $(el).css(t[i] ? '-'+t[i].toLowerCase()+'-'+a : a);
6686            }
6687        }
6688
6689        DOM().html.removeChild(el);
6690        return (has3d !== undefined && has3d.length > 0 && has3d !== "none");
6691    }());
6692
6693    // request animation shim
6694    var requestFrame = (function(){
6695        var r = 'RequestAnimationFrame';
6696        return window.requestAnimationFrame ||
6697               window['webkit'+r] ||
6698               window['moz'+r] ||
6699               window['o'+r] ||
6700               window['ms'+r] ||
6701               function( callback ) {
6702                   window.setTimeout(callback, 1000 / 60);
6703               };
6704    }());
6705
6706    var Finger = function(elem, options) {
6707
6708        // default options
6709        this.config = {
6710            start: 0,
6711            duration: 500,
6712            onchange: function() {},
6713            oncomplete: function() {},
6714            easing: function(x,t,b,c,d) {
6715                return -c * ((t=t/d-1)*t*t*t - 1) + b; // easeOutQuart
6716            }
6717        };
6718
6719        this.easeout = function (x, t, b, c, d) {
6720            return c*((t=t/d-1)*t*t*t*t + 1) + b;
6721        };
6722
6723        if ( !elem.children.length ) {
6724            return;
6725        }
6726
6727        var self = this;
6728
6729        // extend options
6730        $.extend(this.config, options);
6731
6732        this.elem = elem;
6733        this.child = elem.children[0];
6734        this.to = this.pos = 0;
6735        this.touching = false;
6736        this.start = {};
6737        this.index = this.config.start;
6738        this.anim = 0;
6739        this.easing = this.config.easing;
6740
6741        if ( !has3d ) {
6742          this.child.style.position = 'absolute';
6743          this.elem.style.position = 'relative';
6744        }
6745
6746        // Bind event handlers to context
6747        $.each(['ontouchstart','ontouchmove','ontouchend','setup'], function(i, fn) {
6748            self[fn] = (function(caller) {
6749                return function() {
6750                    caller.apply( self, arguments );
6751                };
6752            }(self[fn]));
6753        });
6754
6755        // the physical animator
6756        this.setX = function() {
6757
6758            var style = self.child.style;
6759
6760            if (!has3d) {
6761                // this is actually faster than CSS3 translate
6762                style.left = self.pos+'px';
6763                return;
6764            }
6765            style.MozTransform = style.webkitTransform = style.transform = 'translate3d(' + self.pos + 'px,0,0)';
6766            return;
6767        };
6768
6769        // bind events
6770        $(elem).on('touchstart', this.ontouchstart);
6771        $(window).on('resize', this.setup);
6772        $(window).on('orientationchange', this.setup);
6773
6774        // set up width
6775        this.setup();
6776
6777        // start the animations
6778        (function animloop(){
6779          requestFrame(animloop);
6780          self.loop.call( self );
6781        }());
6782
6783    };
6784
6785    Finger.prototype = {
6786
6787        constructor: Finger,
6788
6789        setup: function() {
6790            this.width = $( this.elem ).width();
6791            this.length = M.ceil( $(this.child).width() / this.width );
6792            if ( this.index !== 0 ) {
6793                this.index = M.max(0, M.min( this.index, this.length-1 ) );
6794                this.pos = this.to = -this.width*this.index;
6795            }
6796        },
6797
6798        setPosition: function(pos) {
6799            this.pos = pos;
6800            this.to = pos;
6801        },
6802
6803        ontouchstart: function(e) {
6804
6805            var touch = e.originalEvent.touches;
6806
6807            this.start = {
6808                pageX: touch[0].pageX,
6809                pageY: touch[0].pageY,
6810                time:  +new Date()
6811            };
6812
6813            this.isScrolling = null;
6814            this.touching = true;
6815            this.deltaX = 0;
6816
6817            $doc.on('touchmove', this.ontouchmove);
6818            $doc.on('touchend', this.ontouchend);
6819        },
6820
6821        ontouchmove: function(e) {
6822
6823            var touch = e.originalEvent.touches;
6824
6825            // ensure swiping with one touch and not pinching
6826            if( touch && touch.length > 1 || e.scale && e.scale !== 1 ) {
6827                return;
6828            }
6829
6830            this.deltaX = touch[0].pageX - this.start.pageX;
6831
6832            // determine if scrolling test has run - one time test
6833            if ( this.isScrolling === null ) {
6834                this.isScrolling = !!(
6835                    this.isScrolling ||
6836                    M.abs(this.deltaX) < M.abs(touch[0].pageY - this.start.pageY)
6837                );
6838            }
6839
6840            // if user is not trying to scroll vertically
6841            if (!this.isScrolling) {
6842
6843                // prevent native scrolling
6844                e.preventDefault();
6845
6846                // increase resistance if first or last slide
6847                this.deltaX /= ( (!this.index && this.deltaX > 0 || this.index == this.length - 1 && this.deltaX < 0 ) ?
6848                    ( M.abs(this.deltaX) / this.width + 1.8 )  : 1 );
6849                this.to = this.deltaX - this.index * this.width;
6850            }
6851            e.stopPropagation();
6852        },
6853
6854        ontouchend: function(e) {
6855
6856            this.touching = false;
6857
6858            // determine if slide attempt triggers next/prev slide
6859            var isValidSlide = +new Date() - this.start.time < 250 &&
6860                M.abs(this.deltaX) > 40 ||
6861                M.abs(this.deltaX) > this.width/2,
6862
6863                isPastBounds = !this.index && this.deltaX > 0 ||
6864                    this.index == this.length - 1 && this.deltaX < 0;
6865
6866            // if not scrolling vertically
6867            if ( !this.isScrolling ) {
6868                this.show( this.index + ( isValidSlide && !isPastBounds ? (this.deltaX < 0 ? 1 : -1) : 0 ) );
6869            }
6870
6871            $doc.off('touchmove', this.ontouchmove);
6872            $doc.off('touchend', this.ontouchend);
6873        },
6874
6875        show: function( index ) {
6876            if ( index != this.index ) {
6877                this.config.onchange.call(this, index);
6878            } else {
6879                this.to = -( index*this.width );
6880            }
6881        },
6882
6883        moveTo: function( index ) {
6884            if ( index != this.index ) {
6885                this.pos = this.to = -( index*this.width );
6886                this.index = index;
6887            }
6888        },
6889
6890        loop: function() {
6891
6892            var distance = this.to - this.pos,
6893                factor = 1;
6894
6895            if ( this.width && distance ) {
6896                factor = M.max(0.5, M.min(1.5, M.abs(distance / this.width) ) );
6897            }
6898
6899            // if distance is short or the user is touching, do a 1-1 animation
6900            if ( this.touching || M.abs(distance) <= 1 ) {
6901                this.pos = this.to;
6902                distance = 0;
6903                if ( this.anim && !this.touching ) {
6904                    this.config.oncomplete( this.index );
6905                }
6906                this.anim = 0;
6907                this.easing = this.config.easing;
6908            } else {
6909                if ( !this.anim ) {
6910                    // save animation parameters
6911                    this.anim = { start: this.pos, time: +new Date(), distance: distance, factor: factor, destination: this.to };
6912                }
6913                // check if to has changed or time has run out
6914                var elapsed = +new Date() - this.anim.time;
6915                var duration = this.config.duration*this.anim.factor;
6916
6917                if ( elapsed > duration || this.anim.destination != this.to ) {
6918                    this.anim = 0;
6919                    this.easing = this.easeout;
6920                    return;
6921                }
6922                // apply easing
6923                this.pos = this.easing(
6924                    null,
6925                    elapsed,
6926                    this.anim.start,
6927                    this.anim.distance,
6928                    duration
6929                );
6930            }
6931            this.setX();
6932        }
6933    };
6934
6935    return Finger;
6936
6937}());
6938
6939// the plugin initializer
6940$.fn.galleria = function( options ) {
6941
6942    var selector = this.selector;
6943
6944    // try domReady if element not found
6945    if ( !$(this).length ) {
6946
6947        $(function() {
6948            if ( $( selector ).length ) {
6949
6950                // if found on domReady, go ahead
6951                $( selector ).galleria( options );
6952
6953            } else {
6954
6955                // if not, try fetching the element for 5 secs, then raise a warning.
6956                Galleria.utils.wait({
6957                    until: function() {
6958                        return $( selector ).length;
6959                    },
6960                    success: function() {
6961                        $( selector ).galleria( options );
6962                    },
6963                    error: function() {
6964                        Galleria.raise('Init failed: Galleria could not find the element "'+selector+'".');
6965                    },
6966                    timeout: 5000
6967                });
6968
6969            }
6970        });
6971        return this;
6972    }
6973
6974    return this.each(function() {
6975
6976        // destroy previous instance and prepare for new load
6977        if ( $.data(this, 'galleria') ) {
6978            $.data( this, 'galleria' ).destroy();
6979            $( this ).find( '*' ).hide();
6980        }
6981
6982        // load the new gallery
6983        $.data( this, 'galleria', new Galleria().init( this, options ) );
6984    });
6985
6986};
6987
6988// phew
6989return Galleria;
6990
6991}));

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.