source: extensions/stripped-galleria/galleria/galleria-1.2.6.js @ 12975

Last change on this file since 12975 was 12975, checked in by Zaphod, 12 years ago

version 1.2.0

File size: 150.5 KB
Line 
1/**
2 * @preserve Galleria v 1.2.6 2011-12-12
3 * http://galleria.aino.se
4 *
5 * Copyright (c) 2011, Aino
6 * Licensed under the MIT license.
7 */
8
9/*global jQuery, navigator, Galleria:true, Image */
10
11(function( $ ) {
12
13// some references
14var undef,
15    window = this,
16    doc    = window.document,
17    $doc   = $( doc ),
18    $win   = $( window ),
19
20// internal constants
21    VERSION = 1.26,
22    DEBUG = true,
23    TIMEOUT = 30000,
24    DUMMY = false,
25    NAV = navigator.userAgent.toLowerCase(),
26    HASH = window.location.hash.replace(/#\//, ''),
27    IE = (function() {
28
29        var v = 3,
30            div = doc.createElement( 'div' ),
31            all = div.getElementsByTagName( 'i' );
32
33        do {
34            div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->';
35        } while ( all[0] );
36
37        return v > 4 ? v : undef;
38
39    }() ),
40    DOM = function() {
41        return {
42            html:  doc.documentElement,
43            body:  doc.body,
44            head:  doc.getElementsByTagName('head')[0],
45            title: doc.title
46        };
47    },
48
49    // list of Galleria events
50    _eventlist = 'data ready thumbnail loadstart loadfinish image play pause progress ' +
51              'fullscreen_enter fullscreen_exit idle_enter idle_exit rescale ' +
52              'lightbox_open lightbox_close lightbox_image',
53
54    _events = (function() {
55
56        var evs = [];
57
58        $.each( _eventlist.split(' '), function( i, ev ) {
59            evs.push( ev );
60
61            // legacy events
62            if ( /_/.test( ev ) ) {
63                evs.push( ev.replace( /_/g, '' ) );
64            }
65        });
66
67        return evs;
68
69    }()),
70
71    // legacy options
72    // allows the old my_setting syntax and converts it to camel case
73
74    _legacyOptions = function( options ) {
75
76        var n;
77
78        if ( typeof options !== 'object' ) {
79
80            // return whatever it was...
81            return options;
82        }
83
84        $.each( options, function( key, value ) {
85            if ( /^[a-z]+_/.test( key ) ) {
86                n = '';
87                $.each( key.split('_'), function( i, k ) {
88                    n += i > 0 ? k.substr( 0, 1 ).toUpperCase() + k.substr( 1 ) : k;
89                });
90                options[ n ] = value;
91                delete options[ key ];
92            }
93        });
94
95        return options;
96    },
97
98    _patchEvent = function( type ) {
99
100        // allow 'image' instead of Galleria.IMAGE
101        if ( $.inArray( type, _events ) > -1 ) {
102            return Galleria[ type.toUpperCase() ];
103        }
104
105        return type;
106    },
107
108    // the internal timeouts object
109    // provides helper methods for controlling timeouts
110    _timeouts = {
111
112        trunk: {},
113
114        add: function( id, fn, delay, loop ) {
115            loop = loop || false;
116            this.clear( id );
117            if ( loop ) {
118                var old = fn;
119                fn = function() {
120                    old();
121                    _timeouts.add( id, fn, delay );
122                };
123            }
124            this.trunk[ id ] = window.setTimeout( fn, delay );
125        },
126
127        clear: function( id ) {
128
129            var del = function( i ) {
130                window.clearTimeout( this.trunk[ i ] );
131                delete this.trunk[ i ];
132            }, i;
133
134            if ( !!id && id in this.trunk ) {
135                del.call( _timeouts, id );
136
137            } else if ( typeof id === 'undefined' ) {
138                for ( i in this.trunk ) {
139                    if ( this.trunk.hasOwnProperty( i ) ) {
140                        del.call( _timeouts, i );
141                    }
142                }
143            }
144        }
145    },
146
147    // the internal gallery holder
148    _galleries = [],
149
150    // the internal instance holder
151    _instances = [],
152
153    // flag for errors
154    _hasError = false,
155
156    // canvas holder
157    _canvas = false,
158
159    // instance pool, holds the galleries until themeLoad is triggered
160    _pool = [],
161
162    // themeLoad trigger
163    _themeLoad = function( theme ) {
164        Galleria.theme = theme;
165
166        // run the instances we have in the pool
167        $.each( _pool, function( i, instance ) {
168            if ( !instance._initialized ) {
169                instance._init.call( instance );
170            }
171        });
172    },
173
174    // the Utils singleton
175    Utils = (function() {
176
177        return {
178
179            array : function( obj ) {
180                return Array.prototype.slice.call(obj, 0);
181            },
182
183            create : function( className, nodeName ) {
184                nodeName = nodeName || 'div';
185                var elem = doc.createElement( nodeName );
186                elem.className = className;
187                return elem;
188            },
189
190            getScriptPath : function( src ) {
191
192                // the currently executing script is always the last
193                src = src || $('script:last').attr('src');
194                var slices = src.split('/');
195
196                if (slices.length == 1) {
197                    return '';
198                }
199
200                slices.pop();
201
202                return slices.join('/') + '/';
203            },
204
205            // CSS3 transitions, added in 1.2.4
206            animate : (function() {
207
208                // detect transition
209                var transition = (function( style ) {
210                    var props = 'transition WebkitTransition MozTransition OTransition'.split(' '),
211                        i;
212
213                    // disable css3 animations in opera until stable
214                    if ( window.opera ) {
215                        return false;
216                    }
217
218                    for ( i = 0; props[i]; i++ ) {
219                        if ( typeof style[ props[ i ] ] !== 'undefined' ) {
220                            return props[ i ];
221                        }
222                    }
223                    return false;
224                }(( doc.body || doc.documentElement).style ));
225
226                // map transitionend event
227                var endEvent = {
228                    MozTransition: 'transitionend',
229                    OTransition: 'oTransitionEnd',
230                    WebkitTransition: 'webkitTransitionEnd',
231                    transition: 'transitionend'
232                }[ transition ];
233
234                // map bezier easing conversions
235                var easings = {
236                    _default: [0.25, 0.1, 0.25, 1],
237                    galleria: [0.645, 0.045, 0.355, 1],
238                    galleriaIn: [0.55, 0.085, 0.68, 0.53],
239                    galleriaOut: [0.25, 0.46, 0.45, 0.94],
240                    ease: [0.25, 0, 0.25, 1],
241                    linear: [0.25, 0.25, 0.75, 0.75],
242                    'ease-in': [0.42, 0, 1, 1],
243                    'ease-out': [0, 0, 0.58, 1],
244                    'ease-in-out': [0.42, 0, 0.58, 1]
245                };
246
247                // function for setting transition css for all browsers
248                var setStyle = function( elem, value, suffix ) {
249                    var css = {};
250                    suffix = suffix || 'transition';
251                    $.each( 'webkit moz ms o'.split(' '), function() {
252                        css[ '-' + this + '-' + suffix ] = value;
253                    });
254                    elem.css( css );
255                };
256
257                // clear styles
258                var clearStyle = function( elem ) {
259                    setStyle( elem, 'none', 'transition' );
260                    if ( Galleria.WEBKIT && Galleria.TOUCH ) {
261                        setStyle( elem, 'translate3d(0,0,0)', 'transform' );
262                        if ( elem.data('revert') ) {
263                            elem.css( elem.data('revert') );
264                            elem.data('revert', null);
265                        }
266                    }
267                };
268
269                // various variables
270                var change, strings, easing, syntax, revert, form, css;
271
272                // the actual animation method
273                return function( elem, to, options ) {
274
275                    // extend defaults
276                    options = $.extend({
277                        duration: 400,
278                        complete: function(){},
279                        stop: false
280                    }, options);
281
282                    // cache jQuery instance
283                    elem = $( elem );
284
285                    if ( !options.duration ) {
286                        elem.css( to );
287                        options.complete.call( elem[0] );
288                        return;
289                    }
290
291                    // fallback to jQuery's animate if transition is not supported
292                    if ( !transition ) {
293                        elem.animate(to, options);
294                        return;
295                    }
296
297                    // stop
298                    if ( options.stop ) {
299                        // clear the animation
300                        elem.unbind( endEvent );
301                        clearStyle( elem );
302                    }
303
304                    // see if there is a change
305                    change = false;
306                    $.each( to, function( key, val ) {
307                        css = elem.css( key );
308                        if ( Utils.parseValue( css ) != Utils.parseValue( val ) ) {
309                            change = true;
310                        }
311                        // also add computed styles for FF
312                        elem.css( key, css );
313                    });
314                    if ( !change ) {
315                        window.setTimeout( function() {
316                            options.complete.call( elem[0] );
317                        }, options.duration );
318                        return;
319                    }
320
321                    // the css strings to be applied
322                    strings = [];
323
324                    // the easing bezier
325                    easing = options.easing in easings ? easings[ options.easing ] : easings._default;
326
327                    // the syntax
328                    syntax = ' ' + options.duration + 'ms' + ' cubic-bezier('  + easing.join(',') + ')';
329
330                    // add a tiny timeout so that the browsers catches any css changes before animating
331                    window.setTimeout(function() {
332
333                        // attach the end event
334                        elem.one(endEvent, (function( elem ) {
335                            return function() {
336                                // clear the animation
337                                clearStyle(elem);
338
339                                // run the complete method
340                                options.complete.call(elem[0]);
341                            };
342                        }( elem )));
343
344                        // do the webkit translate3d for better performance on iOS
345                        if( Galleria.WEBKIT && Galleria.TOUCH ) {
346
347                            revert = {};
348                            form = [0,0,0];
349
350                            $.each( ['left', 'top'], function(i, m) {
351                                if ( m in to ) {
352                                    form[ i ] = ( Utils.parseValue( to[ m ] ) - Utils.parseValue(elem.css( m )) ) + 'px';
353                                    revert[ m ] = to[ m ];
354                                    delete to[ m ];
355                                }
356                            });
357
358                            if ( form[0] || form[1]) {
359
360                                elem.data('revert', revert);
361
362                                strings.push('-webkit-transform' + syntax);
363
364                                // 3d animate
365                                setStyle( elem, 'translate3d(' + form.join(',') + ')', 'transform');
366                            }
367                        }
368
369                        // push the animation props
370                        $.each(to, function( p, val ) {
371                            strings.push(p + syntax);
372                        });
373
374                        // set the animation styles
375                        setStyle( elem, strings.join(',') );
376
377                        // animate
378                        elem.css( to );
379
380                    },1 );
381                };
382            }()),
383
384            removeAlpha : function( elem ) {
385                if ( IE < 9 && elem ) {
386
387                    var style = elem.style,
388                        currentStyle = elem.currentStyle,
389                        filter = currentStyle && currentStyle.filter || style.filter || "";
390
391                    if ( /alpha/.test( filter ) ) {
392                        style.filter = filter.replace( /alpha\([^)]*\)/i, '' );
393                    }
394                }
395            },
396
397            forceStyles : function( elem, styles ) {
398                elem = $(elem);
399                if ( elem.attr( 'style' ) ) {
400                    elem.data( 'styles', elem.attr( 'style' ) ).removeAttr( 'style' );
401                }
402                elem.css( styles );
403            },
404
405            revertStyles : function() {
406                $.each( Utils.array( arguments ), function( i, elem ) {
407
408                    elem = $( elem );
409                    elem.removeAttr( 'style' );
410
411                    elem.attr('style',''); // "fixes" webkit bug
412
413                    if ( elem.data( 'styles' ) ) {
414                        elem.attr( 'style', elem.data('styles') ).data( 'styles', null );
415                    }
416                });
417            },
418
419            moveOut : function( elem ) {
420                Utils.forceStyles( elem, {
421                    position: 'absolute',
422                    left: -10000
423                });
424            },
425
426            moveIn : function() {
427                Utils.revertStyles.apply( Utils, Utils.array( arguments ) );
428            },
429
430            elem : function( elem ) {
431                if (elem instanceof $) {
432                    return {
433                        $: elem,
434                        dom: elem[0]
435                    };
436                } else {
437                    return {
438                        $: $(elem),
439                        dom: elem
440                    };
441                }
442            },
443
444            hide : function( elem, speed, callback ) {
445
446                callback = callback || function(){};
447
448                var el = Utils.elem( elem ),
449                    $elem = el.$;
450
451                elem = el.dom;
452
453                // save the value if not exist
454                if (! $elem.data('opacity') ) {
455                    $elem.data('opacity', $elem.css('opacity') );
456                }
457
458                // always hide
459                var style = { opacity: 0 };
460
461                if (speed) {
462
463                    var complete = IE < 9 && elem ? function() {
464                        Utils.removeAlpha( elem );
465                        elem.style.visibility = 'hidden';
466                        callback.call( elem );
467                    } : callback;
468
469                    Utils.animate( elem, style, {
470                        duration: speed,
471                        complete: complete,
472                        stop: true
473                    });
474                } else {
475                    if ( IE < 9 && elem ) {
476                        Utils.removeAlpha( elem );
477                        elem.style.visibility = 'hidden';
478                    } else {
479                        $elem.css( style );
480                    }
481                }
482            },
483
484            show : function( elem, speed, callback ) {
485
486                callback = callback || function(){};
487
488                var el = Utils.elem( elem ),
489                    $elem = el.$;
490
491                elem = el.dom;
492
493                // bring back saved opacity
494                var saved = parseFloat( $elem.data('opacity') ) || 1,
495                    style = { opacity: saved };
496
497                // animate or toggle
498                if (speed) {
499
500                    if ( IE < 9 ) {
501                        $elem.css('opacity', 0);
502                        elem.style.visibility = 'visible';
503                    }
504
505                    var complete = IE < 9 && elem ? function() {
506                        if ( style.opacity == 1 ) {
507                            Utils.removeAlpha( elem );
508                        }
509                        callback.call( elem );
510                    } : callback;
511
512                    Utils.animate( elem, style, {
513                        duration: speed,
514                        complete: complete,
515                        stop: true
516                    });
517                } else {
518                    if ( IE < 9 && style.opacity == 1 && elem ) {
519                        Utils.removeAlpha( elem );
520                        elem.style.visibility = 'visible';
521                    } else {
522                        $elem.css( style );
523                    }
524                }
525            },
526
527
528            // enhanced click for mobile devices
529            // we bind a touchend and hijack any click event in the bubble
530            // then we execute the click directly and save it in a separate data object for later
531            optimizeTouch: (function() {
532
533                var node,
534                    evs,
535                    fakes,
536                    travel,
537                    evt = {},
538                    handler = function( e ) {
539                        e.preventDefault();
540                        evt = $.extend({}, e, true);
541                    },
542                    attach = function() {
543                        this.evt = evt;
544                    },
545                    fake = function() {
546                        this.handler.call(node, this.evt);
547                    };
548
549                return function( elem ) {
550
551                    $(elem).bind('touchend', function( e ) {
552
553                        node = e.target;
554                        travel = true;
555
556                        while( node.parentNode && node != e.currentTarget && travel ) {
557
558                            evs =   $(node).data('events');
559                            fakes = $(node).data('fakes');
560
561                            if (evs && 'click' in evs) {
562
563                                travel = false;
564                                e.preventDefault();
565
566                                // fake the click and save the event object
567                                $(node).click(handler).click();
568
569                                // remove the faked click
570                                evs.click.pop();
571
572                                // attach the faked event
573                                $.each( evs.click, attach);
574
575                                // save the faked clicks in a new data object
576                                $(node).data('fakes', evs.click);
577
578                                // remove all clicks
579                                delete evs.click;
580
581                            } else if ( fakes ) {
582
583                                travel = false;
584                                e.preventDefault();
585
586                                // fake all clicks
587                                $.each( fakes, fake );
588                            }
589
590                            // bubble
591                            node = node.parentNode;
592                        }
593                    });
594                };
595            }()),
596
597            addTimer : function() {
598                _timeouts.add.apply( _timeouts, Utils.array( arguments ) );
599                return this;
600            },
601
602            clearTimer : function() {
603                _timeouts.clear.apply( _timeouts, Utils.array( arguments ) );
604                return this;
605            },
606
607            wait : function(options) {
608                options = $.extend({
609                    until : function() { return false; },
610                    success : function() {},
611                    error : function() { Galleria.raise('Could not complete wait function.'); },
612                    timeout: 3000
613                }, options);
614
615                var start = Utils.timestamp(),
616                    elapsed,
617                    now,
618                    fn = function() {
619                        now = Utils.timestamp();
620                        elapsed = now - start;
621                        if ( options.until( elapsed ) ) {
622                            options.success();
623                            return false;
624                        }
625
626                        if (now >= start + options.timeout) {
627                            options.error();
628                            return false;
629                        }
630                        window.setTimeout(fn, 10);
631                    };
632
633                window.setTimeout(fn, 10);
634            },
635
636            toggleQuality : function( img, force ) {
637
638                if ( ( IE !== 7 && IE !== 8 ) || !img ) {
639                    return;
640                }
641
642                if ( typeof force === 'undefined' ) {
643                    force = img.style.msInterpolationMode === 'nearest-neighbor';
644                }
645
646                img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
647            },
648
649            insertStyleTag : function( styles ) {
650                var style = doc.createElement( 'style' );
651                DOM().head.appendChild( style );
652
653                if ( style.styleSheet ) { // IE
654                    style.styleSheet.cssText = styles;
655                } else {
656                    var cssText = doc.createTextNode( styles );
657                    style.appendChild( cssText );
658                }
659            },
660
661            // a loadscript method that works for local scripts
662            loadScript: function( url, callback ) {
663
664                var done = false,
665                    script = $('<scr'+'ipt>').attr({
666                        src: url,
667                        async: true
668                    }).get(0);
669
670               // Attach handlers for all browsers
671               script.onload = script.onreadystatechange = function() {
672                   if ( !done && (!this.readyState ||
673                       this.readyState === 'loaded' || this.readyState === 'complete') ) {
674
675                       done = true;
676
677                       // Handle memory leak in IE
678                       script.onload = script.onreadystatechange = null;
679
680                       if (typeof callback === 'function') {
681                           callback.call( this, this );
682                       }
683                   }
684               };
685
686               DOM().head.appendChild( script );
687            },
688
689            // parse anything into a number
690            parseValue: function( val ) {
691                if (typeof val === 'number') {
692                    return val;
693                } else if (typeof val === 'string') {
694                    var arr = val.match(/\-?\d|\./g);
695                    return arr && arr.constructor === Array ? arr.join('')*1 : 0;
696                } else {
697                    return 0;
698                }
699            },
700
701            // timestamp abstraction
702            timestamp: function() {
703                return new Date().getTime();
704            },
705
706            // this is pretty crap, but works for now
707            // it will add a callback, but it can't guarantee that the styles can be fetched
708            // using getComputedStyle further checking needed, possibly a dummy element
709            loadCSS : function( href, id, callback ) {
710
711                var link,
712                    ready = false,
713                    length;
714
715                // look for manual css
716                $('link[rel=stylesheet]').each(function() {
717                    if ( new RegExp( href ).test( this.href ) ) {
718                        link = this;
719                        return false;
720                    }
721                });
722
723                if ( typeof id === 'function' ) {
724                    callback = id;
725                    id = undef;
726                }
727
728                callback = callback || function() {}; // dirty
729
730                // if already present, return
731                if ( link ) {
732                    callback.call( link, link );
733                    return link;
734                }
735
736                // save the length of stylesheets to check against
737                length = doc.styleSheets.length;
738
739                // check for existing id
740                if( $('#'+id).length ) {
741                    $('#'+id).attr('href', href);
742                    length--;
743                    ready = true;
744                } else {
745                    link = $( '<link>' ).attr({
746                        rel: 'stylesheet',
747                        href: href,
748                        id: id
749                    }).get(0);
750
751                    window.setTimeout(function() {
752                        var styles = $('link[rel="stylesheet"], style');
753                        if ( styles.length ) {
754                            styles.get(0).parentNode.insertBefore( link, styles[0] );
755                        } else {
756                            DOM().head.appendChild( link );
757                        }
758
759                        if ( IE ) {
760
761                            // IE has a limit of 31 stylesheets in one document
762                            if( length >= 31 ) {
763                                Galleria.raise( 'You have reached the browser stylesheet limit (31)', true );
764                                return;
765                            }
766
767                            // todo: test if IE really needs the readyState
768                            link.onreadystatechange = function(e) {
769                                if ( !ready && (!this.readyState ||
770                                    this.readyState === 'loaded' || this.readyState === 'complete') ) {
771                                    ready = true;
772                                }
773                            };
774                        } else {
775                            // final test via ajax if not local
776                            if ( !( new RegExp('file://','i').test( href ) ) ) {
777                                $.ajax({
778                                    url: href,
779                                    success: function() {
780                                        ready = true;
781                                    },
782                                    error: function(e) {
783                                        // pass if origin is rejected in chrome for some reason
784                                        if( e.isRejected() && Galleria.WEBKIT ) {
785                                            ready = true;
786                                        }
787                                    }
788                                });
789                            } else {
790                                ready = true;
791                            }
792                        }
793                    }, 10);
794                }
795
796                if ( typeof callback === 'function' ) {
797
798                    Utils.wait({
799                        until: function() {
800                            return ready && doc.styleSheets.length > length;
801                        },
802                        success: function() {
803                            window.setTimeout( function() {
804                                callback.call( link, link );
805                            }, 100);
806                        },
807                        error: function() {
808                            Galleria.raise( 'Theme CSS could not load', true );
809                        },
810                        timeout: 10000
811                    });
812                }
813                return link;
814            }
815        };
816    }()),
817
818    // the transitions holder
819    _transitions = (function() {
820
821        var _slide = function(params, complete, fade, door) {
822
823            var easing = this.getOptions('easing'),
824                distance = this.getStageWidth(),
825                from = { left: distance * ( params.rewind ? -1 : 1 ) },
826                to = { left: 0 };
827
828            if ( fade ) {
829                from.opacity = 0;
830                to.opacity = 1;
831            } else {
832                from.opacity = 1;
833            }
834
835            $(params.next).css(from);
836
837            Utils.animate(params.next, to, {
838                duration: params.speed,
839                complete: (function( elems ) {
840                    return function() {
841                        complete();
842                        elems.css({
843                            left: 0
844                        });
845                    };
846                }( $( params.next ).add( params.prev ) )),
847                queue: false,
848                easing: easing
849            });
850
851            if (door) {
852                params.rewind = !params.rewind;
853            }
854
855            if (params.prev) {
856
857                from = { left: 0 };
858                to = { left: distance * ( params.rewind ? 1 : -1 ) };
859
860                if ( fade ) {
861                    from.opacity = 1;
862                    to.opacity = 0;
863                }
864
865                $(params.prev).css(from);
866                Utils.animate(params.prev, to, {
867                    duration: params.speed,
868                    queue: false,
869                    easing: easing,
870                    complete: function() {
871                        $(this).css('opacity', 0);
872                    }
873                });
874            }
875        };
876
877        return {
878
879            fade: function(params, complete) {
880                $(params.next).css({
881                    opacity: 0,
882                    left: 0
883                }).show();
884                Utils.animate(params.next, {
885                    opacity: 1
886                },{
887                    duration: params.speed,
888                    complete: complete
889                });
890                if (params.prev) {
891                    $(params.prev).css('opacity',1).show();
892                    Utils.animate(params.prev, {
893                        opacity: 0
894                    },{
895                        duration: params.speed
896                    });
897                }
898            },
899
900            flash: function(params, complete) {
901                $(params.next).css({
902                    opacity: 0,
903                    left: 0
904                });
905                if (params.prev) {
906                    Utils.animate( params.prev, {
907                        opacity: 0
908                    },{
909                        duration: params.speed/2,
910                        complete: function() {
911                            Utils.animate( params.next, {
912                                opacity:1
913                            },{
914                                duration: params.speed,
915                                complete: complete
916                            });
917                        }
918                    });
919                } else {
920                    Utils.animate( params.next, {
921                        opacity: 1
922                    },{
923                        duration: params.speed,
924                        complete: complete
925                    });
926                }
927            },
928
929            pulse: function(params, complete) {
930                if (params.prev) {
931                    $(params.prev).hide();
932                }
933                $(params.next).css({
934                    opacity: 0,
935                    left: 0
936                }).show();
937                Utils.animate(params.next, {
938                    opacity:1
939                },{
940                    duration: params.speed,
941                    complete: complete
942                });
943            },
944
945            slide: function(params, complete) {
946                _slide.apply( this, Utils.array( arguments ) );
947            },
948
949            fadeslide: function(params, complete) {
950                _slide.apply( this, Utils.array( arguments ).concat( [true] ) );
951            },
952
953            doorslide: function(params, complete) {
954                _slide.apply( this, Utils.array( arguments ).concat( [false, true] ) );
955            }
956        };
957    }());
958
959/**
960    The main Galleria class
961
962    @class
963    @constructor
964
965    @example var gallery = new Galleria();
966
967    @author http://aino.se
968
969    @requires jQuery
970
971*/
972
973Galleria = function() {
974
975    var self = this;
976
977    // the theme used
978    this._theme = undef;
979
980    // internal options
981    this._options = {};
982
983    // flag for controlling play/pause
984    this._playing = false;
985
986    // internal interval for slideshow
987    this._playtime = 5000;
988
989    // internal variable for the currently active image
990    this._active = null;
991
992    // the internal queue, arrayified
993    this._queue = { length: 0 };
994
995    // the internal data array
996    this._data = [];
997
998    // the internal dom collection
999    this._dom = {};
1000
1001    // the internal thumbnails array
1002    this._thumbnails = [];
1003
1004    // the internal layers array
1005    this._layers = [];
1006
1007    // internal init flag
1008    this._initialized = false;
1009
1010    // internal firstrun flag
1011    this._firstrun = false;
1012
1013    // global stagewidth/height
1014    this._stageWidth = 0;
1015    this._stageHeight = 0;
1016
1017    // target holder
1018    this._target = undef;
1019
1020    // instance id
1021    this._id = Math.random();
1022
1023    // add some elements
1024    var divs =  'container stage images image-nav image-nav-left image-nav-right ' +
1025                'info info-text info-title info-description ' +
1026                'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
1027                'loader counter tooltip',
1028        spans = 'current total';
1029
1030    $.each( divs.split(' '), function( i, elemId ) {
1031        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId );
1032    });
1033
1034    $.each( spans.split(' '), function( i, elemId ) {
1035        self._dom[ elemId ] = Utils.create( 'galleria-' + elemId, 'span' );
1036    });
1037
1038    // the internal keyboard object
1039    // keeps reference of the keybinds and provides helper methods for binding keys
1040    var keyboard = this._keyboard = {
1041
1042        keys : {
1043            'UP': 38,
1044            'DOWN': 40,
1045            'LEFT': 37,
1046            'RIGHT': 39,
1047            'RETURN': 13,
1048            'ESCAPE': 27,
1049            'BACKSPACE': 8,
1050            'SPACE': 32
1051        },
1052
1053        map : {},
1054
1055        bound: false,
1056
1057        press: function(e) {
1058            var key = e.keyCode || e.which;
1059            if ( key in keyboard.map && typeof keyboard.map[key] === 'function' ) {
1060                keyboard.map[key].call(self, e);
1061            }
1062        },
1063
1064        attach: function(map) {
1065
1066            var key, up;
1067
1068            for( key in map ) {
1069                if ( map.hasOwnProperty( key ) ) {
1070                    up = key.toUpperCase();
1071                    if ( up in keyboard.keys ) {
1072                        keyboard.map[ keyboard.keys[up] ] = map[key];
1073                    } else {
1074                        keyboard.map[ up ] = map[key];
1075                    }
1076                }
1077            }
1078            if ( !keyboard.bound ) {
1079                keyboard.bound = true;
1080                $doc.bind('keydown', keyboard.press);
1081            }
1082        },
1083
1084        detach: function() {
1085            keyboard.bound = false;
1086            keyboard.map = {};
1087            $doc.unbind('keydown', keyboard.press);
1088        }
1089    };
1090
1091    // internal controls for keeping track of active / inactive images
1092    var controls = this._controls = {
1093
1094        0: undef,
1095
1096        1: undef,
1097
1098        active : 0,
1099
1100        swap : function() {
1101            controls.active = controls.active ? 0 : 1;
1102        },
1103
1104        getActive : function() {
1105            return controls[ controls.active ];
1106        },
1107
1108        getNext : function() {
1109            return controls[ 1 - controls.active ];
1110        }
1111    };
1112
1113    // internal carousel object
1114    var carousel = this._carousel = {
1115
1116        // shortcuts
1117        next: self.$('thumb-nav-right'),
1118        prev: self.$('thumb-nav-left'),
1119
1120        // cache the width
1121        width: 0,
1122
1123        // track the current position
1124        current: 0,
1125
1126        // cache max value
1127        max: 0,
1128
1129        // save all hooks for each width in an array
1130        hooks: [],
1131
1132        // update the carousel
1133        // you can run this method anytime, f.ex on window.resize
1134        update: function() {
1135            var w = 0,
1136                h = 0,
1137                hooks = [0];
1138
1139            $.each( self._thumbnails, function( i, thumb ) {
1140                if ( thumb.ready ) {
1141                    w += thumb.outerWidth || $( thumb.container ).outerWidth( true );
1142                    hooks[ i+1 ] = w;
1143                    h = Math.max( h, thumb.outerHeight || $( thumb.container).outerHeight( true ) );
1144                }
1145            });
1146
1147            self.$( 'thumbnails' ).css({
1148                width: w,
1149                height: h
1150            });
1151
1152            carousel.max = w;
1153            carousel.hooks = hooks;
1154            carousel.width = self.$( 'thumbnails-list' ).width();
1155            carousel.setClasses();
1156
1157            self.$( 'thumbnails-container' ).toggleClass( 'galleria-carousel', w > carousel.width );
1158
1159            // one extra calculation
1160            carousel.width = self.$( 'thumbnails-list' ).width();
1161
1162            // todo: fix so the carousel moves to the left
1163        },
1164
1165        bindControls: function() {
1166
1167            var i;
1168
1169            carousel.next.bind( 'click', function(e) {
1170                e.preventDefault();
1171
1172                if ( self._options.carouselSteps === 'auto' ) {
1173
1174                    for ( i = carousel.current; i < carousel.hooks.length; i++ ) {
1175                        if ( carousel.hooks[i] - carousel.hooks[ carousel.current ] > carousel.width ) {
1176                            carousel.set(i - 2);
1177                            break;
1178                        }
1179                    }
1180
1181                } else {
1182                    carousel.set( carousel.current + self._options.carouselSteps);
1183                }
1184            });
1185
1186            carousel.prev.bind( 'click', function(e) {
1187                e.preventDefault();
1188
1189                if ( self._options.carouselSteps === 'auto' ) {
1190
1191                    for ( i = carousel.current; i >= 0; i-- ) {
1192                        if ( carousel.hooks[ carousel.current ] - carousel.hooks[i] > carousel.width ) {
1193                            carousel.set( i + 2 );
1194                            break;
1195                        } else if ( i === 0 ) {
1196                            carousel.set( 0 );
1197                            break;
1198                        }
1199                    }
1200                } else {
1201                    carousel.set( carousel.current - self._options.carouselSteps );
1202                }
1203            });
1204        },
1205
1206        // calculate and set positions
1207        set: function( i ) {
1208            i = Math.max( i, 0 );
1209            while ( carousel.hooks[i - 1] + carousel.width >= carousel.max && i >= 0 ) {
1210                i--;
1211            }
1212            carousel.current = i;
1213            carousel.animate();
1214        },
1215
1216        // get the last position
1217        getLast: function(i) {
1218            return ( i || carousel.current ) - 1;
1219        },
1220
1221        // follow the active image
1222        follow: function(i) {
1223
1224            //don't follow if position fits
1225            if ( i === 0 || i === carousel.hooks.length - 2 ) {
1226                carousel.set( i );
1227                return;
1228            }
1229
1230            // calculate last position
1231            var last = carousel.current;
1232            while( carousel.hooks[last] - carousel.hooks[ carousel.current ] <
1233                   carousel.width && last <= carousel.hooks.length ) {
1234                last ++;
1235            }
1236
1237            // set position
1238            if ( i - 1 < carousel.current ) {
1239                carousel.set( i - 1 );
1240            } else if ( i + 2 > last) {
1241                carousel.set( i - last + carousel.current + 2 );
1242            }
1243        },
1244
1245        // helper for setting disabled classes
1246        setClasses: function() {
1247            carousel.prev.toggleClass( 'disabled', !carousel.current );
1248            carousel.next.toggleClass( 'disabled', carousel.hooks[ carousel.current ] + carousel.width >= carousel.max );
1249        },
1250
1251        // the animation method
1252        animate: function(to) {
1253            carousel.setClasses();
1254            var num = carousel.hooks[ carousel.current ] * -1;
1255
1256            if ( isNaN( num ) ) {
1257                return;
1258            }
1259
1260            Utils.animate(self.get( 'thumbnails' ), {
1261                left: num
1262            },{
1263                duration: self._options.carouselSpeed,
1264                easing: self._options.easing,
1265                queue: false
1266            });
1267        }
1268    };
1269
1270    // tooltip control
1271    // added in 1.2
1272    var tooltip = this._tooltip = {
1273
1274        initialized : false,
1275
1276        open: false,
1277
1278        init: function() {
1279
1280            tooltip.initialized = true;
1281
1282            var css = '.galleria-tooltip{padding:3px 8px;max-width:50%;background:#ffe;color:#000;z-index:3;position:absolute;font-size:11px;line-height:1.3' +
1283                      '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);}';
1284
1285            Utils.insertStyleTag(css);
1286
1287            self.$( 'tooltip' ).css('opacity', 0.8);
1288            Utils.hide( self.get('tooltip') );
1289
1290        },
1291
1292        // move handler
1293        move: function( e ) {
1294            var mouseX = self.getMousePosition(e).x,
1295                mouseY = self.getMousePosition(e).y,
1296                $elem = self.$( 'tooltip' ),
1297                x = mouseX,
1298                y = mouseY,
1299                height = $elem.outerHeight( true ) + 1,
1300                width = $elem.outerWidth( true ),
1301                limitY = height + 15;
1302
1303            var maxX = self.$( 'container').width() - width - 2,
1304                maxY = self.$( 'container').height() - height - 2;
1305
1306            if ( !isNaN(x) && !isNaN(y) ) {
1307
1308                x += 10;
1309                y -= 30;
1310
1311                x = Math.max( 0, Math.min( maxX, x ) );
1312                y = Math.max( 0, Math.min( maxY, y ) );
1313
1314                if( mouseY < limitY ) {
1315                    y = limitY;
1316                }
1317
1318                $elem.css({ left: x, top: y });
1319            }
1320        },
1321
1322        // bind elements to the tooltip
1323        // you can bind multiple elementIDs using { elemID : function } or { elemID : string }
1324        // you can also bind single DOM elements using bind(elem, string)
1325        bind: function( elem, value ) {
1326
1327            // todo: revise if alternative tooltip is needed for mobile devices
1328            if (Galleria.TOUCH) {
1329                return;
1330            }
1331
1332            if (! tooltip.initialized ) {
1333                tooltip.init();
1334            }
1335
1336            var hover = function( elem, value) {
1337
1338                tooltip.define( elem, value );
1339
1340                $( elem ).hover(function() {
1341
1342                    Utils.clearTimer('switch_tooltip');
1343                    self.$('container').unbind( 'mousemove', tooltip.move ).bind( 'mousemove', tooltip.move ).trigger( 'mousemove' );
1344                    tooltip.show( elem );
1345
1346                    Galleria.utils.addTimer( 'tooltip', function() {
1347                        self.$( 'tooltip' ).stop().show().animate({
1348                            opacity:1
1349                        });
1350                        tooltip.open = true;
1351
1352                    }, tooltip.open ? 0 : 500);
1353
1354                }, function() {
1355
1356                    self.$( 'container' ).unbind( 'mousemove', tooltip.move );
1357                    Utils.clearTimer( 'tooltip' );
1358
1359                    self.$( 'tooltip' ).stop().animate({
1360                        opacity: 0
1361                    }, 200, function() {
1362
1363                        self.$( 'tooltip' ).hide();
1364
1365                        Utils.addTimer('switch_tooltip', function() {
1366                            tooltip.open = false;
1367                        }, 1000);
1368                    });
1369                });
1370            };
1371
1372            if ( typeof value === 'string' ) {
1373                hover( ( elem in self._dom ? self.get( elem ) : elem ), value );
1374            } else {
1375                // asume elemID here
1376                $.each( elem, function( elemID, val ) {
1377                    hover( self.get(elemID), val );
1378                });
1379            }
1380        },
1381
1382        show: function( elem ) {
1383
1384            elem = $( elem in self._dom ? self.get(elem) : elem );
1385
1386            var text = elem.data( 'tt' ),
1387                mouseup = function( e ) {
1388
1389                    // attach a tiny settimeout to make sure the new tooltip is filled
1390                    window.setTimeout( (function( ev ) {
1391                        return function() {
1392                            tooltip.move( ev );
1393                        };
1394                    }( e )), 10);
1395
1396                    elem.unbind( 'mouseup', mouseup );
1397
1398                };
1399
1400            text = typeof text === 'function' ? text() : text;
1401
1402            if ( ! text ) {
1403                return;
1404            }
1405
1406            self.$( 'tooltip' ).html( text.replace(/\s/, '&nbsp;') );
1407
1408            // trigger mousemove on mouseup in case of click
1409            elem.bind( 'mouseup', mouseup );
1410        },
1411
1412        define: function( elem, value ) {
1413
1414            // we store functions, not strings
1415            if (typeof value !== 'function') {
1416                var s = value;
1417                value = function() {
1418                    return s;
1419                };
1420            }
1421
1422            elem = $( elem in self._dom ? self.get(elem) : elem ).data('tt', value);
1423
1424            tooltip.show( elem );
1425
1426        }
1427    };
1428
1429    // internal fullscreen control
1430    var fullscreen = this._fullscreen = {
1431
1432        scrolled: 0,
1433
1434        crop: undef,
1435
1436        transition: undef,
1437
1438        active: false,
1439
1440        keymap: self._keyboard.map,
1441
1442        enter: function(callback) {
1443
1444            fullscreen.active = true;
1445
1446            // hide the image until rescale is complete
1447            Utils.hide( self.getActiveImage() );
1448
1449            self.$( 'container' ).addClass( 'fullscreen' );
1450
1451            fullscreen.scrolled = $win.scrollTop();
1452
1453            // begin styleforce
1454            Utils.forceStyles(self.get('container'), {
1455                position: 'fixed',
1456                top: 0,
1457                left: 0,
1458                width: '100%',
1459                height: '100%',
1460                zIndex: 10000
1461            });
1462
1463            var htmlbody = {
1464                    height: '100%',
1465                    overflow: 'hidden',
1466                    margin:0,
1467                    padding:0
1468                },
1469
1470                data = self.getData(),
1471
1472                options = self._options;
1473
1474            Utils.forceStyles( DOM().html, htmlbody );
1475            Utils.forceStyles( DOM().body, htmlbody );
1476
1477            // temporarily attach some keys
1478            // save the old ones first in a cloned object
1479            fullscreen.keymap = $.extend({}, self._keyboard.map);
1480
1481            self.attachKeyboard({
1482                escape: self.exitFullscreen,
1483                right: self.next,
1484                left: self.prev
1485            });
1486
1487            // temporarily save the crop
1488            fullscreen.crop = options.imageCrop;
1489
1490            // set fullscreen options
1491            if ( options.fullscreenCrop != undef ) {
1492                options.imageCrop = options.fullscreenCrop;
1493            }
1494
1495            // swap to big image if it's different from the display image
1496            if ( data && data.big && data.image !== data.big ) {
1497                var big    = new Galleria.Picture(),
1498                    cached = big.isCached( data.big ),
1499                    index  = self.getIndex(),
1500                    thumb  = self._thumbnails[ index ];
1501
1502                self.trigger( {
1503                    type: Galleria.LOADSTART,
1504                    cached: cached,
1505                    rewind: false,
1506                    index: index,
1507                    imageTarget: self.getActiveImage(),
1508                    thumbTarget: thumb
1509                });
1510
1511                big.load( data.big, function( big ) {
1512                    self._scaleImage( big, {
1513                        complete: function( big ) {
1514                            self.trigger({
1515                                type: Galleria.LOADFINISH,
1516                                cached: cached,
1517                                index: index,
1518                                rewind: false,
1519                                imageTarget: big.image,
1520                                thumbTarget: thumb
1521                            });
1522                            var image = self._controls.getActive().image;
1523                            if ( image ) {
1524                                $( image ).width( big.image.width ).height( big.image.height )
1525                                    .attr( 'style', $( big.image ).attr('style') )
1526                                    .attr( 'src', big.image.src );
1527                            }
1528                        }
1529                    });
1530                });
1531            }
1532
1533            // init the first rescale and attach callbacks
1534            self.rescale(function() {
1535
1536                Utils.addTimer('fullscreen_enter', function() {
1537                    // show the image after 50 ms
1538                    Utils.show( self.getActiveImage() );
1539
1540                    if (typeof callback === 'function') {
1541                        callback.call( self );
1542                    }
1543
1544                }, 100);
1545
1546                self.trigger( Galleria.FULLSCREEN_ENTER );
1547            });
1548
1549            // bind the scaling to the resize event
1550            $win.resize( function() {
1551                fullscreen.scale();
1552            } );
1553        },
1554
1555        scale : function() {
1556            self.rescale();
1557        },
1558
1559        exit: function(callback) {
1560
1561            fullscreen.active = false;
1562
1563            Utils.hide( self.getActiveImage() );
1564
1565            self.$('container').removeClass( 'fullscreen' );
1566
1567            // revert all styles
1568            Utils.revertStyles( self.get('container'), DOM().html, DOM().body );
1569
1570            // scroll back
1571            window.scrollTo(0, fullscreen.scrolled);
1572
1573            // detach all keyboard events and apply the old keymap
1574            self.detachKeyboard();
1575            self.attachKeyboard( fullscreen.keymap );
1576
1577            // bring back cached options
1578            self._options.imageCrop = fullscreen.crop;
1579            //self._options.transition = fullscreen.transition;
1580
1581            // return to original image
1582            var big = self.getData().big,
1583                image = self._controls.getActive().image;
1584
1585            if ( big && big == image.src ) {
1586
1587                window.setTimeout(function(src) {
1588                    return function() {
1589                        image.src = src;
1590                    };
1591                }( self.getData().image ), 1 );
1592
1593            }
1594
1595            self.rescale(function() {
1596                Utils.addTimer('fullscreen_exit', function() {
1597
1598                    // show the image after 50 ms
1599                    Utils.show( self.getActiveImage() );
1600
1601                    if ( typeof callback === 'function' ) {
1602                        callback.call( self );
1603                    }
1604
1605                }, 50);
1606
1607                self.trigger( Galleria.FULLSCREEN_EXIT );
1608            });
1609
1610
1611            $win.unbind('resize', fullscreen.scale);
1612        }
1613    };
1614
1615    // the internal idle object for controlling idle states
1616    var idle = this._idle = {
1617
1618        trunk: [],
1619
1620        bound: false,
1621
1622        add: function(elem, to) {
1623            if (!elem) {
1624                return;
1625            }
1626            if (!idle.bound) {
1627                idle.addEvent();
1628            }
1629            elem = $(elem);
1630
1631            var from = {},
1632                style;
1633
1634            for ( style in to ) {
1635                if ( to.hasOwnProperty( style ) ) {
1636                    from[ style ] = elem.css( style );
1637                }
1638            }
1639            elem.data('idle', {
1640                from: from,
1641                to: to,
1642                complete: true,
1643                busy: false
1644            });
1645            idle.addTimer();
1646            idle.trunk.push(elem);
1647        },
1648
1649        remove: function(elem) {
1650
1651            elem = jQuery(elem);
1652
1653            $.each(idle.trunk, function(i, el) {
1654                if ( el.length && !el.not(elem).length ) {
1655                    self._idle.show(elem);
1656                    self._idle.trunk.splice(i, 1);
1657                }
1658            });
1659
1660            if (!idle.trunk.length) {
1661                idle.removeEvent();
1662                Utils.clearTimer('idle');
1663            }
1664        },
1665
1666        addEvent : function() {
1667            idle.bound = true;
1668            self.$('container').bind('mousemove click', idle.showAll );
1669        },
1670
1671        removeEvent : function() {
1672            idle.bound = false;
1673            self.$('container').unbind('mousemove click', idle.showAll );
1674        },
1675
1676        addTimer : function() {
1677            Utils.addTimer('idle', function() {
1678                self._idle.hide();
1679            }, self._options.idleTime );
1680        },
1681
1682        hide : function() {
1683
1684            if ( !self._options.idleMode ) {
1685                return;
1686            }
1687
1688            self.trigger( Galleria.IDLE_ENTER );
1689
1690            $.each( idle.trunk, function(i, elem) {
1691
1692                var data = elem.data('idle');
1693
1694                if (! data) {
1695                    return;
1696                }
1697
1698                elem.data('idle').complete = false;
1699
1700                Utils.animate( elem, data.to, {
1701                    duration: self._options.idleSpeed
1702                });
1703            });
1704        },
1705
1706        showAll : function() {
1707
1708            Utils.clearTimer('idle');
1709
1710            $.each(self._idle.trunk, function( i, elem ) {
1711                self._idle.show( elem );
1712            });
1713        },
1714
1715        show: function(elem) {
1716
1717            var data = elem.data('idle');
1718
1719            if (!data.busy && !data.complete) {
1720
1721                data.busy = true;
1722
1723                self.trigger( Galleria.IDLE_EXIT );
1724
1725                Utils.clearTimer( 'idle' );
1726
1727                Utils.animate( elem, data.from, {
1728                    duration: self._options.idleSpeed/2,
1729                    complete: function() {
1730                        $(this).data('idle').busy = false;
1731                        $(this).data('idle').complete = true;
1732                    }
1733                });
1734
1735            }
1736            idle.addTimer();
1737        }
1738    };
1739
1740    // internal lightbox object
1741    // creates a predesigned lightbox for simple popups of images in galleria
1742    var lightbox = this._lightbox = {
1743
1744        width : 0,
1745
1746        height : 0,
1747
1748        initialized : false,
1749
1750        active : null,
1751
1752        image : null,
1753
1754        elems : {},
1755
1756        keymap: false,
1757
1758        init : function() {
1759
1760            // trigger the event
1761            self.trigger( Galleria.LIGHTBOX_OPEN );
1762
1763            if ( lightbox.initialized ) {
1764                return;
1765            }
1766            lightbox.initialized = true;
1767
1768            // create some elements to work with
1769            var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
1770                el = {},
1771                op = self._options,
1772                css = '',
1773                abs = 'position:absolute;',
1774                prefix = 'lightbox-',
1775                cssMap = {
1776                    overlay:    'position:fixed;display:none;opacity:'+op.overlayOpacity+';filter:alpha(opacity='+(op.overlayOpacity*100)+
1777                                ');top:0;left:0;width:100%;height:100%;background:'+op.overlayBackground+';z-index:99990',
1778                    box:        'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
1779                    shadow:     abs+'background:#000;width:100%;height:100%;',
1780                    content:    abs+'background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
1781                    info:       abs+'bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
1782                    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',
1783                    image:      abs+'top:10px;left:10px;right:10px;bottom:30px;overflow:hidden;display:block;',
1784                    prevholder: abs+'width:50%;top:0;bottom:40px;cursor:pointer;',
1785                    nextholder: abs+'width:50%;top:0;bottom:40px;right:-1px;cursor:pointer;',
1786                    prev:       abs+'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;text-align:center;color:#000;font:bold 16px/36px arial,sans-serif',
1787                    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',
1788                    title:      'float:left',
1789                    counter:    'float:right;margin-left:8px;'
1790                },
1791                hover = function(elem) {
1792                    return elem.hover(
1793                        function() { $(this).css( 'color', '#bbb' ); },
1794                        function() { $(this).css( 'color', '#444' ); }
1795                    );
1796                },
1797                appends = {};
1798
1799            // IE8 fix for IE's transparent background event "feature"
1800            if ( IE === 8 ) {
1801                cssMap.nextholder += 'background:#000;filter:alpha(opacity=0);';
1802                cssMap.prevholder += 'background:#000;filter:alpha(opacity=0);';
1803            }
1804
1805            // create and insert CSS
1806            $.each(cssMap, function( key, value ) {
1807                css += '.galleria-'+prefix+key+'{'+value+'}';
1808            });
1809
1810            Utils.insertStyleTag( css );
1811
1812            // create the elements
1813            $.each(elems.split(' '), function( i, elemId ) {
1814                self.addElement( 'lightbox-' + elemId );
1815                el[ elemId ] = lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
1816            });
1817
1818            // initiate the image
1819            lightbox.image = new Galleria.Picture();
1820
1821            // append the elements
1822            $.each({
1823                    box: 'shadow content close prevholder nextholder',
1824                    info: 'title counter',
1825                    content: 'info image',
1826                    prevholder: 'prev',
1827                    nextholder: 'next'
1828                }, function( key, val ) {
1829                    var arr = [];
1830                    $.each( val.split(' '), function( i, prop ) {
1831                        arr.push( prefix + prop );
1832                    });
1833                    appends[ prefix+key ] = arr;
1834            });
1835
1836            self.append( appends );
1837
1838            $( el.image ).append( lightbox.image.container );
1839
1840            $( DOM().body ).append( el.overlay, el.box );
1841
1842            Utils.optimizeTouch( el.box );
1843
1844            // add the prev/next nav and bind some controls
1845
1846            hover( $( el.close ).bind( 'click', lightbox.hide ).html('&#215;') );
1847
1848            $.each( ['Prev','Next'], function(i, dir) {
1849
1850                var $d = $( el[ dir.toLowerCase() ] ).html( /v/.test( dir ) ? '&#8249;&nbsp;' : '&nbsp;&#8250;' ),
1851                    $e = $( el[ dir.toLowerCase()+'holder'] );
1852
1853                $e.bind( 'click', function() {
1854                    lightbox[ 'show' + dir ]();
1855                });
1856
1857                // IE7 and touch devices will simply show the nav
1858                if ( IE < 8 || Galleria.TOUCH ) {
1859                    $d.show();
1860                    return;
1861                }
1862
1863                $e.hover( function() {
1864                    $d.show();
1865                }, function(e) {
1866                    $d.stop().fadeOut( 200 );
1867                });
1868
1869            });
1870            $( el.overlay ).bind( 'click', lightbox.hide );
1871
1872            // the lightbox animation is slow on ipad
1873            if ( Galleria.IPAD ) {
1874                self._options.lightboxTransitionSpeed = 0;
1875            }
1876
1877        },
1878
1879        rescale: function(event) {
1880
1881            // calculate
1882             var width = Math.min( $win.width()-40, lightbox.width ),
1883                height = Math.min( $win.height()-60, lightbox.height ),
1884                ratio = Math.min( width / lightbox.width, height / lightbox.height ),
1885                destWidth = Math.round( lightbox.width * ratio ) + 40,
1886                destHeight = Math.round( lightbox.height * ratio ) + 60,
1887                to = {
1888                    width: destWidth,
1889                    height: destHeight,
1890                    'margin-top': Math.ceil( destHeight / 2 ) *- 1,
1891                    'margin-left': Math.ceil( destWidth / 2 ) *- 1
1892                };
1893
1894            // if rescale event, don't animate
1895            if ( event ) {
1896                $( lightbox.elems.box ).css( to );
1897            } else {
1898                $( lightbox.elems.box ).animate( to, {
1899                    duration: self._options.lightboxTransitionSpeed,
1900                    easing: self._options.easing,
1901                    complete: function() {
1902                        var image = lightbox.image,
1903                            speed = self._options.lightboxFadeSpeed;
1904
1905                        self.trigger({
1906                            type: Galleria.LIGHTBOX_IMAGE,
1907                            imageTarget: image.image
1908                        });
1909
1910                        $( image.container ).show();
1911
1912                        Utils.show( image.image, speed );
1913                        Utils.show( lightbox.elems.info, speed );
1914                    }
1915                });
1916            }
1917        },
1918
1919        hide: function() {
1920
1921            // remove the image
1922            lightbox.image.image = null;
1923
1924            $win.unbind('resize', lightbox.rescale);
1925
1926            $( lightbox.elems.box ).hide();
1927
1928            Utils.hide( lightbox.elems.info );
1929
1930            self.detachKeyboard();
1931            self.attachKeyboard( lightbox.keymap );
1932
1933            lightbox.keymap = false;
1934
1935            Utils.hide( lightbox.elems.overlay, 200, function() {
1936                $( this ).hide().css( 'opacity', self._options.overlayOpacity );
1937                self.trigger( Galleria.LIGHTBOX_CLOSE );
1938            });
1939        },
1940
1941        showNext: function() {
1942            lightbox.show( self.getNext( lightbox.active ) );
1943        },
1944
1945        showPrev: function() {
1946            lightbox.show( self.getPrev( lightbox.active ) );
1947        },
1948
1949        show: function(index) {
1950
1951            lightbox.active = index = typeof index === 'number' ? index : self.getIndex();
1952
1953            if ( !lightbox.initialized ) {
1954                lightbox.init();
1955            }
1956
1957            // temporarily attach some keys
1958            // save the old ones first in a cloned object
1959            if ( !lightbox.keymap ) {
1960
1961                lightbox.keymap = $.extend({}, self._keyboard.map);
1962
1963                self.attachKeyboard({
1964                    escape: lightbox.hide,
1965                    right: lightbox.showNext,
1966                    left: lightbox.showPrev
1967                });
1968            }
1969
1970            $win.unbind('resize', lightbox.rescale );
1971
1972            var data = self.getData(index),
1973                total = self.getDataLength(),
1974                n = self.getNext( index ),
1975                ndata, p, i;
1976
1977            Utils.hide( lightbox.elems.info );
1978
1979            try {
1980                for ( i = self._options.preload; i > 0; i-- ) {
1981                    p = new Galleria.Picture();
1982                    ndata = self.getData( n );
1983                    p.preload( 'big' in ndata ? ndata.big : ndata.image );
1984                    n = self.getNext( n );
1985                }
1986            } catch(e) {}
1987
1988            lightbox.image.load( data.big || data.image, function( image ) {
1989
1990                lightbox.width = image.original.width;
1991                lightbox.height = image.original.height;
1992
1993                $( image.image ).css({
1994                    width: '100.5%',
1995                    height: '100.5%',
1996                    top: 0,
1997                    zIndex: 99998
1998                });
1999
2000                Utils.hide( image.image );
2001
2002                lightbox.elems.title.innerHTML = data.title || '';
2003                lightbox.elems.counter.innerHTML = (index + 1) + ' / ' + total;
2004                $win.resize( lightbox.rescale );
2005                lightbox.rescale();
2006            });
2007
2008            $( lightbox.elems.overlay ).show();
2009            $( lightbox.elems.box ).show();
2010        }
2011    };
2012
2013    return this;
2014};
2015
2016// end Galleria constructor
2017
2018Galleria.prototype = {
2019
2020    // bring back the constructor reference
2021
2022    constructor: Galleria,
2023
2024    /**
2025        Use this function to initialize the gallery and start loading.
2026        Should only be called once per instance.
2027
2028        @param {HTMLElement} target The target element
2029        @param {Object} options The gallery options
2030
2031        @returns Instance
2032    */
2033
2034    init: function( target, options ) {
2035
2036        var self = this;
2037
2038        options = _legacyOptions( options );
2039
2040        // save the original ingredients
2041        this._original = {
2042            target: target,
2043            options: options,
2044            data: null
2045        };
2046
2047        // save the target here
2048        this._target = this._dom.target = target.nodeName ? target : $( target ).get(0);
2049
2050        // push the instance
2051        _instances.push( this );
2052
2053        // raise error if no target is detected
2054        if ( !this._target ) {
2055             Galleria.raise('Target not found.', true);
2056             return;
2057        }
2058
2059        // apply options
2060        this._options = {
2061            autoplay: false,
2062            carousel: true,
2063            carouselFollow: true,
2064            carouselSpeed: 400,
2065            carouselSteps: 'auto',
2066            clicknext: false,
2067            dataConfig : function( elem ) { return {}; },
2068            dataSelector: 'img',
2069            dataSource: this._target,
2070            debug: undef,
2071            dummy: undef, // 1.2.5
2072            easing: 'galleria',
2073            extend: function(options) {},
2074            fullscreenCrop: undef, // 1.2.5
2075            fullscreenDoubleTap: true, // 1.2.4 toggles fullscreen on double-tap for touch devices
2076            fullscreenTransition: undef, // 1.2.6
2077            height: 'auto',
2078            idleMode: true, // 1.2.4 toggles idleMode
2079            idleTime: 3000,
2080            idleSpeed: 200,
2081            imageCrop: false,
2082            imageMargin: 0,
2083            imagePan: false,
2084            imagePanSmoothness: 12,
2085            imagePosition: '50%',
2086            imageTimeout: undef, // 1.2.5
2087            initialTransition: undef, // 1.2.4, replaces transitionInitial
2088            keepSource: false,
2089            layerFollow: true, // 1.2.5
2090            lightbox: false, // 1.2.3
2091            lightboxFadeSpeed: 200,
2092            lightboxTransitionSpeed: 200,
2093            linkSourceImages: true,
2094            maxScaleRatio: undef,
2095            minScaleRatio: undef,
2096            overlayOpacity: 0.85,
2097            overlayBackground: '#0b0b0b',
2098            pauseOnInteraction: true,
2099            popupLinks: false,
2100            preload: 2,
2101            queue: true,
2102            show: 0,
2103            showInfo: true,
2104            showCounter: true,
2105            showImagenav: true,
2106            swipe: true, // 1.2.4
2107            thumbCrop: true,
2108            thumbEventType: 'click',
2109            thumbFit: true,
2110            thumbMargin: 0,
2111            thumbQuality: 'auto',
2112            thumbnails: true,
2113            touchTransition: undef, // 1.2.6
2114            transition: 'fade',
2115            transitionInitial: undef, // legacy, deprecate in 1.3. Use initialTransition instead.
2116            transitionSpeed: 400,
2117            useCanvas: false, // 1.2.4
2118            width: 'auto'
2119        };
2120
2121        // legacy support for transitionInitial
2122        this._options.initialTransition = this._options.initialTransition || this._options.transitionInitial;
2123
2124        // turn off debug
2125        if ( options && options.debug === false ) {
2126            DEBUG = false;
2127        }
2128
2129        // set timeout
2130        if ( options && typeof options.imageTimeout === 'number' ) {
2131            TIMEOUT = options.imageTimeout;
2132        }
2133
2134        // set dummy
2135        if ( options && typeof options.dummy === 'string' ) {
2136            DUMMY = options.dummy;
2137        }
2138
2139        // hide all content
2140        $( this._target ).children().hide();
2141
2142        // now we just have to wait for the theme...
2143        if ( typeof Galleria.theme === 'object' ) {
2144            this._init();
2145        } else {
2146            // push the instance into the pool and run it when the theme is ready
2147            _pool.push( this );
2148        }
2149
2150        return this;
2151    },
2152
2153    // this method should only be called once per instance
2154    // for manipulation of data, use the .load method
2155
2156    _init: function() {
2157
2158        var self = this,
2159            options = this._options;
2160
2161        if ( this._initialized ) {
2162            Galleria.raise( 'Init failed: Gallery instance already initialized.' );
2163            return this;
2164        }
2165
2166        this._initialized = true;
2167
2168        if ( !Galleria.theme ) {
2169            Galleria.raise( 'Init failed: No theme found.' );
2170            return this;
2171        }
2172
2173        // merge the theme & caller options
2174        $.extend( true, options, Galleria.theme.defaults, this._original.options );
2175
2176        // check for canvas support
2177        (function( can ) {
2178
2179            if ( !( 'getContext' in can ) ) {
2180                can = null;
2181                return;
2182            }
2183
2184            _canvas = _canvas || {
2185                elem: can,
2186                context: can.getContext( '2d' ),
2187                cache: {},
2188                length: 0
2189            };
2190
2191        }( doc.createElement( 'canvas' ) ) );
2192
2193        // bind the gallery to run when data is ready
2194        this.bind( Galleria.DATA, function() {
2195
2196            // Warn for quirks mode
2197            if ( Galleria.QUIRK ) {
2198                Galleria.raise('Your page is in Quirks mode, Galleria may not render correctly. Please validate your HTML.');
2199            }
2200
2201            // save the new data
2202            this._original.data = this._data;
2203
2204            // lets show the counter here
2205            this.get('total').innerHTML = this.getDataLength();
2206
2207            // cache the container
2208            var $container = this.$( 'container' );
2209
2210            // the gallery is ready, let's just wait for the css
2211            var num = { width: 0, height: 0 };
2212            var testHeight = function() {
2213                return self.$( 'stage' ).height();
2214            };
2215
2216            // check container and thumbnail height
2217            Utils.wait({
2218                until: function() {
2219
2220                    // keep trying to get the value
2221                    $.each(['width', 'height'], function( i, m ) {
2222
2223                        // first check if options is set
2224
2225                        if ( options[ m ] && typeof options[ m ] === 'number' ) {
2226                            num[ m ] = options[ m ];
2227                        } else {
2228
2229                            // else extract the measures from different sources and grab the highest value
2230                            num[ m ] = Math.max(
2231                                Utils.parseValue( $container.css( m ) ),         // 1. the container css
2232                                Utils.parseValue( self.$( 'target' ).css( m ) ), // 2. the target css
2233                                $container[ m ](),                               // 3. the container jQuery method
2234                                self.$( 'target' )[ m ]()                        // 4. the container jQuery method
2235                            );
2236                        }
2237
2238                        // apply the new measures
2239                        $container[ m ]( num[ m ] );
2240
2241                    });
2242
2243                    return testHeight() && num.width && num.height > 10;
2244
2245                },
2246                success: function() {
2247
2248                    // for some strange reason, webkit needs a single setTimeout to play ball
2249                    if ( Galleria.WEBKIT ) {
2250                        window.setTimeout( function() {
2251                            self._run();
2252                        }, 1);
2253                    } else {
2254                        self._run();
2255                    }
2256                },
2257                error: function() {
2258
2259                    // Height was probably not set, raise hard errors
2260
2261                    if ( testHeight() ) {
2262                        Galleria.raise('Could not extract sufficient width/height of the gallery container. Traced measures: width:' + num.width + 'px, height: ' + num.height + 'px.', true);
2263                    } else {
2264                        Galleria.raise('Could not extract a stage height from the CSS. Traced height: ' + testHeight() + 'px.', true);
2265                    }
2266                },
2267                timeout: 10000
2268            });
2269        });
2270
2271        // build the gallery frame
2272        this.append({
2273            'info-text' :
2274                ['info-title', 'info-description'],
2275            'info' :
2276                ['info-text'],
2277            'image-nav' :
2278                ['image-nav-right', 'image-nav-left'],
2279            'stage' :
2280                ['images', 'loader', 'counter', 'image-nav'],
2281            'thumbnails-list' :
2282                ['thumbnails'],
2283            'thumbnails-container' :
2284                ['thumb-nav-left', 'thumbnails-list', 'thumb-nav-right'],
2285            'container' :
2286                ['stage', 'thumbnails-container', 'info', 'tooltip']
2287        });
2288
2289        Utils.hide( this.$( 'counter' ).append(
2290            this.get( 'current' ),
2291            doc.createTextNode(' / '),
2292            this.get( 'total' )
2293        ) );
2294
2295        this.setCounter('&#8211;');
2296
2297        Utils.hide( self.get('tooltip') );
2298
2299        // add a notouch class on the container to prevent unwanted :hovers on touch devices
2300        this.$( 'container' ).addClass( Galleria.TOUCH ? 'touch' : 'notouch' );
2301
2302        // add images to the controls
2303        $.each( new Array(2), function( i ) {
2304
2305            // create a new Picture instance
2306            var image = new Galleria.Picture();
2307
2308            // apply some styles, create & prepend overlay
2309            $( image.container ).css({
2310                position: 'absolute',
2311                top: 0,
2312                left: 0
2313            }).prepend( self._layers[i] = $( Utils.create('galleria-layer') ).css({
2314                position: 'absolute',
2315                top:0, left:0, right:0, bottom:0,
2316                zIndex:2
2317            })[0] );
2318
2319            // append the image
2320            self.$( 'images' ).append( image.container );
2321
2322            // reload the controls
2323            self._controls[i] = image;
2324
2325        });
2326
2327        // some forced generic styling
2328        this.$( 'images' ).css({
2329            position: 'relative',
2330            top: 0,
2331            left: 0,
2332            width: '100%',
2333            height: '100%'
2334        });
2335
2336        this.$( 'thumbnails, thumbnails-list' ).css({
2337            overflow: 'hidden',
2338            position: 'relative'
2339        });
2340
2341        // bind image navigation arrows
2342        this.$( 'image-nav-right, image-nav-left' ).bind( 'click', function(e) {
2343
2344            // tune the clicknext option
2345            if ( options.clicknext ) {
2346                e.stopPropagation();
2347            }
2348
2349            // pause if options is set
2350            if ( options.pauseOnInteraction ) {
2351                self.pause();
2352            }
2353
2354            // navigate
2355            var fn = /right/.test( this.className ) ? 'next' : 'prev';
2356            self[ fn ]();
2357
2358        });
2359
2360        // hide controls if chosen to
2361        $.each( ['info','counter','image-nav'], function( i, el ) {
2362            if ( options[ 'show' + el.substr(0,1).toUpperCase() + el.substr(1).replace(/-/,'') ] === false ) {
2363                Utils.moveOut( self.get( el.toLowerCase() ) );
2364            }
2365        });
2366
2367        // load up target content
2368        this.load();
2369
2370        // now it's usually safe to remove the content
2371        // IE will never stop loading if we remove it, so let's keep it hidden for IE (it's usually fast enough anyway)
2372        if ( !options.keepSource && !IE ) {
2373            this._target.innerHTML = '';
2374        }
2375
2376        // re-append the errors, if they happened before clearing
2377        if ( this.get( 'errors' ) ) {
2378            this.appendChild( 'target', 'errors' );
2379        }
2380
2381        // append the gallery frame
2382        this.appendChild( 'target', 'container' );
2383
2384        // parse the carousel on each thumb load
2385        if ( options.carousel ) {
2386            var count = 0,
2387                show = options.show;
2388            this.bind( Galleria.THUMBNAIL, function() {
2389                this.updateCarousel();
2390                if ( ++count == this.getDataLength() && typeof show == 'number' && show > 0 ) {
2391                    this._carousel.follow( show );
2392                }
2393            });
2394        }
2395
2396        // bind swipe gesture
2397        if ( options.swipe ) {
2398
2399            (function( images ) {
2400
2401                var swipeStart = [0,0],
2402                    swipeStop = [0,0],
2403                    limitX = 30,
2404                    limitY = 100,
2405                    multi = false,
2406                    tid = 0,
2407                    data,
2408                    ev = {
2409                        start: 'touchstart',
2410                        move: 'touchmove',
2411                        stop: 'touchend'
2412                    },
2413                    getData = function(e) {
2414                        return e.originalEvent.touches ? e.originalEvent.touches[0] : e;
2415                    },
2416                    moveHandler = function( e ) {
2417
2418                        if ( e.originalEvent.touches && e.originalEvent.touches.length > 1 ) {
2419                            return;
2420                        }
2421
2422                        data = getData( e );
2423                        swipeStop = [ data.pageX, data.pageY ];
2424
2425                        if ( !swipeStart[0] ) {
2426                            swipeStart = swipeStop;
2427                        }
2428
2429                        if ( Math.abs( swipeStart[0] - swipeStop[0] ) > 10 ) {
2430                            e.preventDefault();
2431                        }
2432                    },
2433                    upHandler = function( e ) {
2434
2435                        images.unbind( ev.move, moveHandler );
2436
2437                        // if multitouch (possibly zooming), abort
2438                        if ( ( e.originalEvent.touches && e.originalEvent.touches.length ) || multi ) {
2439                            multi = !multi;
2440                            return;
2441                        }
2442
2443                        if ( Utils.timestamp() - tid < 1000 &&
2444                             Math.abs( swipeStart[0] - swipeStop[0] ) > limitX &&
2445                             Math.abs( swipeStart[1] - swipeStop[1] ) < limitY ) {
2446
2447                            e.preventDefault();
2448                            self[ swipeStart[0] > swipeStop[0] ? 'next' : 'prev' ]();
2449                        }
2450
2451                        swipeStart = swipeStop = [0,0];
2452                    };
2453
2454                images.bind(ev.start, function(e) {
2455
2456                    if ( e.originalEvent.touches && e.originalEvent.touches.length > 1 ) {
2457                        return;
2458                    }
2459
2460                    data = getData(e);
2461                    tid = Utils.timestamp();
2462                    swipeStart = swipeStop = [ data.pageX, data.pageY ];
2463                    images.bind(ev.move, moveHandler ).one(ev.stop, upHandler);
2464
2465                });
2466
2467            }( self.$( 'images' ) ));
2468
2469            // double-tap/click fullscreen toggle
2470
2471            if ( options.fullscreenDoubleTap ) {
2472
2473                this.$( 'stage' ).bind( 'touchstart', (function() {
2474                    var last, cx, cy, lx, ly, now,
2475                        getData = function(e) {
2476                            return e.originalEvent.touches ? e.originalEvent.touches[0] : e;
2477                        };
2478                    return function(e) {
2479                        now = Galleria.utils.timestamp();
2480                        cx = getData(e).pageX;
2481                        cy = getData(e).pageY;
2482                        if ( ( now - last < 500 ) && ( cx - lx < 20) && ( cy - ly < 20) ) {
2483                            self.toggleFullscreen();
2484                            e.preventDefault();
2485                            self.$( 'stage' ).unbind( 'touchend', arguments.callee );
2486                            return;
2487                        }
2488                        last = now;
2489                        lx = cx;
2490                        ly = cy;
2491                    };
2492                }()));
2493            }
2494
2495        }
2496
2497        // optimize touch for container
2498        Utils.optimizeTouch( this.get( 'container' ) );
2499
2500        return this;
2501    },
2502
2503    // Creates the thumbnails and carousel
2504    // can be used at any time, f.ex when the data object is manipulated
2505
2506    _createThumbnails : function() {
2507
2508        this.get( 'total' ).innerHTML = this.getDataLength();
2509
2510        var i,
2511            src,
2512            thumb,
2513            data,
2514
2515            $container,
2516
2517            self = this,
2518            o = this._options,
2519
2520            // get previously active thumbnail, if exists
2521            active = (function() {
2522                var a = self.$('thumbnails').find('.active');
2523                if ( !a.length ) {
2524                    return false;
2525                }
2526                return a.find('img').attr('src');
2527            }()),
2528
2529            // cache the thumbnail option
2530            optval = typeof o.thumbnails === 'string' ? o.thumbnails.toLowerCase() : null,
2531
2532            // move some data into the instance
2533            // for some reason, jQuery cant handle css(property) when zooming in FF, breaking the gallery
2534            // so we resort to getComputedStyle for browsers who support it
2535            getStyle = function( prop ) {
2536                return doc.defaultView && doc.defaultView.getComputedStyle ?
2537                    doc.defaultView.getComputedStyle( thumb.container, null )[ prop ] :
2538                    $container.css( prop );
2539            },
2540
2541            fake = function(image, index, container) {
2542                return function() {
2543                    $( container ).append( image );
2544                    self.trigger({
2545                        type: Galleria.THUMBNAIL,
2546                        thumbTarget: image,
2547                        index: index
2548                    });
2549                };
2550            },
2551
2552            onThumbEvent = function( e ) {
2553
2554                // pause if option is set
2555                if ( o.pauseOnInteraction ) {
2556                    self.pause();
2557                }
2558
2559                // extract the index from the data
2560                var index = $( e.currentTarget ).data( 'index' );
2561                if ( self.getIndex() !== index ) {
2562                    self.show( index );
2563                }
2564
2565                e.preventDefault();
2566            },
2567
2568            onThumbLoad = function( thumb ) {
2569
2570                // scale when ready
2571                thumb.scale({
2572                    width:    thumb.data.width,
2573                    height:   thumb.data.height,
2574                    crop:     o.thumbCrop,
2575                    margin:   o.thumbMargin,
2576                    canvas:   o.useCanvas,
2577                    complete: function( thumb ) {
2578
2579                        // shrink thumbnails to fit
2580                        var top = ['left', 'top'],
2581                            arr = ['Width', 'Height'],
2582                            m,
2583                            css;
2584
2585                        // calculate shrinked positions
2586                        $.each(arr, function( i, measure ) {
2587                            m = measure.toLowerCase();
2588                            if ( (o.thumbCrop !== true || o.thumbCrop === m ) && o.thumbFit ) {
2589                                css = {};
2590                                css[ m ] = thumb[ m ];
2591                                $( thumb.container ).css( css );
2592                                css = {};
2593                                css[ top[ i ] ] = 0;
2594                                $( thumb.image ).css( css );
2595                            }
2596
2597                            // cache outer measures
2598                            thumb[ 'outer' + measure ] = $( thumb.container )[ 'outer' + measure ]( true );
2599                        });
2600
2601                        // set high quality if downscale is moderate
2602                        Utils.toggleQuality( thumb.image,
2603                            o.thumbQuality === true ||
2604                            ( o.thumbQuality === 'auto' && thumb.original.width < thumb.width * 3 )
2605                        );
2606
2607                        // trigger the THUMBNAIL event
2608                        self.trigger({
2609                            type: Galleria.THUMBNAIL,
2610                            thumbTarget: thumb.image,
2611                            index: thumb.data.order
2612                        });
2613                    }
2614                });
2615            };
2616
2617        this._thumbnails = [];
2618
2619        this.$( 'thumbnails' ).empty();
2620
2621        // loop through data and create thumbnails
2622        for( i = 0; this._data[ i ]; i++ ) {
2623
2624            data = this._data[ i ];
2625
2626            if ( o.thumbnails === true ) {
2627
2628                // add a new Picture instance
2629                thumb = new Galleria.Picture(i);
2630
2631                // get source from thumb or image
2632                src = data.thumb || data.image;
2633
2634                // append the thumbnail
2635                this.$( 'thumbnails' ).append( thumb.container );
2636
2637                // cache the container
2638                $container = $( thumb.container );
2639
2640                thumb.data = {
2641                    width  : Utils.parseValue( getStyle( 'width' ) ),
2642                    height : Utils.parseValue( getStyle( 'height' ) ),
2643                    order  : i
2644                };
2645
2646                // grab & reset size for smoother thumbnail loads
2647                if ( o.thumbFit && o.thumbCrop !== true ) {
2648                    $container.css( { width: 0, height: 0 } );
2649                } else {
2650                    $container.css( { width: thumb.data.width, height: thumb.data.height } );
2651                }
2652
2653                // load the thumbnail
2654                thumb.load( src, onThumbLoad );
2655
2656                // preload all images here
2657                if ( o.preload === 'all' ) {
2658                    thumb.preload( data.image );
2659                }
2660
2661            // create empty spans if thumbnails is set to 'empty'
2662            } else if ( optval === 'empty' || optval === 'numbers' ) {
2663
2664                thumb = {
2665                    container:  Utils.create( 'galleria-image' ),
2666                    image: Utils.create( 'img', 'span' ),
2667                    ready: true
2668                };
2669
2670                // create numbered thumbnails
2671                if ( optval === 'numbers' ) {
2672                    $( thumb.image ).text( i + 1 );
2673                }
2674
2675                this.$( 'thumbnails' ).append( thumb.container );
2676
2677                // we need to "fake" a loading delay before we append and trigger
2678                // 50+ should be enough
2679
2680                window.setTimeout( ( fake )( thumb.image, i, thumb.container ), 50 + ( i*20 ) );
2681
2682            // create null object to silent errors
2683            } else {
2684                thumb = {
2685                    container: null,
2686                    image: null
2687                };
2688            }
2689
2690            // add events for thumbnails
2691            // you can control the event type using thumb_event_type
2692            // we'll add the same event to the source if it's kept
2693
2694            $( thumb.container ).add( o.keepSource && o.linkSourceImages ? data.original : null )
2695                .data('index', i).bind( o.thumbEventType, onThumbEvent );
2696
2697            if (active === src) {
2698                $( thumb.container ).addClass( 'active' );
2699            }
2700
2701            this._thumbnails.push( thumb );
2702        }
2703    },
2704
2705    // the internal _run method should be called after loading data into galleria
2706    // makes sure the gallery has proper measurements before postrun & ready
2707    _run : function() {
2708
2709        var self = this;
2710
2711        self._createThumbnails();
2712
2713        // make sure we have a stageHeight && stageWidth
2714
2715        Utils.wait({
2716
2717            until: function() {
2718
2719                // Opera crap
2720                if ( Galleria.OPERA ) {
2721                    self.$( 'stage' ).css( 'display', 'inline-block' );
2722                }
2723
2724                self._stageWidth  = self.$( 'stage' ).width();
2725                self._stageHeight = self.$( 'stage' ).height();
2726
2727                return( self._stageWidth &&
2728                        self._stageHeight > 50 ); // what is an acceptable height?
2729            },
2730
2731            success: function() {
2732
2733                // save the instance
2734                _galleries.push( self );
2735
2736                // postrun some stuff after the gallery is ready
2737
2738                // show counter
2739                Utils.show( self.get('counter') );
2740
2741                // bind carousel nav
2742                if ( self._options.carousel ) {
2743                    self._carousel.bindControls();
2744                }
2745
2746                // start autoplay
2747                if ( self._options.autoplay ) {
2748
2749                    self.pause();
2750
2751                    if ( typeof self._options.autoplay === 'number' ) {
2752                        self._playtime = self._options.autoplay;
2753                    }
2754
2755                    self.trigger( Galleria.PLAY );
2756                    self._playing = true;
2757                }
2758                // if second load, just do the show and return
2759                if ( self._firstrun ) {
2760                    if ( typeof self._options.show === 'number' ) {
2761                        self.show( self._options.show );
2762                    }
2763                    return;
2764                }
2765
2766                self._firstrun = true;
2767
2768                // bind clicknext
2769                if ( self._options.clicknext && !Galleria.TOUCH ) {
2770                    $.each( self._data, function( i, data ) {
2771                        delete data.link;
2772                    });
2773                    self.$( 'stage' ).css({ cursor : 'pointer' }).bind( 'click', function(e) {
2774                        // pause if options is set
2775                        if ( self._options.pauseOnInteraction ) {
2776                            self.pause();
2777                        }
2778                        self.next();
2779                    });
2780                }
2781
2782                // initialize the History plugin
2783                if ( Galleria.History ) {
2784
2785                    // bind the show method
2786                    Galleria.History.change(function( value ) {
2787
2788                        // if ID is NaN, the user pressed back from the first image
2789                        // return to previous address
2790                        if ( isNaN( value ) ) {
2791                            window.history.go(-1);
2792
2793                        // else show the image
2794                        } else {
2795                            self.show( value, undef, true );
2796                        }
2797                    });
2798                }
2799
2800                // Trigger Galleria.ready
2801                $.each( Galleria.ready.callbacks, function() {
2802                    this.call( self, self._options );
2803                });
2804
2805                self.trigger( Galleria.READY );
2806
2807                // call the theme init method
2808                Galleria.theme.init.call( self, self._options );
2809
2810                // call the extend option
2811                self._options.extend.call( self, self._options );
2812
2813                // show the initial image
2814                // first test for permalinks in history
2815                if ( /^[0-9]{1,4}$/.test( HASH ) && Galleria.History ) {
2816                    self.show( HASH, undef, true );
2817
2818                } else if( self._data[ self._options.show ] ) {
2819                    self.show( self._options.show );
2820                }
2821            },
2822
2823            error: function() {
2824                Galleria.raise('Stage width or height is too small to show the gallery. Traced measures: width:' + self._stageWidth + 'px, height: ' + self._stageHeight + 'px.', true);
2825            }
2826
2827        });
2828    },
2829
2830    /**
2831        Loads data into the gallery.
2832        You can call this method on an existing gallery to reload the gallery with new data.
2833
2834        @param {Array|string} source Optional JSON array of data or selector of where to find data in the document.
2835        Defaults to the Galleria target or dataSource option.
2836
2837        @param {string} selector Optional element selector of what elements to parse.
2838        Defaults to 'img'.
2839
2840        @param {Function} [config] Optional function to modify the data extraction proceedure from the selector.
2841        See the data_config option for more information.
2842
2843        @returns Instance
2844    */
2845
2846    load : function( source, selector, config ) {
2847
2848        var self = this;
2849
2850        // empty the data array
2851        this._data = [];
2852
2853        // empty the thumbnails
2854        this._thumbnails = [];
2855        this.$('thumbnails').empty();
2856
2857        // shorten the arguments
2858        if ( typeof selector === 'function' ) {
2859            config = selector;
2860            selector = null;
2861        }
2862
2863        // use the source set by target
2864        source = source || this._options.dataSource;
2865
2866        // use selector set by option
2867        selector = selector || this._options.dataSelector;
2868
2869        // use the data_config set by option
2870        config = config || this._options.dataConfig;
2871
2872        // if source is a true object, make it into an array
2873        if( /^function Object/.test( source.constructor ) ) {
2874            source = [source];
2875        }
2876
2877        // check if the data is an array already
2878        if ( source.constructor === Array ) {
2879            if ( this.validate( source ) ) {
2880
2881                this._data = source;
2882                this._parseData().trigger( Galleria.DATA );
2883
2884            } else {
2885                Galleria.raise( 'Load failed: JSON Array not valid.' );
2886            }
2887            return this;
2888        }
2889
2890        // loop through images and set data
2891        $( source ).find( selector ).each( function( i, img ) {
2892            img = $( img );
2893            var data = {},
2894                parent = img.parent(),
2895                href = parent.attr( 'href' ),
2896                rel  = parent.attr( 'rel' );
2897
2898            if ( href ) {
2899                data.image = data.big = href;
2900            }
2901            if ( rel ) {
2902                data.big = rel;
2903            }
2904
2905            // mix default extractions with the hrefs and config
2906            // and push it into the data array
2907            self._data.push( $.extend({
2908
2909                title:       img.attr('title') || '',
2910                thumb:       img.attr('src'),
2911                image:       img.attr('src'),
2912                big:         img.attr('src'),
2913                description: img.attr('alt') || '',
2914                link:        img.attr('longdesc'),
2915                original:    img.get(0) // saved as a reference
2916
2917            }, data, config( img ) ) );
2918
2919        });
2920        // trigger the DATA event and return
2921        if ( this.getDataLength() ) {
2922            this.trigger( Galleria.DATA );
2923        } else {
2924            Galleria.raise('Load failed: no data found.');
2925        }
2926        return this;
2927
2928    },
2929
2930    // make sure the data works properly
2931    _parseData : function() {
2932
2933        var self = this;
2934
2935        $.each( this._data, function( i, data ) {
2936            // copy image as thumb if no thumb exists
2937            if ( 'thumb' in data === false ) {
2938                self._data[ i ].thumb = data.image;
2939            }
2940            // copy image as big image if no biggie exists
2941            if ( !'big' in data ) {
2942                self._data[ i ].big = data.image;
2943            }
2944        });
2945
2946        return this;
2947    },
2948
2949    /**
2950        Adds and/or removes images from the gallery
2951        Works just like Array.splice
2952        https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice
2953
2954        @example this.splice( 2, 4 ); // removes 4 images after the second image
2955
2956        @returns Instance
2957    */
2958
2959    splice: function() {
2960        Array.prototype.splice.apply( this._data, Utils.array( arguments ) );
2961        return this._parseData()._createThumbnails();
2962    },
2963
2964    /**
2965        Append images to the gallery
2966        Works just like Array.push
2967        https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push
2968
2969        @example this.push({ image: 'image1.jpg' }); // appends the image to the gallery
2970
2971        @returns Instance
2972    */
2973
2974    push: function() {
2975        Array.prototype.push.apply( this._data, Utils.array( arguments ) );
2976        return this._parseData()._createThumbnails();
2977    },
2978
2979    _getActive: function() {
2980        return this._controls.getActive();
2981    },
2982
2983    validate : function( data ) {
2984        // todo: validate a custom data array
2985        return true;
2986    },
2987
2988    /**
2989        Bind any event to Galleria
2990
2991        @param {string} type The Event type to listen for
2992        @param {Function} fn The function to execute when the event is triggered
2993
2994        @example this.bind( 'image', function() { Galleria.log('image shown') });
2995
2996        @returns Instance
2997    */
2998
2999    bind : function(type, fn) {
3000
3001        // allow 'image' instead of Galleria.IMAGE
3002        type = _patchEvent( type );
3003
3004        this.$( 'container' ).bind( type, this.proxy(fn) );
3005        return this;
3006    },
3007
3008    /**
3009        Unbind any event to Galleria
3010
3011        @param {string} type The Event type to forget
3012
3013        @returns Instance
3014    */
3015
3016    unbind : function(type) {
3017
3018        type = _patchEvent( type );
3019
3020        this.$( 'container' ).unbind( type );
3021        return this;
3022    },
3023
3024    /**
3025        Manually trigger a Galleria event
3026
3027        @param {string} type The Event to trigger
3028
3029        @returns Instance
3030    */
3031
3032    trigger : function( type ) {
3033
3034        type = typeof type === 'object' ?
3035            $.extend( type, { scope: this } ) :
3036            { type: _patchEvent( type ), scope: this };
3037
3038        this.$( 'container' ).trigger( type );
3039
3040        return this;
3041    },
3042
3043    /**
3044        Assign an "idle state" to any element.
3045        The idle state will be applied after a certain amount of idle time
3046        Useful to hide f.ex navigation when the gallery is inactive
3047
3048        @param {HTMLElement|string} elem The Dom node or selector to apply the idle state to
3049        @param {Object} styles the CSS styles to apply
3050
3051        @example addIdleState( this.get('image-nav'), { opacity: 0 });
3052        @example addIdleState( '.galleria-image-nav', { top: -200 });
3053
3054        @returns Instance
3055    */
3056
3057    addIdleState: function( elem, styles ) {
3058        this._idle.add.apply( this._idle, Utils.array( arguments ) );
3059        return this;
3060    },
3061
3062    /**
3063        Removes any idle state previously set using addIdleState()
3064
3065        @param {HTMLElement|string} elem The Dom node or selector to remove the idle state from.
3066
3067        @returns Instance
3068    */
3069
3070    removeIdleState: function( elem ) {
3071        this._idle.remove.apply( this._idle, Utils.array( arguments ) );
3072        return this;
3073    },
3074
3075    /**
3076        Force Galleria to enter idle mode.
3077
3078        @returns Instance
3079    */
3080
3081    enterIdleMode: function() {
3082        this._idle.hide();
3083        return this;
3084    },
3085
3086    /**
3087        Force Galleria to exit idle mode.
3088
3089        @returns Instance
3090    */
3091
3092    exitIdleMode: function() {
3093        this._idle.showAll();
3094        return this;
3095    },
3096
3097    /**
3098        Enter FullScreen mode
3099
3100        @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
3101
3102        @returns Instance
3103    */
3104
3105    enterFullscreen: function( callback ) {
3106        this._fullscreen.enter.apply( this, Utils.array( arguments ) );
3107        return this;
3108    },
3109
3110    /**
3111        Exits FullScreen mode
3112
3113        @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
3114
3115        @returns Instance
3116    */
3117
3118    exitFullscreen: function( callback ) {
3119        this._fullscreen.exit.apply( this, Utils.array( arguments ) );
3120        return this;
3121    },
3122
3123    /**
3124        Toggle FullScreen mode
3125
3126        @param {Function} callback the function to be executed when the fullscreen mode is fully applied or removed.
3127
3128        @returns Instance
3129    */
3130
3131    toggleFullscreen: function( callback ) {
3132        this._fullscreen[ this.isFullscreen() ? 'exit' : 'enter'].apply( this, Utils.array( arguments ) );
3133        return this;
3134    },
3135
3136    /**
3137        Adds a tooltip to any element.
3138        You can also call this method with an object as argument with elemID:value pairs to apply tooltips to (see examples)
3139
3140        @param {HTMLElement} elem The DOM Node to attach the event to
3141        @param {string|Function} value The tooltip message. Can also be a function that returns a string.
3142
3143        @example this.bindTooltip( this.get('thumbnails'), 'My thumbnails');
3144        @example this.bindTooltip( this.get('thumbnails'), function() { return 'My thumbs' });
3145        @example this.bindTooltip( { image_nav: 'Navigation' });
3146
3147        @returns Instance
3148    */
3149
3150    bindTooltip: function( elem, value ) {
3151        this._tooltip.bind.apply( this._tooltip, Utils.array(arguments) );
3152        return this;
3153    },
3154
3155    /**
3156        Note: this method is deprecated. Use refreshTooltip() instead.
3157
3158        Redefine a tooltip.
3159        Use this if you want to re-apply a tooltip value to an already bound tooltip element.
3160
3161        @param {HTMLElement} elem The DOM Node to attach the event to
3162        @param {string|Function} value The tooltip message. Can also be a function that returns a string.
3163
3164        @returns Instance
3165    */
3166
3167    defineTooltip: function( elem, value ) {
3168        this._tooltip.define.apply( this._tooltip, Utils.array(arguments) );
3169        return this;
3170    },
3171
3172    /**
3173        Refresh a tooltip value.
3174        Use this if you want to change the tooltip value at runtime, f.ex if you have a play/pause toggle.
3175
3176        @param {HTMLElement} elem The DOM Node that has a tooltip that should be refreshed
3177
3178        @returns Instance
3179    */
3180
3181    refreshTooltip: function( elem ) {
3182        this._tooltip.show.apply( this._tooltip, Utils.array(arguments) );
3183        return this;
3184    },
3185
3186    /**
3187        Open a pre-designed lightbox with the currently active image.
3188        You can control some visuals using gallery options.
3189
3190        @returns Instance
3191    */
3192
3193    openLightbox: function() {
3194        this._lightbox.show.apply( this._lightbox, Utils.array( arguments ) );
3195        return this;
3196    },
3197
3198    /**
3199        Close the lightbox.
3200
3201        @returns Instance
3202    */
3203
3204    closeLightbox: function() {
3205        this._lightbox.hide.apply( this._lightbox, Utils.array( arguments ) );
3206        return this;
3207    },
3208
3209    /**
3210        Get the currently active image element.
3211
3212        @returns {HTMLElement} The image element
3213    */
3214
3215    getActiveImage: function() {
3216        return this._getActive().image || undef;
3217    },
3218
3219    /**
3220        Get the currently active thumbnail element.
3221
3222        @returns {HTMLElement} The thumbnail element
3223    */
3224
3225    getActiveThumb: function() {
3226        return this._thumbnails[ this._active ].image || undef;
3227    },
3228
3229    /**
3230        Get the mouse position relative to the gallery container
3231
3232        @param e The mouse event
3233
3234        @example
3235
3236var gallery = this;
3237$(document).mousemove(function(e) {
3238    console.log( gallery.getMousePosition(e).x );
3239});
3240
3241        @returns {Object} Object with x & y of the relative mouse postion
3242    */
3243
3244    getMousePosition : function(e) {
3245        return {
3246            x: e.pageX - this.$( 'container' ).offset().left,
3247            y: e.pageY - this.$( 'container' ).offset().top
3248        };
3249    },
3250
3251    /**
3252        Adds a panning effect to the image
3253
3254        @param img The optional image element. If not specified it takes the currently active image
3255
3256        @returns Instance
3257    */
3258
3259    addPan : function( img ) {
3260
3261        if ( this._options.imageCrop === false ) {
3262            return;
3263        }
3264
3265        img = $( img || this.getActiveImage() );
3266
3267        // define some variables and methods
3268        var self   = this,
3269            x      = img.width() / 2,
3270            y      = img.height() / 2,
3271            destX  = parseInt( img.css( 'left' ), 10 ),
3272            destY  = parseInt( img.css( 'top' ), 10 ),
3273            curX   = destX || 0,
3274            curY   = destY || 0,
3275            distX  = 0,
3276            distY  = 0,
3277            active = false,
3278            ts     = Utils.timestamp(),
3279            cache  = 0,
3280            move   = 0,
3281
3282            // positions the image
3283            position = function( dist, cur, pos ) {
3284                if ( dist > 0 ) {
3285                    move = Math.round( Math.max( dist * -1, Math.min( 0, cur ) ) );
3286                    if ( cache !== move ) {
3287
3288                        cache = move;
3289
3290                        if ( IE === 8 ) { // scroll is faster for IE
3291                            img.parent()[ 'scroll' + pos ]( move * -1 );
3292                        } else {
3293                            var css = {};
3294                            css[ pos.toLowerCase() ] = move;
3295                            img.css(css);
3296                        }
3297                    }
3298                }
3299            },
3300
3301            // calculates mouse position after 50ms
3302            calculate = function(e) {
3303                if (Utils.timestamp() - ts < 50) {
3304                    return;
3305                }
3306                active = true;
3307                x = self.getMousePosition(e).x;
3308                y = self.getMousePosition(e).y;
3309            },
3310
3311            // the main loop to check
3312            loop = function(e) {
3313
3314                if (!active) {
3315                    return;
3316                }
3317
3318                distX = img.width() - self._stageWidth;
3319                distY = img.height() - self._stageHeight;
3320                destX = x / self._stageWidth * distX * -1;
3321                destY = y / self._stageHeight * distY * -1;
3322                curX += ( destX - curX ) / self._options.imagePanSmoothness;
3323                curY += ( destY - curY ) / self._options.imagePanSmoothness;
3324
3325                position( distY, curY, 'Top' );
3326                position( distX, curX, 'Left' );
3327
3328            };
3329
3330        // we need to use scroll in IE8 to speed things up
3331        if ( IE === 8 ) {
3332
3333            img.parent().scrollTop( curY * -1 ).scrollLeft( curX * -1 );
3334            img.css({
3335                top: 0,
3336                left: 0
3337            });
3338
3339        }
3340
3341        // unbind and bind event
3342        this.$( 'stage' ).unbind( 'mousemove', calculate ).bind( 'mousemove', calculate );
3343
3344        // loop the loop
3345        Utils.addTimer('pan', loop, 50, true);
3346
3347        return this;
3348    },
3349
3350    /**
3351        Brings the scope into any callback
3352
3353        @param fn The callback to bring the scope into
3354        @param scope Optional scope to bring
3355
3356        @example $('#fullscreen').click( this.proxy(function() { this.enterFullscreen(); }) )
3357
3358        @returns {Function} Return the callback with the gallery scope
3359    */
3360
3361    proxy : function( fn, scope ) {
3362        if ( typeof fn !== 'function' ) {
3363            return function() {};
3364        }
3365        scope = scope || this;
3366        return function() {
3367            return fn.apply( scope, Utils.array( arguments ) );
3368        };
3369    },
3370
3371    /**
3372        Removes the panning effect set by addPan()
3373
3374        @returns Instance
3375    */
3376
3377    removePan: function() {
3378
3379        // todo: doublecheck IE8
3380
3381        this.$( 'stage' ).unbind( 'mousemove' );
3382
3383        Utils.clearTimer( 'pan' );
3384
3385        return this;
3386    },
3387
3388    /**
3389        Adds an element to the Galleria DOM array.
3390        When you add an element here, you can access it using element ID in many API calls
3391
3392        @param {string} id The element ID you wish to use. You can add many elements by adding more arguments.
3393
3394        @example addElement('mybutton');
3395        @example addElement('mybutton','mylink');
3396
3397        @returns Instance
3398    */
3399
3400    addElement : function( id ) {
3401
3402        var dom = this._dom;
3403
3404        $.each( Utils.array(arguments), function( i, blueprint ) {
3405           dom[ blueprint ] = Utils.create( 'galleria-' + blueprint );
3406        });
3407
3408        return this;
3409    },
3410
3411    /**
3412        Attach keyboard events to Galleria
3413
3414        @param {Object} map The map object of events.
3415        Possible keys are 'UP', 'DOWN', 'LEFT', 'RIGHT', 'RETURN', 'ESCAPE', 'BACKSPACE', and 'SPACE'.
3416
3417        @example
3418
3419this.attachKeyboard({
3420    right: this.next,
3421    left: this.prev,
3422    up: function() {
3423        console.log( 'up key pressed' )
3424    }
3425});
3426
3427        @returns Instance
3428    */
3429
3430    attachKeyboard : function( map ) {
3431        this._keyboard.attach.apply( this._keyboard, Utils.array( arguments ) );
3432        return this;
3433    },
3434
3435    /**
3436        Detach all keyboard events to Galleria
3437
3438        @returns Instance
3439    */
3440
3441    detachKeyboard : function() {
3442        this._keyboard.detach.apply( this._keyboard, Utils.array( arguments ) );
3443        return this;
3444    },
3445
3446    /**
3447        Fast helper for appending galleria elements that you added using addElement()
3448
3449        @param {string} parentID The parent element ID where the element will be appended
3450        @param {string} childID the element ID that should be appended
3451
3452        @example this.addElement('myElement');
3453        this.appendChild( 'info', 'myElement' );
3454
3455        @returns Instance
3456    */
3457
3458    appendChild : function( parentID, childID ) {
3459        this.$( parentID ).append( this.get( childID ) || childID );
3460        return this;
3461    },
3462
3463    /**
3464        Fast helper for prepending galleria elements that you added using addElement()
3465
3466        @param {string} parentID The parent element ID where the element will be prepended
3467        @param {string} childID the element ID that should be prepended
3468
3469        @example
3470
3471this.addElement('myElement');
3472this.prependChild( 'info', 'myElement' );
3473
3474        @returns Instance
3475    */
3476
3477    prependChild : function( parentID, childID ) {
3478        this.$( parentID ).prepend( this.get( childID ) || childID );
3479        return this;
3480    },
3481
3482    /**
3483        Remove an element by blueprint
3484
3485        @param {string} elemID The element to be removed.
3486        You can remove multiple elements by adding arguments.
3487
3488        @returns Instance
3489    */
3490
3491    remove : function( elemID ) {
3492        this.$( Utils.array( arguments ).join(',') ).remove();
3493        return this;
3494    },
3495
3496    // a fast helper for building dom structures
3497    // leave this out of the API for now
3498
3499    append : function( data ) {
3500        var i, j;
3501        for( i in data ) {
3502            if ( data.hasOwnProperty( i ) ) {
3503                if ( data[i].constructor === Array ) {
3504                    for( j = 0; data[i][j]; j++ ) {
3505                        this.appendChild( i, data[i][j] );
3506                    }
3507                } else {
3508                    this.appendChild( i, data[i] );
3509                }
3510            }
3511        }
3512        return this;
3513    },
3514
3515    // an internal helper for scaling according to options
3516    _scaleImage : function( image, options ) {
3517
3518        image = image || this._controls.getActive();
3519
3520        // janpub (JH) fix:
3521        // image might be unselected yet
3522        // e.g. when external logics rescales the gallery on window resize events
3523        if( !image ) {
3524            return;
3525        }
3526
3527        var self = this,
3528
3529            complete,
3530
3531            scaleLayer = function( img ) {
3532                $( img.container ).children(':first').css({
3533                    top: Math.max(0, Utils.parseValue( img.image.style.top )),
3534                    left: Math.max(0, Utils.parseValue( img.image.style.left )),
3535                    width: Utils.parseValue( img.image.width ),
3536                    height: Utils.parseValue( img.image.height )
3537                });
3538            };
3539
3540        options = $.extend({
3541            width:    this._stageWidth,
3542            height:   this._stageHeight,
3543            crop:     this._options.imageCrop,
3544            max:      this._options.maxScaleRatio,
3545            min:      this._options.minScaleRatio,
3546            margin:   this._options.imageMargin,
3547            position: this._options.imagePosition
3548        }, options );
3549
3550        if ( this._options.layerFollow && this._options.imageCrop !== true ) {
3551
3552            if ( typeof options.complete == 'function' ) {
3553                complete = options.complete;
3554                options.complete = function() {
3555                    complete.call( image, image );
3556                    scaleLayer( image );
3557                };
3558            } else {
3559                options.complete = scaleLayer;
3560            }
3561
3562        } else {
3563            $( image.container ).children(':first').css({ top: 0, left: 0 });
3564        }
3565
3566        image.scale( options );
3567        return this;
3568    },
3569
3570    /**
3571        Updates the carousel,
3572        useful if you resize the gallery and want to re-check if the carousel nav is needed.
3573
3574        @returns Instance
3575    */
3576
3577    updateCarousel : function() {
3578        this._carousel.update();
3579        return this;
3580    },
3581
3582    /**
3583        Rescales the gallery
3584
3585        @param {number} width The target width
3586        @param {number} height The target height
3587        @param {Function} complete The callback to be called when the scaling is complete
3588
3589        @returns Instance
3590    */
3591
3592    rescale : function( width, height, complete ) {
3593
3594        var self = this;
3595
3596        // allow rescale(fn)
3597        if ( typeof width === 'function' ) {
3598            complete = width;
3599            width = undef;
3600        }
3601
3602        var scale = function() {
3603
3604            // set stagewidth
3605            self._stageWidth = width || self.$( 'stage' ).width();
3606            self._stageHeight = height || self.$( 'stage' ).height();
3607
3608            // scale the active image
3609            self._scaleImage();
3610
3611            if ( self._options.carousel ) {
3612                self.updateCarousel();
3613            }
3614
3615            self.trigger( Galleria.RESCALE );
3616
3617            if ( typeof complete === 'function' ) {
3618                complete.call( self );
3619            }
3620        };
3621
3622        if ( Galleria.WEBKIT && !width && !height ) {
3623            Utils.addTimer( 'scale', scale, 10 );// webkit is too fast
3624        } else {
3625            scale.call( self );
3626        }
3627
3628        return this;
3629    },
3630
3631    /**
3632        Refreshes the gallery.
3633        Useful if you change image options at runtime and want to apply the changes to the active image.
3634
3635        @returns Instance
3636    */
3637
3638    refreshImage : function() {
3639        this._scaleImage();
3640        if ( this._options.imagePan ) {
3641            this.addPan();
3642        }
3643        return this;
3644    },
3645
3646    /**
3647        Shows an image by index
3648
3649        @param {number|boolean} index The index to show
3650        @param {Boolean} rewind A boolean that should be true if you want the transition to go back
3651
3652        @returns Instance
3653    */
3654
3655    show : function( index, rewind, _history ) {
3656
3657        // do nothing if index is false or queue is false and transition is in progress
3658        if ( index === false || ( !this._options.queue && this._queue.stalled ) ) {
3659            return;
3660        }
3661
3662        index = Math.max( 0, Math.min( parseInt( index, 10 ), this.getDataLength() - 1 ) );
3663
3664        rewind = typeof rewind !== 'undefined' ? !!rewind : index < this.getIndex();
3665
3666        _history = _history || false;
3667
3668        // do the history thing and return
3669        if ( !_history && Galleria.History ) {
3670            Galleria.History.set( index.toString() );
3671            return;
3672        }
3673
3674        this._active = index;
3675
3676        Array.prototype.push.call( this._queue, {
3677            index : index,
3678            rewind : rewind
3679        });
3680        if ( !this._queue.stalled ) {
3681            this._show();
3682        }
3683
3684        return this;
3685    },
3686
3687    // the internal _show method does the actual showing
3688    _show : function() {
3689
3690        // shortcuts
3691        var self = this,
3692            queue = this._queue[ 0 ],
3693            data = this.getData( queue.index );
3694
3695        if ( !data ) {
3696            return;
3697        }
3698
3699        var src = this.isFullscreen() && 'big' in data ? data.big : data.image, // use big image if fullscreen mode
3700            active = this._controls.getActive(),
3701            next = this._controls.getNext(),
3702            cached = next.isCached( src ),
3703            thumb = this._thumbnails[ queue.index ];
3704
3705        // to be fired when loading & transition is complete:
3706        var complete = (function( data, next, active, queue, thumb ) {
3707
3708            return function() {
3709
3710                var win;
3711
3712                // remove stalled
3713                self._queue.stalled = false;
3714
3715                // optimize quality
3716                Utils.toggleQuality( next.image, self._options.imageQuality );
3717
3718                // remove old layer
3719                self._layers[ self._controls.active ].innerHTML = '';
3720
3721                // swap
3722                $( active.container ).css({
3723                    zIndex: 0,
3724                    opacity: 0
3725                }).show();
3726
3727                $( next.container ).css({
3728                    zIndex: 1
3729                }).show();
3730
3731                self._controls.swap();
3732
3733                // add pan according to option
3734                if ( self._options.imagePan ) {
3735                    self.addPan( next.image );
3736                }
3737
3738                // make the image link or add lightbox
3739                // link takes precedence over lightbox if both are detected
3740                if ( data.link || self._options.lightbox ) {
3741
3742                    $( next.image ).css({
3743                        cursor: 'pointer'
3744                    }).bind( 'mouseup', function() {
3745
3746                        // popup link
3747                        if ( data.link ) {
3748                            if ( self._options.popupLinks ) {
3749                                win = window.open( data.link, '_blank' );
3750                            } else {
3751                                window.location.href = data.link;
3752                            }
3753                            return;
3754                        }
3755
3756                        self.openLightbox();
3757
3758                    });
3759                }
3760
3761                // remove the queued image
3762                Array.prototype.shift.call( self._queue );
3763
3764                // if we still have images in the queue, show it
3765                if ( self._queue.length ) {
3766                    self._show();
3767                }
3768
3769                // check if we are playing
3770                self._playCheck();
3771
3772                // trigger IMAGE event
3773                self.trigger({
3774                    type: Galleria.IMAGE,
3775                    index: queue.index,
3776                    imageTarget: next.image,
3777                    thumbTarget: thumb.image
3778                });
3779            };
3780        }( data, next, active, queue, thumb ));
3781
3782        // let the carousel follow
3783        if ( this._options.carousel && this._options.carouselFollow ) {
3784            this._carousel.follow( queue.index );
3785        }
3786
3787        // preload images
3788        if ( this._options.preload ) {
3789
3790            var p, i,
3791                n = this.getNext(),
3792                ndata;
3793
3794            try {
3795                for ( i = this._options.preload; i > 0; i-- ) {
3796                    p = new Galleria.Picture();
3797                    ndata = self.getData( n );
3798                    p.preload( this.isFullscreen() && 'big' in ndata ? ndata.big : ndata.image );
3799                    n = self.getNext( n );
3800                }
3801            } catch(e) {}
3802        }
3803
3804        // show the next image, just in case
3805        Utils.show( next.container );
3806
3807        // add active classes
3808        $( self._thumbnails[ queue.index ].container )
3809            .addClass( 'active' )
3810            .siblings( '.active' )
3811            .removeClass( 'active' );
3812
3813        // trigger the LOADSTART event
3814        self.trigger( {
3815            type: Galleria.LOADSTART,
3816            cached: cached,
3817            index: queue.index,
3818            rewind: queue.rewind,
3819            imageTarget: next.image,
3820            thumbTarget: thumb.image
3821        });
3822
3823        // begin loading the next image
3824        next.load( src, function( next ) {
3825
3826            // add layer HTML
3827            var layer = $( self._layers[ 1-self._controls.active ] ).html( data.layer || '' ).hide();
3828
3829            self._scaleImage( next, {
3830
3831                complete: function( next ) {
3832
3833                    // toggle low quality for IE
3834                    if ( 'image' in active ) {
3835                        Utils.toggleQuality( active.image, false );
3836                    }
3837                    Utils.toggleQuality( next.image, false );
3838
3839                    // stall the queue
3840                    self._queue.stalled = true;
3841
3842                    // remove the image panning, if applied
3843                    // TODO: rethink if this is necessary
3844                    self.removePan();
3845
3846                    // set the captions and counter
3847                    self.setInfo( queue.index );
3848                    self.setCounter( queue.index );
3849
3850                    // show the layer now
3851                    if ( data.layer ) {
3852                        layer.show();
3853                        // inherit click events set on image or stage
3854                        if ( data.link || self._options.clicknext ) {
3855                            layer.css( 'cursor', 'pointer' ).one( 'click', function() {
3856                                if ( data.link ) {
3857                                    $( next.image ).trigger( 'mouseup' );
3858                                } else {
3859                                    self.$( 'stage' ).trigger( 'click' );
3860                                }
3861                            });
3862                        }
3863                    }
3864
3865                    // trigger the LOADFINISH event
3866                    self.trigger({
3867                        type: Galleria.LOADFINISH,
3868                        cached: cached,
3869                        index: queue.index,
3870                        rewind: queue.rewind,
3871                        imageTarget: next.image,
3872                        thumbTarget: self._thumbnails[ queue.index ].image
3873                    });
3874
3875                    var transition = self._options.transition;
3876
3877                    // can JavaScript loop through objects in order? yes.
3878                    $.each({
3879                        initial: active.image === null,
3880                        touch: Galleria.TOUCH,
3881                        fullscreen: self.isFullscreen()
3882                    }, function( type, arg ) {
3883                        if ( arg && self._options[ type + 'Transition' ] !== undef ) {
3884                            transition = self._options[ type + 'Transition' ];
3885                            return false;
3886                        }
3887                    });
3888
3889                    // validate the transition
3890                    if ( transition in _transitions === false ) {
3891                        complete();
3892                    } else {
3893                        var params = {
3894                            prev: active.container,
3895                            next: next.container,
3896                            rewind: queue.rewind,
3897                            speed: self._options.transitionSpeed || 400
3898                        };
3899
3900                        // call the transition function and send some stuff
3901                        _transitions[ transition ].call(self, params, complete );
3902
3903                    }
3904                }
3905            });
3906        });
3907    },
3908
3909    /**
3910        Gets the next index
3911
3912        @param {number} base Optional starting point
3913
3914        @returns {number} the next index, or the first if you are at the first (looping)
3915    */
3916
3917    getNext : function( base ) {
3918        base = typeof base === 'number' ? base : this.getIndex();
3919        return base === this.getDataLength() - 1 ? 0 : base + 1;
3920    },
3921
3922    /**
3923        Gets the previous index
3924
3925        @param {number} base Optional starting point
3926
3927        @returns {number} the previous index, or the last if you are at the first (looping)
3928    */
3929
3930    getPrev : function( base ) {
3931        base = typeof base === 'number' ? base : this.getIndex();
3932        return base === 0 ? this.getDataLength() - 1 : base - 1;
3933    },
3934
3935    /**
3936        Shows the next image in line
3937
3938        @returns Instance
3939    */
3940
3941    next : function() {
3942        if ( this.getDataLength() > 1 ) {
3943            this.show( this.getNext(), false );
3944        }
3945        return this;
3946    },
3947
3948    /**
3949        Shows the previous image in line
3950
3951        @returns Instance
3952    */
3953
3954    prev : function() {
3955        if ( this.getDataLength() > 1 ) {
3956            this.show( this.getPrev(), true );
3957        }
3958        return this;
3959    },
3960
3961    /**
3962        Retrieve a DOM element by element ID
3963
3964        @param {string} elemId The delement ID to fetch
3965
3966        @returns {HTMLElement} The elements DOM node or null if not found.
3967    */
3968
3969    get : function( elemId ) {
3970        return elemId in this._dom ? this._dom[ elemId ] : null;
3971    },
3972
3973    /**
3974        Retrieve a data object
3975
3976        @param {number} index The data index to retrieve.
3977        If no index specified it will take the currently active image
3978
3979        @returns {Object} The data object
3980    */
3981
3982    getData : function( index ) {
3983        return index in this._data ?
3984            this._data[ index ] : this._data[ this._active ];
3985    },
3986
3987    /**
3988        Retrieve the number of data items
3989
3990        @returns {number} The data length
3991    */
3992    getDataLength : function() {
3993        return this._data.length;
3994    },
3995
3996    /**
3997        Retrieve the currently active index
3998
3999        @returns {number|boolean} The active index or false if none found
4000    */
4001
4002    getIndex : function() {
4003        return typeof this._active === 'number' ? this._active : false;
4004    },
4005
4006    /**
4007        Retrieve the stage height
4008
4009        @returns {number} The stage height
4010    */
4011
4012    getStageHeight : function() {
4013        return this._stageHeight;
4014    },
4015
4016    /**
4017        Retrieve the stage width
4018
4019        @returns {number} The stage width
4020    */
4021
4022    getStageWidth : function() {
4023        return this._stageWidth;
4024    },
4025
4026    /**
4027        Retrieve the option
4028
4029        @param {string} key The option key to retrieve. If no key specified it will return all options in an object.
4030
4031        @returns option or options
4032    */
4033
4034    getOptions : function( key ) {
4035        return typeof key === 'undefined' ? this._options : this._options[ key ];
4036    },
4037
4038    /**
4039        Set options to the instance.
4040        You can set options using a key & value argument or a single object argument (see examples)
4041
4042        @param {string} key The option key
4043        @param {string} value the the options value
4044
4045        @example setOptions( 'autoplay', true )
4046        @example setOptions({ autoplay: true });
4047
4048        @returns Instance
4049    */
4050
4051    setOptions : function( key, value ) {
4052        if ( typeof key === 'object' ) {
4053            $.extend( this._options, key );
4054        } else {
4055            this._options[ key ] = value;
4056        }
4057        return this;
4058    },
4059
4060    /**
4061        Starts playing the slideshow
4062
4063        @param {number} delay Sets the slideshow interval in milliseconds.
4064        If you set it once, you can just call play() and get the same interval the next time.
4065
4066        @returns Instance
4067    */
4068
4069    play : function( delay ) {
4070
4071        this._playing = true;
4072
4073        this._playtime = delay || this._playtime;
4074
4075        this._playCheck();
4076
4077        this.trigger( Galleria.PLAY );
4078
4079        return this;
4080    },
4081
4082    /**
4083        Stops the slideshow if currently playing
4084
4085        @returns Instance
4086    */
4087
4088    pause : function() {
4089
4090        this._playing = false;
4091
4092        this.trigger( Galleria.PAUSE );
4093
4094        return this;
4095    },
4096
4097    /**
4098        Toggle between play and pause events.
4099
4100        @param {number} delay Sets the slideshow interval in milliseconds.
4101
4102        @returns Instance
4103    */
4104
4105    playToggle : function( delay ) {
4106        return ( this._playing ) ? this.pause() : this.play( delay );
4107    },
4108
4109    /**
4110        Checks if the gallery is currently playing
4111
4112        @returns {Boolean}
4113    */
4114
4115    isPlaying : function() {
4116        return this._playing;
4117    },
4118
4119    /**
4120        Checks if the gallery is currently in fullscreen mode
4121
4122        @returns {Boolean}
4123    */
4124
4125    isFullscreen : function() {
4126        return this._fullscreen.active;
4127    },
4128
4129    _playCheck : function() {
4130        var self = this,
4131            played = 0,
4132            interval = 20,
4133            now = Utils.timestamp(),
4134            timer_id = 'play' + this._id;
4135
4136        if ( this._playing ) {
4137
4138            Utils.clearTimer( timer_id );
4139
4140            var fn = function() {
4141
4142                played = Utils.timestamp() - now;
4143                if ( played >= self._playtime && self._playing ) {
4144                    Utils.clearTimer( timer_id );
4145                    self.next();
4146                    return;
4147                }
4148                if ( self._playing ) {
4149
4150                    // trigger the PROGRESS event
4151                    self.trigger({
4152                        type:         Galleria.PROGRESS,
4153                        percent:      Math.ceil( played / self._playtime * 100 ),
4154                        seconds:      Math.floor( played / 1000 ),
4155                        milliseconds: played
4156                    });
4157
4158                    Utils.addTimer( timer_id, fn, interval );
4159                }
4160            };
4161            Utils.addTimer( timer_id, fn, interval );
4162        }
4163    },
4164
4165    /**
4166        Modify the slideshow delay
4167
4168        @param {number} delay the number of milliseconds between slides,
4169
4170        @returns Instance
4171    */
4172
4173    setPlaytime: function( delay ) {
4174        this._playtime = delay;
4175        return this;
4176    },
4177
4178    setIndex: function( val ) {
4179        this._active = val;
4180        return this;
4181    },
4182
4183    /**
4184        Manually modify the counter
4185
4186        @param {number} index Optional data index to fectch,
4187        if no index found it assumes the currently active index
4188
4189        @returns Instance
4190    */
4191
4192    setCounter: function( index ) {
4193
4194        if ( typeof index === 'number' ) {
4195            index++;
4196        } else if ( typeof index === 'undefined' ) {
4197            index = this.getIndex()+1;
4198        }
4199
4200        this.get( 'current' ).innerHTML = index;
4201
4202        if ( IE ) { // weird IE bug
4203
4204            var count = this.$( 'counter' ),
4205                opacity = count.css( 'opacity' );
4206
4207            if ( parseInt( opacity, 10 ) === 1) {
4208                Utils.removeAlpha( count[0] );
4209            } else {
4210                this.$( 'counter' ).css( 'opacity', opacity );
4211            }
4212
4213        }
4214
4215        return this;
4216    },
4217
4218    /**
4219        Manually set captions
4220
4221        @param {number} index Optional data index to fectch and apply as caption,
4222        if no index found it assumes the currently active index
4223
4224        @returns Instance
4225    */
4226
4227    setInfo : function( index ) {
4228
4229        var self = this,
4230            data = this.getData( index );
4231
4232        $.each( ['title','description'], function( i, type ) {
4233
4234            var elem = self.$( 'info-' + type );
4235
4236            if ( !!data[type] ) {
4237                elem[ data[ type ].length ? 'show' : 'hide' ]().html( data[ type ] );
4238            } else {
4239               elem.empty().hide();
4240            }
4241        });
4242
4243        return this;
4244    },
4245
4246    /**
4247        Checks if the data contains any captions
4248
4249        @param {number} index Optional data index to fectch,
4250        if no index found it assumes the currently active index.
4251
4252        @returns {boolean}
4253    */
4254
4255    hasInfo : function( index ) {
4256
4257        var check = 'title description'.split(' '),
4258            i;
4259
4260        for ( i = 0; check[i]; i++ ) {
4261            if ( !!this.getData( index )[ check[i] ] ) {
4262                return true;
4263            }
4264        }
4265        return false;
4266
4267    },
4268
4269    jQuery : function( str ) {
4270
4271        var self = this,
4272            ret = [];
4273
4274        $.each( str.split(','), function( i, elemId ) {
4275            elemId = $.trim( elemId );
4276
4277            if ( self.get( elemId ) ) {
4278                ret.push( elemId );
4279            }
4280        });
4281
4282        var jQ = $( self.get( ret.shift() ) );
4283
4284        $.each( ret, function( i, elemId ) {
4285            jQ = jQ.add( self.get( elemId ) );
4286        });
4287
4288        return jQ;
4289
4290    },
4291
4292    /**
4293        Converts element IDs into a jQuery collection
4294        You can call for multiple IDs separated with commas.
4295
4296        @param {string} str One or more element IDs (comma-separated)
4297
4298        @returns jQuery
4299
4300        @example this.$('info,container').hide();
4301    */
4302
4303    $ : function( str ) {
4304        return this.jQuery.apply( this, Utils.array( arguments ) );
4305    }
4306
4307};
4308
4309// End of Galleria prototype
4310
4311// Add events as static variables
4312$.each( _events, function( i, ev ) {
4313
4314    // legacy events
4315    var type = /_/.test( ev ) ? ev.replace( /_/g, '' ) : ev;
4316
4317    Galleria[ ev.toUpperCase() ] = 'galleria.'+type;
4318
4319} );
4320
4321$.extend( Galleria, {
4322
4323    // Browser helpers
4324    IE9:     IE === 9,
4325    IE8:     IE === 8,
4326    IE7:     IE === 7,
4327    IE6:     IE === 6,
4328    IE:      IE,
4329    WEBKIT:  /webkit/.test( NAV ),
4330    SAFARI:  /safari/.test( NAV ),
4331    CHROME:  /chrome/.test( NAV ),
4332    QUIRK:   ( IE && doc.compatMode && doc.compatMode === "BackCompat" ),
4333    MAC:     /mac/.test( navigator.platform.toLowerCase() ),
4334    OPERA:   !!window.opera,
4335    IPHONE:  /iphone/.test( NAV ),
4336    IPAD:    /ipad/.test( NAV ),
4337    ANDROID: /android/.test( NAV ),
4338    TOUCH:   ('ontouchstart' in doc)
4339
4340});
4341
4342// Galleria static methods
4343
4344/**
4345    Adds a theme that you can use for your Gallery
4346
4347    @param {Object} theme Object that should contain all your theme settings.
4348    <ul>
4349        <li>name - name of the theme</li>
4350        <li>author - name of the author</li>
4351        <li>css - css file name (not path)</li>
4352        <li>defaults - default options to apply, including theme-specific options</li>
4353        <li>init - the init function</li>
4354    </ul>
4355
4356    @returns {Object} theme
4357*/
4358
4359Galleria.addTheme = function( theme ) {
4360
4361    // make sure we have a name
4362    if ( !theme.name ) {
4363        Galleria.raise('No theme name specified');
4364    }
4365
4366    if ( typeof theme.defaults !== 'object' ) {
4367        theme.defaults = {};
4368    } else {
4369        theme.defaults = _legacyOptions( theme.defaults );
4370    }
4371
4372    var css = false,
4373        reg;
4374
4375    if ( typeof theme.css === 'string' ) {
4376
4377        // look for manually added CSS
4378        $('link').each(function( i, link ) {
4379            reg = new RegExp( theme.css );
4380            if ( reg.test( link.href ) ) {
4381
4382                // we found the css
4383                css = true;
4384
4385                // the themeload trigger
4386                _themeLoad( theme );
4387
4388                return false;
4389            }
4390        });
4391
4392        // else look for the absolute path and load the CSS dynamic
4393        if ( !css ) {
4394
4395            $('script').each(function( i, script ) {
4396
4397                // look for the theme script
4398                reg = new RegExp( 'galleria\\.' + theme.name.toLowerCase() + '\\.' );
4399                if( reg.test( script.src )) {
4400
4401                    // we have a match
4402                    css = script.src.replace(/[^\/]*$/, '') + theme.css;
4403
4404                    Utils.addTimer( "css", function() {
4405                        Utils.loadCSS( css, 'galleria-theme', function() {
4406
4407                            // the themeload trigger
4408                            _themeLoad( theme );
4409
4410                        });
4411                    }, 1);
4412
4413                }
4414            });
4415        }
4416
4417        if ( !css ) {
4418            Galleria.raise('No theme CSS loaded');
4419        }
4420    } else {
4421
4422        // pass
4423        _themeLoad( theme );
4424    }
4425    return theme;
4426};
4427
4428/**
4429    loadTheme loads a theme js file and attaches a load event to Galleria
4430
4431    @param {string} src The relative path to the theme source file
4432
4433    @param {Object} [options] Optional options you want to apply
4434*/
4435
4436Galleria.loadTheme = function( src, options ) {
4437
4438    var loaded = false,
4439        length = _galleries.length,
4440        err = window.setTimeout( function() {
4441            Galleria.raise( "Theme at " + src + " could not load, check theme path.", true );
4442        }, 5000 );
4443
4444    // first clear the current theme, if exists
4445    Galleria.theme = undef;
4446
4447    // load the theme
4448    Utils.loadScript( src, function() {
4449
4450        window.clearTimeout( err );
4451
4452        // check for existing galleries and reload them with the new theme
4453        if ( length ) {
4454
4455            // temporary save the new galleries
4456            var refreshed = [];
4457
4458            // refresh all instances
4459            // when adding a new theme to an existing gallery, all options will be resetted but the data will be kept
4460            // you can apply new options as a second argument
4461            $.each( Galleria.get(), function(i, instance) {
4462
4463                // mix the old data and options into the new instance
4464                var op = $.extend( instance._original.options, {
4465                    data_source: instance._data
4466                }, options);
4467
4468                // remove the old container
4469                instance.$('container').remove();
4470
4471                // create a new instance
4472                var g = new Galleria();
4473
4474                // move the id
4475                g._id = instance._id;
4476
4477                // initialize the new instance
4478                g.init( instance._original.target, op );
4479
4480                // push the new instance
4481                refreshed.push( g );
4482            });
4483
4484            // now overwrite the old holder with the new instances
4485            _galleries = refreshed;
4486        }
4487
4488    });
4489};
4490
4491/**
4492    Retrieves a Galleria instance.
4493
4494    @param {number} [index] Optional index to retrieve.
4495    If no index is supplied, the method will return all instances in an array.
4496
4497    @returns Instance or Array of instances
4498*/
4499
4500Galleria.get = function( index ) {
4501    if ( !!_instances[ index ] ) {
4502        return _instances[ index ];
4503    } else if ( typeof index !== 'number' ) {
4504        return _instances;
4505    } else {
4506        Galleria.raise('Gallery index ' + index + ' not found');
4507    }
4508};
4509
4510/**
4511    Creates a transition to be used in your gallery
4512
4513    @param {string} name The name of the transition that you will use as an option
4514
4515    @param {Function} fn The function to be executed in the transition.
4516    The function contains two arguments, params and complete.
4517    Use the params Object to integrate the transition, and then call complete when you are done.
4518
4519*/
4520
4521Galleria.addTransition = function( name, fn ) {
4522    _transitions[name] = fn;
4523};
4524
4525/**
4526    The Galleria utilites
4527*/
4528
4529Galleria.utils = Utils;
4530
4531/**
4532    A helper metod for cross-browser logging.
4533    It uses the console log if available otherwise it falls back to alert
4534
4535    @example Galleria.log("hello", document.body, [1,2,3]);
4536*/
4537
4538Galleria.log = (function() {
4539    if( 'console' in window && 'log' in window.console ) {
4540        return window.console.log;
4541    } else {
4542        return function() {
4543            window.alert( Utils.array( arguments ).join(', ') );
4544        };
4545    }
4546}());
4547
4548/**
4549    A ready method for adding callbacks when a gallery is ready
4550    Each method is call before the extend option for every instance
4551
4552    @param {function} callback The function to call
4553*/
4554
4555Galleria.ready = function( fn ) {
4556    $.each( _galleries, function( i, gallery ) {
4557        fn.call( gallery, gallery._options );
4558    });
4559    Galleria.ready.callbacks.push( fn );
4560};
4561
4562Galleria.ready.callbacks = [];
4563
4564/**
4565    Method for raising errors
4566
4567    @param {string} msg The message to throw
4568
4569    @param {boolean} [fatal] Set this to true to override debug settings and display a fatal error
4570*/
4571
4572Galleria.raise = function( msg, fatal ) {
4573
4574    var type = fatal ? 'Fatal error' : 'Error',
4575
4576        self = this,
4577
4578        echo = function( msg ) {
4579
4580            var html = '<div style="padding:4px;margin:0 0 2px;background:#' +
4581                ( fatal ? '811' : '222' ) + '";>' +
4582                ( fatal ? '<strong>' + type + ': </strong>' : '' ) +
4583                msg + '</div>';
4584
4585            $.each( _instances, function() {
4586
4587                var cont = this.$( 'errors' ),
4588                    target = this.$( 'target' );
4589
4590                if ( !cont.length ) {
4591
4592                    target.css( 'position', 'relative' );
4593
4594                    cont = this.addElement( 'errors' ).appendChild( 'target', 'errors' ).$( 'errors' ).css({
4595                        color: '#fff',
4596                        position: 'absolute',
4597                        top: 0,
4598                        left: 0,
4599                        zIndex: 100000
4600                    });
4601                }
4602
4603                cont.append( html );
4604
4605            });
4606        };
4607
4608    // if debug is on, display errors and throw exception if fatal
4609    if ( DEBUG ) {
4610        echo( msg );
4611        if ( fatal ) {
4612            throw new Error(type + ': ' + msg);
4613        }
4614
4615    // else just echo a silent generic error if fatal
4616    } else if ( fatal ) {
4617        if ( _hasError ) {
4618            return;
4619        }
4620        _hasError = true;
4621        fatal = false;
4622        echo( 'Image gallery could not load.' );
4623    }
4624};
4625
4626// Add the version
4627Galleria.version = VERSION;
4628
4629/**
4630    A method for checking what version of Galleria the user has installed and throws a readable error if the user needs to upgrade.
4631    Useful when building plugins that requires a certain version to function.
4632
4633    @param {number} version The minimum version required
4634
4635    @param {string} [msg] Optional message to display. If not specified, Galleria will throw a generic error.
4636*/
4637
4638Galleria.requires = function( version, msg ) {
4639    msg = msg || 'You need to upgrade Galleria to version ' + version + ' to use one or more components.';
4640    if ( Galleria.version < version ) {
4641        Galleria.raise(msg, true);
4642    }
4643};
4644
4645/**
4646    Adds preload, cache, scale and crop functionality
4647
4648    @constructor
4649
4650    @requires jQuery
4651
4652    @param {number} [id] Optional id to keep track of instances
4653*/
4654
4655Galleria.Picture = function( id ) {
4656
4657    // save the id
4658    this.id = id || null;
4659
4660    // the image should be null until loaded
4661    this.image = null;
4662
4663    // Create a new container
4664    this.container = Utils.create('galleria-image');
4665
4666    // add container styles
4667    $( this.container ).css({
4668        overflow: 'hidden',
4669        position: 'relative' // for IE Standards mode
4670    });
4671
4672    // saves the original measurements
4673    this.original = {
4674        width: 0,
4675        height: 0
4676    };
4677
4678    // flag when the image is ready
4679    this.ready = false;
4680
4681    // placeholder for the timeout
4682    this.tid = null;
4683
4684};
4685
4686Galleria.Picture.prototype = {
4687
4688    // the inherited cache object
4689    cache: {},
4690
4691    // show the image on stage
4692    show: function() {
4693        Utils.show( this.image );
4694    },
4695
4696    // hide the image
4697    hide: function() {
4698        Utils.moveOut( this.image );
4699    },
4700
4701    clear: function() {
4702        this.image = null;
4703    },
4704
4705    /**
4706        Checks if an image is in cache
4707
4708        @param {string} src The image source path, ex '/path/to/img.jpg'
4709
4710        @returns {boolean}
4711    */
4712
4713    isCached: function( src ) {
4714        return !!this.cache[src];
4715    },
4716
4717    /**
4718        Preloads an image into the cache
4719
4720        @param {string} src The image source path, ex '/path/to/img.jpg'
4721
4722        @returns Galleria.Picture
4723    */
4724
4725    preload: function( src ) {
4726        $( new Image() ).load((function(src, cache) {
4727            return function() {
4728                cache[ src ] = src;
4729            };
4730        }( src, this.cache ))).attr( 'src', src );
4731    },
4732
4733    /**
4734        Loads an image and call the callback when ready.
4735        Will also add the image to cache.
4736
4737        @param {string} src The image source path, ex '/path/to/img.jpg'
4738        @param {Function} callback The function to be executed when the image is loaded & scaled
4739
4740        @returns The image container (jQuery object)
4741    */
4742
4743    load: function(src, callback) {
4744
4745        // set a load timeout for debugging
4746        this.tid = window.setTimeout( (function(src) {
4747            return function() {
4748                Galleria.raise('Image not loaded in ' + Math.round( TIMEOUT/1000 ) + ' seconds: '+ src);
4749            };
4750        }( src )), TIMEOUT );
4751
4752        this.image = new Image();
4753
4754        var i = 0,
4755            reload = false,
4756
4757            // some jquery cache
4758            $container = $( this.container ),
4759            $image = $( this.image ),
4760
4761            // the onload method
4762            onload = (function( self, callback, src ) {
4763
4764                return function() {
4765
4766                    var complete = function() {
4767
4768                        $( this ).unbind( 'load' );
4769
4770                        // save the original size
4771                        self.original = {
4772                            height: this.height,
4773                            width: this.width
4774                        };
4775
4776                        self.cache[ src ] = src; // will override old cache
4777
4778                        // clear the debug timeout
4779                        window.clearTimeout( self.tid );
4780
4781                        if (typeof callback == 'function' ) {
4782                            window.setTimeout(function() {
4783                                callback.call( self, self );
4784                            },1);
4785                        }
4786                    };
4787
4788                    // Delay the callback to "fix" the Adblock Bug
4789                    // http://code.google.com/p/adblockforchrome/issues/detail?id=3701
4790                    if ( ( !this.width || !this.height ) ) {
4791                        window.setTimeout( (function( img ) {
4792                            return function() {
4793                                if ( img.width && img.height ) {
4794                                    complete.call( img );
4795                                } else {
4796                                    Galleria.raise('Could not extract width/height from image: ' + img.src +
4797                                        '. Traced measures: width:' + img.width + 'px, height: ' + img.height + 'px.');
4798                                }
4799                            };
4800                        }( this )), 2);
4801                    } else {
4802                        complete.call( this );
4803                    }
4804                };
4805            }( this, callback, src ));
4806
4807        // remove any previous images
4808        $container.find( 'img' ).remove();
4809
4810        // append the image
4811        $image.css( 'display', 'block').appendTo( this.container );
4812
4813        // hide it for now
4814        Utils.hide( this.image );
4815
4816        if ( this.cache[ src ] ) {
4817
4818            // quick load on cache
4819            $( this.image ).load( onload ).attr( 'src', src );
4820
4821            return this.container;
4822        }
4823
4824        // begin load and insert in cache when done
4825        $( this.image ).load( onload ).error( function() {
4826            if ( !reload ) {
4827                reload = true;
4828                // reload the image with a timestamp
4829                window.setTimeout((function(image, src) {
4830                    return function() {
4831                        image.attr('src', src + '?' + Utils.timestamp() );
4832                    };
4833                }( $(this), src )), 50);
4834            } else {
4835                // apply the dummy image if it exists
4836                if ( DUMMY ) {
4837                    $( this ).attr( 'src', DUMMY );
4838                } else {
4839                    Galleria.raise('Image not found: ' + src);
4840                }
4841            }
4842        }).attr( 'src', src );
4843
4844        // return the container
4845        return this.container;
4846    },
4847
4848    /**
4849        Scales and crops the image
4850
4851        @param {Object} options The method takes an object with a number of options:
4852
4853        <ul>
4854            <li>width - width of the container</li>
4855            <li>height - height of the container</li>
4856            <li>min - minimum scale ratio</li>
4857            <li>max - maximum scale ratio</li>
4858            <li>margin - distance in pixels from the image border to the container</li>
4859            <li>complete - a callback that fires when scaling is complete</li>
4860            <li>position - positions the image, works like the css background-image property.</li>
4861            <li>crop - defines how to crop. Can be true, false, 'width' or 'height'</li>
4862            <li>canvas - set to true to try a canvas-based rescale</li>
4863        </ul>
4864
4865        @returns The image container object (jQuery)
4866    */
4867
4868    scale: function( options ) {
4869
4870        // extend some defaults
4871        options = $.extend({
4872            width: 0,
4873            height: 0,
4874            min: undef,
4875            max: undef,
4876            margin: 0,
4877            complete: function() {},
4878            position: 'center',
4879            crop: false,
4880            canvas: false
4881        }, options);
4882
4883        // return the element if no image found
4884        if (!this.image) {
4885            return this.container;
4886        }
4887
4888        // store locale variables
4889        var width,
4890            height,
4891            self = this,
4892            $container = $( self.container ),
4893            data;
4894
4895        // wait for the width/height
4896        Utils.wait({
4897            until: function() {
4898                width  = options.width ||
4899                         $container.width() ||
4900                         Utils.parseValue( $container.css('width') );
4901
4902                height = options.height ||
4903                         $container.height() ||
4904                         Utils.parseValue( $container.css('height') );
4905
4906                return width && height;
4907            },
4908            success: function() {
4909                // calculate some cropping
4910                var newWidth = ( width - options.margin * 2 ) / self.original.width,
4911                    newHeight = ( height - options.margin * 2 ) / self.original.height,
4912                    cropMap = {
4913                        'true'  : Math.max( newWidth, newHeight ),
4914                        'width' : newWidth,
4915                        'height': newHeight,
4916                        'false' : Math.min( newWidth, newHeight )
4917                    },
4918                    ratio = cropMap[ options.crop.toString() ],
4919                    canvasKey = '';
4920
4921                // allow max_scale_ratio
4922                if ( options.max ) {
4923                    ratio = Math.min( options.max, ratio );
4924                }
4925
4926                // allow min_scale_ratio
4927                if ( options.min ) {
4928                    ratio = Math.max( options.min, ratio );
4929                }
4930
4931                $.each( ['width','height'], function( i, m ) {
4932                    $( self.image )[ m ]( self[ m ] = self.image[ m ] = Math.round( self.original[ m ] * ratio ) );
4933                });
4934
4935                $( self.container ).width( width ).height( height );
4936
4937                if ( options.canvas && _canvas ) {
4938
4939                    _canvas.elem.width = self.width;
4940                    _canvas.elem.height = self.height;
4941
4942                    canvasKey = self.image.src + ':' + self.width + 'x' + self.height;
4943
4944                    self.image.src = _canvas.cache[ canvasKey ] || (function( key ) {
4945
4946                        _canvas.context.drawImage(self.image, 0, 0, self.original.width*ratio, self.original.height*ratio);
4947
4948                        try {
4949
4950                            data = _canvas.elem.toDataURL();
4951                            _canvas.length += data.length;
4952                            _canvas.cache[ key ] = data;
4953                            return data;
4954
4955                        } catch( e ) {
4956                            return self.image.src;
4957                        }
4958
4959                    }( canvasKey ) );
4960
4961                }
4962
4963                // calculate image_position
4964                var pos = {},
4965                    mix = {},
4966                    getPosition = function(value, measure, margin) {
4967                        var result = 0;
4968                        if (/\%/.test(value)) {
4969                            var flt = parseInt( value, 10 ) / 100,
4970                                m = self.image[ measure ] || $( self.image )[ measure ]();
4971
4972                            result = Math.ceil( m * -1 * flt + margin * flt );
4973                        } else {
4974                            result = Utils.parseValue( value );
4975                        }
4976                        return result;
4977                    },
4978                    positionMap = {
4979                        'top': { top: 0 },
4980                        'left': { left: 0 },
4981                        'right': { left: '100%' },
4982                        'bottom': { top: '100%' }
4983                    };
4984
4985                $.each( options.position.toLowerCase().split(' '), function( i, value ) {
4986                    if ( value === 'center' ) {
4987                        value = '50%';
4988                    }
4989                    pos[i ? 'top' : 'left'] = value;
4990                });
4991
4992                $.each( pos, function( i, value ) {
4993                    if ( positionMap.hasOwnProperty( value ) ) {
4994                        $.extend( mix, positionMap[ value ] );
4995                    }
4996                });
4997
4998                pos = pos.top ? $.extend( pos, mix ) : mix;
4999
5000                pos = $.extend({
5001                    top: '50%',
5002                    left: '50%'
5003                }, pos);
5004
5005                // apply position
5006                $( self.image ).css({
5007                    position : 'absolute',
5008                    top :  getPosition(pos.top, 'height', height),
5009                    left : getPosition(pos.left, 'width', width)
5010                });
5011
5012                // show the image
5013                self.show();
5014
5015                // flag ready and call the callback
5016                self.ready = true;
5017                options.complete.call( self, self );
5018
5019            },
5020            error: function() {
5021                Galleria.raise('Could not scale image: '+self.image.src);
5022            },
5023            timeout: 1000
5024        });
5025        return this;
5026    }
5027};
5028
5029// our own easings
5030$.extend( $.easing, {
5031
5032    galleria: function (_, t, b, c, d) {
5033        if ((t/=d/2) < 1) {
5034            return c/2*t*t*t + b;
5035        }
5036        return c/2*((t-=2)*t*t + 2) + b;
5037    },
5038
5039    galleriaIn: function (_, t, b, c, d) {
5040        return c*(t/=d)*t + b;
5041    },
5042
5043    galleriaOut: function (_, t, b, c, d) {
5044        return -c *(t/=d)*(t-2) + b;
5045    }
5046
5047});
5048
5049// the plugin initializer
5050$.fn.galleria = function( options ) {
5051
5052    return this.each(function() {
5053        $( this ).data( 'galleria', new Galleria().init( this, options ) );
5054    });
5055
5056};
5057
5058// phew
5059
5060}( jQuery ) );
Note: See TracBrowser for help on using the repository browser.