PageSourceSearch

https://www.teatroreal.es/themes/custom/teatro_real/dist/js/galleria-1.5.7.js?tlt7qj

js teatroreal.es collected 2026-09-24 09:27:32 UTC 216,665 bytes, 6,930 lines download raw bytes

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

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.