1 /*
  2  * Galleria v 1.2 prerelease 1.1 2010-10-29
  3  * http://galleria.aino.se
  4  *
  5  * Copyright (c) 2010, Aino
  6  * Licensed under the MIT license.
  7  */
  8 
  9 (function($) {
 10 
 11 // some references
 12 var undef,
 13     window = this,
 14     doc    = document,
 15     $doc   = $( doc );
 16 
 17 // internal constants
 18 var DEBUG = false,
 19     NAV   = navigator.userAgent.toLowerCase(),
 20     HASH  = window.location.hash.replace(/#\//, ''),
 21     CLICK = function() {
 22         // use this to make touch devices snappier
 23         return Galleria.TOUCH ? 'touchstart' : 'click';
 24     },
 25     IE    = (function() {
 26         var v = 3,
 27             div = doc.createElement( 'div' );
 28         while (
 29             div.innerHTML = '<!--[if gt IE '+(++v)+']><i></i><![endif]-->',
 30             div.getElementsByTagName('i')[0]
 31         );
 32         return v > 4 ? v : undef;
 33     }() ),
 34     DOM   = function() {
 35         return {
 36             html:  doc.documentElement,
 37             body:  doc.body,
 38             head:  doc.getElementsByTagName('head')[0],
 39             title: doc.title
 40         };
 41     },
 42 
 43     // the internal timeouts object
 44     // provides helper methods for controlling timeouts
 45     _timeouts = {
 46 
 47         trunk: {},
 48 
 49         add: function( id, fn, delay, loop ) {
 50             loop = loop || false;
 51             this.clear( id );
 52             if ( loop ) {
 53                 var old = fn;
 54                 fn = function() {
 55                     old();
 56                     _timeouts.add( id, fn, delay );
 57                 };
 58             }
 59             this.trunk[ id ] = window.setTimeout( fn, delay );
 60         },
 61 
 62         clear: function( id ) {
 63 
 64             var del = function( i ) {
 65                 window.clearTimeout( this.trunk[ i ] );
 66                 delete this.trunk[ i ];
 67             };
 68 
 69             if ( !!id && id in this.trunk ) {
 70                 del.call( _timeouts, id );
 71 
 72             } else if ( typeof id == 'undefined' ) {
 73                 for ( var i in this.trunk ) {
 74                     del.call( _timeouts, i );
 75                 }
 76             }
 77         }
 78     },
 79 
 80     // the internal gallery holder
 81     _galleries = [],
 82 
 83     // the transitions holder
 84     _transitions = {
 85 
 86         fade: function(params, complete) {
 87             $(params.next).css('opacity', 0).show().animate({
 88                 opacity: 1
 89             }, params.speed, complete);
 90 
 91             if (params.prev) {
 92                 $(params.prev).css('opacity', 1).show().animate({
 93                     opacity: 0
 94                 }, params.speed);
 95             }
 96         },
 97 
 98         flash: function(params, complete) {
 99             $(params.next).css('opacity', 0);
100             if (params.prev) {
101                 $(params.prev).animate({
102                     opacity: 0
103                 }, (params.speed / 2), function() {
104                     $(params.next).animate({
105                         opacity: 1
106                     }, params.speed, complete);
107                 });
108             } else {
109                 $(params.next).animate({
110                     opacity: 1
111                 }, params.speed, complete);
112             }
113         },
114 
115         pulse: function(params, complete) {
116             if (params.prev) {
117                 $(params.prev).hide();
118             }
119             $(params.next).css('opacity', 0).animate({
120                 opacity:1
121             }, params.speed, complete);
122         },
123 
124         slide: function(params, complete) {
125             var image  = $(params.next).parent(),
126                 images = this.$('images'), // ??
127                 width  = this._stageWidth,
128                 easing = this.getOptions( 'easing' );
129 
130             image.css({
131                 left: width * ( params.rewind ? -1 : 1 )
132             });
133             images.animate({
134                 left: width * ( params.rewind ? 1 : -1 )
135             }, {
136                 duration: params.speed,
137                 queue: false,
138                 easing: easing,
139                 complete: function() {
140                     images.css('left', 0);
141                     image.css('left', 0);
142                     complete();
143                 }
144             });
145         },
146 
147         fadeslide: function(params, complete) {
148 
149             var x = 0,
150                 easing = this.getOptions('easing');
151 
152             if (params.prev) {
153                 x = Utils.parseValue( $(params.prev).css('left') );
154                 $(params.prev).css({
155                     opacity: 1,
156                     left: x
157                 }).animate({
158                     opacity: 0,
159                     left: x + ( 50 * ( params.rewind ? 1 : -1 ) )
160                 },{
161                     duration: params.speed,
162                     queue: false,
163                     easing: easing
164                 });
165             }
166 
167             x = Utils.parseValue( $(params.next).css('left') );
168 
169             $(params.next).css({
170                 left: x + ( 50 * ( params.rewind ? -1 : 1 ) ),
171                 opacity: 0
172             }).animate({
173                 opacity: 1,
174                 left: x
175             }, {
176                 duration: params.speed,
177                 complete: complete,
178                 queue: false,
179                 easing: easing
180             });
181         }
182     },
183 
184     // the Utils singleton
185     Utils = (function() {
186 
187         return {
188 
189             array : function( obj ) {
190                 return Array.prototype.slice.call(obj);
191             },
192 
193             create : function( className, nodeName ) {
194                 nodeName = nodeName || 'div';
195                 var elem = doc.createElement( nodeName );
196                 elem.className = className;
197                 return elem;
198             },
199 
200             forceStyles : function( elem, styles ) {
201                 elem = $(elem);
202                 if ( elem.attr( 'style' ) ) {
203                     elem.data( 'styles', elem.attr( 'style' ) ).removeAttr( 'style' );
204                 }
205                 elem.css( styles );
206             },
207 
208             revertStyles : function() {
209                 $.each( Utils.array( arguments ), function( i, elem ) {
210 
211                     elem = $( elem ).removeAttr( 'style' );
212 
213                     if ( elem.data( 'styles' ) ) {
214                         elem.attr( 'style', elem.data('styles') ).data( 'styles', null );
215                     }
216                 });
217             },
218 
219             moveOut : function( elem ) {
220                 Utils.forceStyles( elem, {
221                     position: 'absolute',
222                     left: -10000
223                 });
224             },
225 
226             moveIn : function() {
227                 Utils.revertStyles.apply( Utils, Utils.array( arguments ) );
228             },
229 
230             hide : function( elem, speed, callback ) {
231                 elem = $(elem);
232 
233                 // save the value if not exist
234                 if (! elem.data('opacity') ) {
235                     elem.data('opacity', elem.css('opacity') );
236                 }
237 
238                 // always hide
239                 var style = { opacity: 0 };
240 
241                 if (speed) {
242                     elem.stop().animate( style, speed, callback );
243                 } else {
244                     elem.css( style );
245                 };
246             },
247 
248             show : function( elem, speed, callback ) {
249                 elem = $(elem);
250 
251                 // bring back saved opacity
252                 var saved = parseFloat( elem.data('opacity') ) || 1,
253                     style = { opacity: saved };
254 
255                 // reset save if opacity == 1
256                 if (saved == 1) {
257                     elem.data('opacity', null);
258                 }
259 
260                 // animate or toggle
261                 if (speed) {
262                     elem.stop().animate( style, speed, callback );
263                 } else {
264                     elem.css( style );
265                 };
266             },
267 
268             addTimer : function() {
269                 _timeouts.add.apply( _timeouts, Utils.array( arguments ) );
270                 return this;
271             },
272 
273             clearTimer : function() {
274                 _timeouts.clear.apply( _timeouts, Utils.array( arguments ) );
275                 return this;
276             },
277 
278             wait : function(options) {
279                 options = $.extend({
280                     until : function() { return false; },
281                     success : function() {},
282                     error : function() { Galleria.raise('Could not complete wait function.'); },
283                     timeout: 3000
284                 }, options);
285 
286                 var start = Utils.timestamp(),
287                     elapsed,
288                     now;
289 
290                 window.setTimeout(function() {
291                     now = Utils.timestamp();
292                     elapsed = now - start;
293                     if ( options.until( elapsed ) ) {
294                         options.success();
295                         return false;
296                     }
297 
298                     if (now >= start + options.timeout) {
299                         options.error();
300                         return false;
301                     }
302                     window.setTimeout(arguments.callee, 2);
303                 }, 2);
304             },
305 
306             toggleQuality : function( img, force ) {
307 
308                 if ( !( IE == 7 || IE == 8 ) || !!img === false ) {
309                     return;
310                 }
311 
312                 if ( typeof force === 'undefined' ) {
313                     force = img.style.msInterpolationMode == 'nearest-neighbor';
314                 }
315 
316                 img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
317             },
318 
319             insertStyleTag : function( styles ) {
320                 var style = doc.createElement( 'style' );
321                 DOM().head.appendChild( style );
322 
323                 if ( style.styleSheet ) { // IE
324                     style.styleSheet.cssText = styles;
325                 } else {
326                     var cssText = doc.createTextNode( styles );
327                     style.appendChild( cssText );
328                 }
329             },
330 
331             // a loadscript method that works for local scripts
332             loadScript: function( url, callback ) {
333                 var done = false,
334                     script = $('<scr'+'ipt>').attr({
335                         src: url,
336                         async: true
337                     }).get(0);
338 
339                // Attach handlers for all browsers
340                script.onload = script.onreadystatechange = function() {
341                    if ( !done && (!this.readyState ||
342                        this.readyState == 'loaded' || this.readyState == 'complete') ) {
343                        done = true;
344 
345                        if (typeof callback == 'function') {
346                            callback.call( this, this );
347                        }
348 
349                        // Handle memory leak in IE
350                        script.onload = script.onreadystatechange = null;
351                    }
352                };
353 
354                var s = doc.getElementsByTagName( 'script' )[0];
355                s.parentNode.insertBefore( script, s );
356             },
357 
358             // parse anything into a number
359             parseValue: function( val ) {
360                 if (typeof val == 'number') {
361                     return val;
362                 } else if (typeof val == 'string') {
363                     var arr = val.match(/\-?\d/g);
364                     return arr && arr.constructor == Array ? arr.join('') * 1 : 0;
365                 } else {
366                     return 0;
367                 }
368             },
369 
370             // timestamp abstraction
371             timestamp: function() {
372                 return new Date().getTime();
373             },
374 
375             // this is pretty crap, but works for now
376             // it will add a callback, but it can't guarantee that the styles can be fetched
377             // using getComputedStyle further checking needed, possibly a dummy element
378             loadCSS : function( href, id, callback ) {
379 
380                 var link,
381                     ready = false,
382                     length;
383 
384                 // look for manual css
385                 $('link[rel=stylesheet]').each(function() {
386                     if ( new RegExp( href ).test( this.href ) ) {
387                         link = this;
388                         return false;
389                     }
390                 });
391 
392                 if ( typeof id == 'function' ) {
393                     callback = id;
394                     id = undef;
395                 }
396 
397                 callback = callback || function() {}; // dirty
398 
399                 // if already present, return
400                 if ( link ) {
401                     callback.call( link, link );
402                     return link;
403                 }
404 
405                 // save the length of stylesheets to check against
406                 length = doc.styleSheets.length;
407 
408                 // add timestamp if DEBUG is true
409                 if ( DEBUG ) {
410                     href += '?' + Utils.timestamp();
411                 }
412 
413                 // check for existing id
414                 if( $('#'+id).length ) {
415                     $('#'+id).attr('href', href);
416                     length--;
417                     ready = true;
418                 } else {
419                     link = $( '<link>' ).attr({
420                         rel: 'stylesheet',
421                         href: href,
422                         id: id
423                     }).get(0);
424 
425                     window.setTimeout(function() {
426                         var styles = $('link[rel="stylesheet"], style');
427                         if ( styles.length ) {
428                             styles.get(0).parentNode.insertBefore( link, styles[0] );
429                         } else {
430                             DOM().head.appendChild( link );
431                         }
432 
433                         if ( IE ) {
434                             link.attachEvent( 'onreadystatechange', function(e) {
435                                 if( link.readyState == 'complete' ) {
436                                     ready = true;
437                                 }
438                             });
439                         } else {
440                             // what to do here? returning for now.
441                             ready = true;
442                         }
443                     }, 10);
444                 }
445 
446                 if (typeof callback == 'function') {
447 
448                     Utils.wait({
449                         until: function() {
450                             return ready && doc.styleSheets.length > length;
451                         },
452                         success: function() {
453                             Utils.addTimer( 'css', function() {
454                                 callback.call( link, link );
455                             }, 100);
456                         },
457                         error: function() {
458                             Galleria.raise( 'Theme CSS could not load' );
459                         },
460                         timeout: 1000
461                     });
462                 }
463                 return link;
464             }
465         };
466     })();
467 
468 /**
469     The main Galleria class
470 
471     @class
472 
473     @example var gallery = new Galleria();
474 
475     @author http://aino.se
476 
477     @requires jQuery
478 
479     @returns {Galleria}
480 */
481 
482 Galleria = function() {
483 
484     var self = this;
485 
486     // the theme used
487     this._theme = undef;
488 
489     // internal options
490     this._options = {};
491 
492     // flag for controlling play/pause
493     this._playing = false;
494 
495     // internal interval for slideshow
496     this._playtime = 5000;
497 
498     // internal variable for the currently active image
499     this._active = null;
500 
501     // the internal queue, arrayified
502     this._queue = { length: 0 };
503 
504     // the internal data array
505     this._data = [];
506 
507     // the internal dom collection
508     this._dom = {};
509 
510     // the internal thumbnails array
511     this._thumbnails = [];
512 
513     // internal init flag
514     this._initialized = false;
515 
516     // global stagewidth/height
517     this._stageWidth = 0;
518     this._stageHeight = 0;
519 
520     // target holder
521     this._target = undef;
522 
523     // instance id
524     this._id = Utils.timestamp();
525 
526     // add some elements
527     var divs =  'container stage images image-nav image-nav-left image-nav-right ' +
528                 'info info-text info-title info-description info-author ' +
529                 'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
530                 'loader counter tooltip',
531         spans = 'current total';
532 
533     $.each( divs.split(' '), function( i, elemId ) {
534         self._dom[ elemId ] = Utils.create( 'galleria-' + elemId );
535     });
536 
537     $.each( spans.split(' '), function( i, elemId ) {
538         self._dom[ elemId ] = Utils.create( 'galleria-' + elemId, 'span' );
539     });
540 
541     // the internal keyboard object
542     // keeps reference of the keybinds and provides helper methods for binding keys
543     var keyboard = this._keyboard = {
544 
545         keys : {
546             'UP': 38,
547             'DOWN': 40,
548             'LEFT': 37,
549             'RIGHT': 39,
550             'RETURN': 13,
551             'ESCAPE': 27,
552             'BACKSPACE': 8
553         },
554 
555         map : {},
556 
557         bound: false,
558 
559         press: function(e) {
560             var key = e.keyCode || e.which;
561             if ( key in k.map && typeof keyboard.map[key] == 'function' ) {
562                 keyboard.map[key].call(self, e);
563             }
564         },
565 
566         attach: function(map) {
567             for( var key in map ) {
568                 var up = key.toUpperCase();
569                 if ( up in keyboard.keys ) {
570                     keyboard.map[ keyboard.keys[up] ] = map[key];
571                 }
572             }
573             if ( !keyboard.bound ) {
574                 keyboard.bound = true;
575                 $doc.bind('keydown', keyboard.press);
576             }
577         },
578 
579         detach: function() {
580             keyboard.bound = false;
581             $doc.unbind('keydown', keyboard.press);
582         }
583     };
584 
585     // internal controls for keeping track of active / inactive images
586     var controls = this._controls = {
587 
588         0: undef,
589 
590         1: undef,
591 
592         active : 0,
593 
594         swap : function() {
595             controls.active = controls.active ? 0 : 1;
596         },
597 
598         getActive : function() {
599             return controls[ controls.active ];
600         },
601 
602         getNext : function() {
603             return controls[ 1 - controls.active ];
604         }
605     };
606 
607     // internal carousel object
608     var carousel = this._carousel = {
609 
610         // shortcuts
611         next: self.$('thumb-nav-right'),
612         prev: self.$('thumb-nav-left'),
613 
614         // cache the width
615         width: 0,
616 
617         // track the current position
618         current: 0,
619 
620         // cache max value
621         max: 0,
622 
623         // save all hooks for each width in an array
624         hooks: [],
625 
626         // update the carousel
627         // you can run this method anytime, f.ex on window.resize
628         update: function() {
629             var w = 0,
630                 h = 0,
631                 hooks = [0];
632 
633             $.each( self._thumbnails, function( i, thumb ) {
634                 if ( thumb.ready ) {
635                     w += thumb.outerWidth || $( thumb.container ).outerWidth( true );
636                     hooks[ i+1 ] = w;
637                     h = Math.max( h, thumb.outerHeight || $( thumb.container).outerHeight() );
638                 }
639             });
640             self.$( 'thumbnails-container' ).toggleClass( 'galleria-carousel', w > self._stageWidth );
641 
642             self.$( 'thumbnails' ).css({
643                 width: w,
644                 height: h
645             });
646 
647             carousel.max = w;
648             carousel.hooks = hooks;
649             carousel.width = self.$( 'thumbnails-list' ).width();
650             carousel.setClasses();
651 
652             // todo: fix so the carousel moves to the left
653         },
654 
655         bindControls: function() {
656 
657             carousel.next.bind( CLICK(), function(e) {
658                 e.preventDefault();
659 
660                 if ( self._options.carousel_steps == 'auto' ) {
661 
662                     for ( var i = carousel.current; i < carousel.hooks.length; i++ ) {
663                         if ( carousel.hooks[i] - carousel.hooks[ carousel.current ] > carousel.width ) {
664                             carousel.set(i - 2);
665                             break;
666                         }
667                     }
668 
669                 } else {
670                     carousel.set( carousel.current + self._options.carousel_steps);
671                 }
672             });
673 
674             carousel.prev.bind( CLICK(), function(e) {
675                 e.preventDefault();
676 
677                 if ( self._options.carousel_steps == 'auto' ) {
678 
679                     for ( var i = carousel.current; i >= 0; i-- ) {
680                         if ( carousel.hooks[ carousel.current ] - carousel.hooks[i] > carousel.width ) {
681                             carousel.set( i + 2 );
682                             break;
683                         } else if ( i == 0 ) {
684                             carousel.set( 0 );
685                             break;
686                         }
687                     }
688                 } else {
689                     carousel.set( carousel.current - self._options.carousel_steps );
690                 }
691             });
692         },
693 
694         // calculate and set positions
695         set: function( i ) {
696             i = Math.max( i, 0 );
697             while ( carousel.hooks[i - 1] + carousel.width > carousel.max && i >= 0 ) {
698                 i--;
699             }
700             carousel.current = i;
701             carousel.animate();
702         },
703 
704         // get the last position
705         getLast: function(i) {
706             return ( i || carousel.current ) - 1;
707         },
708 
709         // follow the active image
710         follow: function(i) {
711 
712             //don't follow if position fits
713             if ( i == 0 || i == carousel.hooks.length - 2 ) {
714                 carousel.set( i );
715                 return;
716             }
717 
718             // calculate last position
719             var last = carousel.current;
720             while( carousel.hooks[last] - carousel.hooks[ carousel.current ] <
721                    carousel.width && last <= carousel.hooks.length ) {
722                 last ++;
723             }
724 
725             // set position
726             if ( i - 1 < carousel.current ) {
727                 carousel.set( i - 1 );
728             } else if ( i + 2 > last) {
729                 carousel.set( i - last + carousel.current + 2 );
730             }
731         },
732 
733         // helper for setting disabled classes
734         setClasses: function() {
735             carousel.prev.toggleClass( 'disabled', !carousel.current );
736             carousel.next.toggleClass( 'disabled', carousel.hooks[ carousel.current ] + carousel.width > carousel.max );
737         },
738 
739         // the animation method
740         animate: function(to) {
741             carousel.setClasses();
742             var num = carousel.hooks[ carousel.current ] * -1;
743 
744             if ( isNaN( num ) ) {
745                 return;
746             }
747 
748             self.$( 'thumbnails' ).animate({
749                 left: num
750             },{
751                 duration: self._options.carousel_speed,
752                 easing: self._options.easing,
753                 queue: false
754             });
755         }
756     };
757 
758     // tooltip control
759     // added in 1.2
760     var tooltip = this._tooltip = {
761 
762         initialized : false,
763 
764         active: null,
765 
766         open: false,
767 
768         init: function() {
769 
770             tooltip.initialized = true;
771 
772             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' +
773                       '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);}';
774 
775             Utils.insertStyleTag(css);
776 
777             self.$( 'tooltip' ).css('opacity', .8);
778             Utils.hide( self.get('tooltip') );
779 
780         },
781 
782         // move handler
783         move: function( e ) {
784             var mouseX = self.getMousePosition(e).x,
785                 mouseY = self.getMousePosition(e).y,
786                 $elem = self.$( 'tooltip' ),
787                 x = mouseX,
788                 y = mouseY,
789                 height = $elem.outerHeight( true ) + 1,
790                 width = $elem.outerWidth( true ),
791                 limitY = height + 15;
792 
793             var maxX = self._stageWidth - width,
794                 maxY = self._stageHeight - height;
795 
796             if ( !isNaN(x) && !isNaN(y) ) {
797 
798                 x += 15;
799                 y -= 35;
800 
801                 x = Math.max( 0, Math.min( maxX, x ) );
802                 y = Math.max( 0, Math.min( maxY, y ) );
803 
804                 if( mouseY < limitY ) {
805                     y = limitY;
806                 }
807 
808                 $elem.css({ left: x, top: y });
809             }
810         },
811 
812         // bind elements to the tooltip
813         // you can bind multiple elementIDs using { elemID : function } or { elemID : string }
814         // you can also bind single DOM elements using bind(elem, string)
815         bind: function( elem, value ) {
816 
817             if (! tooltip.initialized ) {
818                 tooltip.init();
819             }
820 
821             var hover = function( elem, value) {
822 
823                 tooltip.define( elem, value );
824 
825                 $( elem ).hover(function() {
826 
827                     tooltip.active = elem;
828                     Utils.clearTimer('switch_tooltip');
829                     self.$('container').unbind( 'mousemove', tooltip.move ).bind( 'mousemove', tooltip.move ).trigger( 'mousemove' );
830                     tooltip.show( elem );
831 
832                     Galleria.utils.addTimer( 'tooltip', function() {
833 
834                         Utils.show( self.get( 'tooltip' ), 400 );
835                         tooltip.open = true;
836 
837                     }, tooltip.open ? 0 : 1000);
838 
839                 }, function() {
840 
841                     tooltip.active = null;
842 
843                     self.$( 'container' ).unbind( 'mousemove', tooltip.move );
844                     Utils.clearTimer( 'tooltip' );
845 
846                     Utils.hide( self.get( 'tooltip' ), 200, function() {
847                         Utils.addTimer('switch_tooltip', function() {
848                             tooltip.open = false;
849                         }, 1000);
850                     });
851                 });
852             };
853 
854             if (typeof value == 'string') {
855                 hover( ( elem in self._dom ? self.get(elem) : elem ), value );
856             } else {
857                 // asume elemID here
858                 $.each( elem, function( elemID, val ) {
859                     hover( self.get(elemID), val );
860                 });
861             }
862         },
863 
864         show: function( elem ) {
865             var text = $(elem).data('tt');
866             if ( ! text ) {
867                 return;
868             }
869             text = typeof text == 'function' ? text() : text;
870             self.$( 'tooltip' ).html( text.replace(/\s/, ' ') );
871         },
872 
873         // redefine the tooltip here
874         define: function( elem, value ) {
875 
876             // we store functions, not strings
877             if (typeof value !== 'function') {
878                 var s = value;
879                 value = function() {
880                     return s;
881                 };
882             }
883 
884             if ( elem in self._dom ) {
885                 elem = self.get( elem );
886             }
887 
888             $(elem).data('tt', value);
889 
890             tooltip.show( elem );
891 
892         },
893 
894         refresh: function() {
895             $.each( arguments, function(i, elem) {
896                 if ( tooltip.active == elem ) {
897                     tooltip.show( elem );
898                 }
899             });
900         }
901     };
902 
903     // internal fullscreen control
904     // added in 1.195
905     // still kind of experimental
906     var fullscreen = this._fullscreen = {
907         scrolled: 0,
908         enter: function(callback) {
909 
910             // hide the image until rescale is complete
911             Utils.hide( self.getActiveImage() );
912 
913             self.$( 'container' ).addClass( 'fullscreen' );
914 
915             fullscreen.scrolled = $(window).scrollTop();
916 
917             // begin styleforce
918             Utils.forceStyles(self.get('container'), {
919                 position: 'fixed',
920                 top: 0,
921                 left: 0,
922                 width: '100%',
923                 height: '100%',
924                 zIndex: 10000
925             });
926 
927             var htmlbody = {
928                 height: '100%',
929                 overflow: 'hidden',
930                 margin:0,
931                 padding:0
932             };
933 
934             Utils.forceStyles( DOM().html, htmlbody );
935             Utils.forceStyles( DOM().body, htmlbody );
936 
937             // attach some keys
938             self.attachKeyboard({
939                 escape: self.exitFullscreen,
940                 right: self.next,
941                 left: self.prev
942             });
943 
944             // init the first rescale and attach callbacks
945             self.rescale(function() {
946 
947                 Utils.addTimer('fullscreen_enter', function() {
948                     // show the image after 50 ms
949                     Utils.show( self.getActiveImage() );
950 
951                     if (typeof callback == 'function') {
952                         callback.call( self );
953                     }
954 
955                 }, 50);
956 
957                 self.trigger( Galleria.FULLSCREEN_ENTER );
958             });
959 
960             // bind the scaling to the resize event
961             $(window).resize( function() {
962                 fullscreen.scale();
963             } );
964         },
965 
966         scale : function() {
967             self.rescale();
968         },
969 
970         exit: function(callback) {
971 
972             Utils.hide( self.getActiveImage() );
973 
974             self.$('container').removeClass( 'fullscreen' );
975 
976             // revert all styles
977             Utils.revertStyles( self.get('container'), DOM().html, DOM().body );
978 
979             // scroll back
980             window.scrollTo(0, fullscreen.scrolled);
981 
982             // detach all keyboard events (is this good?)
983             self.detachKeyboard();
984 
985             self.rescale(function() {
986                 Utils.addTimer('fullscreen_exit', function() {
987 
988                     // show the image after 50 ms
989                     Utils.show( self.getActiveImage() );
990 
991                     if ( typeof callback == 'function' ) {
992                         callback.call( self );
993                     }
994 
995                 }, 50);
996 
997                 self.trigger( Galleria.FULLSCREEN_EXIT );
998             });
999 
1000             $(window).unbind('resize', fullscreen.scale);
1001         }
1002     };
1003 
1004     // the internal idle object for controlling idle states
1005     // TODO occational event conflicts
1006     var idle = this._idle = {
1007 
1008         trunk: [],
1009 
1010         bound: false,
1011 
1012         add: function(elem, to) {
1013             if (!elem) {
1014                 return;
1015             }
1016             if (!idle.bound) {
1017                 idle.addEvent();
1018             }
1019             elem = $(elem);
1020 
1021             var from = {};
1022 
1023             for (var style in to) {
1024                 from[style] = elem.css(style);
1025             }
1026             elem.data('idle', {
1027                 from: from,
1028                 to: to,
1029                 complete: true,
1030                 busy: false
1031             });
1032             idle.addTimer();
1033             idle.trunk.push(elem);
1034         },
1035 
1036         remove: function(elem) {
1037 
1038             elem = jQuery(elem);
1039 
1040             $.each(idle.trunk, function(i, el) {
1041                 if ( el.length && !el.not(elem).length ) {
1042                     self._idle.show(elem);
1043                     self._idle.trunk.splice(i, 1);
1044                 }
1045             });
1046 
1047             if (!idle.trunk.length) {
1048                 idle.removeEvent();
1049                 Utils.clearTimer('idle');
1050             }
1051         },
1052 
1053         addEvent : function() {
1054             idle.bound = true;
1055             self.$('container').bind('mousemove click', idle.showAll );
1056         },
1057 
1058         removeEvent : function() {
1059             idle.bound = false;
1060             self.$('container').unbind('mousemove click', idle.showAll );
1061         },
1062 
1063         addTimer : function() {
1064             Utils.addTimer('idle', function() {
1065                 self._idle.hide();
1066             }, self._options.idle_time );
1067         },
1068 
1069         hide : function() {
1070             self.trigger( Galleria.IDLE_ENTER );
1071 
1072             $.each( idle.trunk, function(i, elem) {
1073 
1074                 var data = elem.data('idle');
1075 
1076                 if (! data) {
1077                     return;
1078                 }
1079 
1080                 data.complete = false;
1081 
1082                 elem.stop().animate(data.to, {
1083                     duration: 600,
1084                     queue: false,
1085                     easing: 'swing'
1086                 });
1087             });
1088         },
1089 
1090         showAll : function() {
1091             Utils.clearTimer('idle');
1092 
1093             $.each(self._idle.trunk, function( i, elem ) {
1094                 self._idle.show( elem );
1095             });
1096         },
1097 
1098         show: function(elem) {
1099 
1100             var data = elem.data('idle');
1101 
1102             if (!data.busy && !data.complete) {
1103 
1104                 data.busy = true;
1105 
1106                 self.trigger( Galleria.IDLE_EXIT );
1107 
1108                 elem.animate(data.from, {
1109                     duration: 300,
1110                     queue: false,
1111                     easing: 'swing',
1112                     complete: function() {
1113                         $(this).data('idle').busy = false;
1114                         $(this).data('idle').complete = true;
1115                     }
1116                 });
1117             }
1118             idle.addTimer();
1119         }
1120     };
1121 
1122     // internal lightbox object
1123     // creates a predesigned lightbox for simple popups of images in galleria
1124     var lightbox = this._lightbox = {
1125 
1126         width : 0,
1127 
1128         height : 0,
1129 
1130         initialized : false,
1131 
1132         active : null,
1133 
1134         image : null,
1135 
1136         elems : {},
1137 
1138         init : function() {
1139 
1140             // trigger the event
1141             self.trigger( Galleria.LIGHTBOX_OPEN );
1142 
1143             if ( lightbox.initialized ) {
1144                 return;
1145             }
1146             lightbox.initialized = true;
1147 
1148             // create some elements to work with
1149             var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
1150                 el = {},
1151                 op = self._options,
1152                 css = '',
1153                 cssMap = {
1154                     overlay:    'position:fixed;display:none;opacity:'+op.overlay_opacity+';top:0;left:0;width:100%;height:100%;background:'+op.overlay_background+';z-index:99990',
1155                     box:        'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
1156                     shadow:     'position:absolute;background:#000;width:100%;height:100%;',
1157                     content:    'position:absolute;background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
1158                     info:       'position:absolute;bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
1159                     close:      'position:absolute;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',
1160                     image:      'position:absolute;top:10px;left:10px;right:10px;bottom:30px;overflow:hidden',
1161                     prevholder: 'position:absolute;width:50%;height:100%;cursor:pointer',
1162                     nextholder: 'position:absolute;width:50%;height:100%;right:0;cursor:pointer',
1163                     prev:       'position:absolute;top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;line-height:40px;text-align:center;color:#000',
1164                     next:       'position:absolute;top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;right:20px;left:auto;display:none;line-height:40px;text-align:center;color:#000',
1165                     title:      'float:left',
1166                     counter:    'float:right;margin-left:8px'
1167                 },
1168                 hover = function(elem) {
1169                     return elem.hover(
1170                         function() { $(this).css( 'color', '#bbb' ); },
1171                         function() { $(this).css( 'color', '#444' ); }
1172                     );
1173                 };
1174 
1175             // create and insert CSS
1176             $.each(cssMap, function( key, value ) {
1177                 css += '.galleria-lightbox-'+key+'{'+value+'}';
1178             });
1179 
1180             Utils.insertStyleTag( css );
1181 
1182             // create the elements
1183             $.each(elems.split(' '), function( i, elemId ) {
1184                 self.addElement( 'lightbox-' + elemId );
1185                 el[ elemId ] = lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
1186             });
1187 
1188             // initiate the image
1189             lightbox.image = new Galleria.Picture();
1190 
1191             // append the elements
1192             self.append({
1193                 'lightbox-box': ['lightbox-shadow','lightbox-content', 'lightbox-close','lightbox-prevholder','lightbox-nextholder'],
1194                 'lightbox-info': ['lightbox-title','lightbox-counter'],
1195                 'lightbox-content': ['lightbox-info', 'lightbox-image'],
1196                 'lightbox-prevholder': 'lightbox-prev',
1197                 'lightbox-nextholder': 'lightbox-next'
1198             });
1199 
1200             $( el.image ).append( lightbox.image.container );
1201 
1202             $( DOM().body ).append( el.overlay, el.box );
1203 
1204             // add the prev/next nav and bind some controls
1205 
1206             hover( $( el.close ).bind( CLICK(), lightbox.hide ).html('×') );
1207 
1208             $.each( ['Prev','Next'], function(i, dir) {
1209 
1210                 var $d = $( el[ dir.toLowerCase() ] ).html( /v/.test( dir ) ? '‹ ' : ' â€º' );
1211 
1212                 $( el[ dir.toLowerCase()+'holder'] ).hover(function() {
1213                     $d.show();
1214                 }, function() {
1215                     $d.fadeOut( 200 );
1216                 }).bind( CLICK(), function() {
1217                     lightbox[ 'show' + dir ]();
1218                 });
1219 
1220             });
1221             $( el.overlay ).bind( CLICK(), lightbox.hide );
1222 
1223         },
1224 
1225         rescale: function(event) {
1226 
1227             // calculate
1228             var width = Math.min( $(window).width(), lightbox.width ),
1229                 height = Math.min( $(window).height(), lightbox.height ),
1230                 ratio = Math.min( (width - 60) / lightbox.width, (height - 80) / lightbox.height ),
1231                 destWidth = ( lightbox.width * ratio ) + 40,
1232                 destHeight = ( lightbox.height * ratio ) + 60,
1233                 to = {
1234                     width: destWidth,
1235                     height: destHeight,
1236                     marginTop: Math.ceil( destHeight / 2 ) *- 1,
1237                     marginLeft: Math.ceil( destWidth / 2 ) *- 1
1238                 };
1239 
1240             // if rescale event, don't animate
1241             if ( event ) {
1242                 $( lightbox.elems.box ).css( to );
1243             } else {
1244                 $( lightbox.elems.box ).animate(
1245                     to,
1246                     self._options.lightbox_transition_speed,
1247                     self._options.easing,
1248                     function() {
1249                         var image = lightbox.image,
1250                             speed = self._options.lightbox_fade_speed;
1251 
1252                         self.trigger({
1253                             type: Galleria.LIGHTBOX_IMAGE,
1254                             imageTarget: image.image
1255                         });
1256 
1257                         image.show();
1258                         Utils.show( image.image, speed );
1259                         Utils.show( lightbox.elems.info, speed );
1260                     }
1261                 );
1262             }
1263         },
1264 
1265         hide: function() {
1266 
1267             // remove the image
1268             lightbox.image.image = null;
1269 
1270             $(window).unbind('resize', lightbox.rescale);
1271 
1272             $( lightbox.elems.box ).hide();
1273 
1274             Utils.hide( lightbox.elems.info );
1275 
1276             Utils.hide( lightbox.elems.overlay, 200, function() {
1277                 $( this ).hide().css( 'opacity', self._options.overlay_opacity );
1278                 self.trigger( Galleria.LIGHTBOX_CLOSE );
1279             });
1280         },
1281 
1282         showNext: function() {
1283             lightbox.show( self.getNext( lightbox.active ) );
1284         },
1285 
1286         showPrev: function() {
1287             lightbox.show( self.getPrev( lightbox.active ) );
1288         },
1289 
1290         show: function(index) {
1291 
1292             lightbox.active = index = typeof index == 'number' ? index : self.getIndex();
1293 
1294             if ( !lightbox.initialized ) {
1295                 lightbox.init();
1296             }
1297 
1298             $(window).unbind('resize', lightbox.rescale );
1299 
1300             var data = self.getData(index),
1301                 total = self.getDataLength();
1302 
1303             Utils.hide( lightbox.elems.info );
1304 
1305             lightbox.image.load( data.image, function( image ) {
1306 
1307                 lightbox.width = image.original.width;
1308                 lightbox.height = image.original.height;
1309 
1310                 $( image.image ).css({
1311                     width: '100.5%',
1312                     height: '100.5%',
1313                     top: 0,
1314                     zIndex: 99998,
1315                     opacity: 0
1316                 });
1317 
1318                 lightbox.elems.title.innerHTML = data.title;
1319                 lightbox.elems.counter.innerHTML = (index + 1) + ' / ' + total;
1320                 $(window).resize( lightbox.rescale );
1321                 lightbox.rescale();
1322             });
1323 
1324             $( lightbox.elems.overlay ).show();
1325             $( lightbox.elems.box ).show();
1326         }
1327     };
1328 
1329     return this;
1330 };
1331 
1332 // end Galleria constructor
1333 
1334 Galleria.prototype = {
1335 
1336     /**
1337         Use this function to initialize the gallery and start loading.
1338         Should only be called once per instance.
1339 
1340         @param {HTML Element} target The target element
1341         @param {Object} options The gallery options
1342 
1343         @returns {Galleria}
1344     */
1345 
1346     init: function( target, options ) {
1347 
1348         var self = this;
1349 
1350         // save the instance
1351         _galleries.push( this );
1352 
1353         // save the original ingredients
1354         this._original = {
1355             target: target,
1356             options: options,
1357             data: null
1358         };
1359 
1360         // save the target here
1361         this._target = this._dom.target = target.nodeName ? target : $( target ).get(0);
1362 
1363         // raise error if no target is detected
1364         if ( !this._target ) {
1365              Galleria.raise('Target not found.');
1366              return;
1367         }
1368 
1369         // apply options
1370         this._options = {
1371             autoplay: false,
1372             carousel: true,
1373             carousel_follow: true,
1374             carousel_speed: 400,
1375             carousel_steps: 'auto',
1376             clicknext: false,
1377             data_config : function( elem ) { return {}; },
1378             data_selector: 'img',
1379             data_source: this._target,
1380             debug: undef,
1381             easing: 'galleria',
1382             extend: function(options) {},
1383             height: 'auto',
1384             idle_time: 3000,
1385             image_crop: false,
1386             image_margin: 0,
1387             image_pan: false,
1388             image_pan_smoothness: 12,
1389             image_position: '50%',
1390             keep_source: false,
1391             lightbox_fade_speed: 200,
1392             lightbox_transition_speed: 500,
1393             link_source_images: true,
1394             max_scale_ratio: undef,
1395             min_scale_ratio: undef,
1396             on_image: function(img,thumb) {},
1397             overlay_opacity: .85,
1398             overlay_background: '#0b0b0b',
1399             pause_on_interaction: true, // 1.9.96
1400             popup_links: false,
1401             preload: 2,
1402             queue: true,
1403             show: 0,
1404             show_info: true,
1405             show_counter: true,
1406             show_imagenav: true,
1407             thumb_crop: true,
1408             thumb_event_type: CLICK(),
1409             thumb_fit: true,
1410             thumb_margin: 0,
1411             thumb_quality: 'auto',
1412             thumbnails: true,
1413             transition: 'fade',
1414             transition_initial: undef,
1415             transition_speed: 400,
1416             width: 'auto'
1417         };
1418 
1419         // apply debug
1420         if ( options && options.debug === true ) {
1421             DEBUG = true;
1422         }
1423 
1424         // hide all content
1425         $( this._target ).children().hide();
1426 
1427         // now we just have to wait for the theme...
1428         if ( Galleria.theme ) {
1429             this._init();
1430         } else {
1431             Utils.addTimer('themeload', function() {
1432                 Galleria.raise( 'No theme found. ');
1433             }, 2000);
1434 
1435             $doc.one( Galleria.THEMELOAD, function() {
1436                 Utils.clearTimer( 'themeload' );
1437                 self._init.call( self );
1438             });
1439         }
1440     },
1441 
1442     // the internal _init is called when the THEMELOAD event is triggered
1443     // this method should only be called once per instance
1444     // for manipulation of data, use the .load method
1445 
1446     _init: function() {
1447         var self = this;
1448 
1449         if ( this._initialized ) {
1450             Galleria.raise( 'Init failed: Gallery instance already initialized.' );
1451             return this;
1452         }
1453 
1454         this._initialized = true;
1455 
1456         if ( !Galleria.theme ) {
1457             Galleria.raise( 'Init failed: No theme found.' );
1458             return this;
1459         }
1460 
1461         // merge the theme & caller options
1462         $.extend( true, this._options, Galleria.theme.defaults, this._original.options );
1463 
1464         // bind the gallery to run when data is ready
1465         this.bind( Galleria.DATA, function() {
1466 
1467             // save the new data
1468             this._original.data = this._data;
1469 
1470             // lets show the counter here
1471             this.get('total').innerHTML = this.getDataLength();
1472 
1473             // cache the container
1474             var $container = this.$( 'container' );
1475 
1476             // the gallery is ready, let's just wait for the css
1477             var num = { width: 0, height: 0 };
1478             var testElem =  Utils.create('galleria-image');
1479 
1480             // check container and thumbnail height
1481             Utils.wait({
1482                 until: function() {
1483 
1484                     // keep trying to get the value
1485                     $.each(['width', 'height'], function( i, m ) {
1486 
1487                         if (self._options[ m ] && typeof self._options[ m ] == 'number') {
1488                             num[ m ] = self._options[ m ];
1489                         } else {
1490                             num[m] = Utils.parseValue( self.$( 'target' ).css( m ) ) ||
1491                                      Utils.parseValue( $container.css( m ) ) ||
1492                                      self.$( 'target' )[ m ]() ||
1493                                      $container[ m ]()
1494                         }
1495 
1496                         $container[m]( num[ m ] );
1497                     });
1498 
1499                     var thumbHeight = function() {
1500                         return true;
1501                     };
1502 
1503                     // make sure thumbnails have a height as well
1504                     if ( self._options.thumbnails ) {
1505                         self.$('thumbnails').append( testElem );
1506                         thumbHeight = function() {
1507                             return !!$( testElem ).height();
1508                         };
1509                     }
1510 
1511                     return thumbHeight() && num.width && num.height > 50;
1512 
1513                 },
1514                 success: function() {
1515 
1516                     // remove the testElem
1517                     $( testElem ).remove();
1518 
1519                     // for some strange reason, webkit needs a single setTimeout to play ball
1520                     if ( Galleria.WEBKIT ) {
1521                         window.setTimeout( function() {
1522                             self._run();
1523                         }, 1);
1524                     } else {
1525                         self._run();
1526                     }
1527                 },
1528                 error: function() {
1529                     // Height was probably not set, raise a hard error
1530                     Galleria.raise('Width & Height not found.', true);
1531                 },
1532                 timeout: 2000
1533             });
1534         });
1535 
1536         // postrun some stuff after the gallery is ready
1537         // make sure it only runs once
1538         var one = false;
1539 
1540         this.bind( Galleria.READY, function() {
1541 
1542             // show counter
1543             Utils.show( this.get('counter') );
1544 
1545             // bind clicknext
1546             if ( this._options.clicknext ) {
1547                 $.each( this._data, function( i, data ) {
1548                     delete data.link;
1549                 });
1550                 this.$( 'stage' ).css({ cursor : 'pointer' }).bind( CLICK(), function(e) {
1551                     self.next();
1552                 });
1553             }
1554 
1555             // bind carousel nav
1556             if ( this._options.carousel ) {
1557                 this._carousel.bindControls();
1558             }
1559 
1560             // start autoplay
1561             if ( this._options.autoplay ) {
1562 
1563                 this.pause();
1564 
1565                 if ( typeof this._options.autoplay == 'number' ) {
1566                     this._playtime = this._options.autoplay;
1567                 }
1568 
1569                 this.trigger( Galleria.PLAY );
1570                 this._playing = true;
1571             }
1572 
1573             // if second load, just do the show and return
1574             if ( one ) {
1575                 this.show( this._options.show );
1576                 return;
1577             }
1578 
1579             one = true;
1580 
1581             // initialize the History plugin
1582             if ( Galleria.History ) {
1583 
1584                 // bind the show method
1585                 Galleria.History.change(function(e) {
1586 
1587                     // grab history ID
1588                     var val = parseInt( e.value.replace( /\//, '' ) );
1589 
1590                     // if ID is NaN, the user pressed back from the first image
1591                     // return to previous address
1592                     if (isNaN(val)) {
1593                         window.history.go(-1);
1594 
1595                     // else show the image
1596                     } else {
1597                         self.show( val, undef, true );
1598                     }
1599                 });
1600             }
1601 
1602             // call the theme init method
1603             Galleria.theme.init.call( this, this._options );
1604 
1605             // call the extend option
1606             this._options.extend.call( this, this._options );
1607 
1608             // show the initial image
1609             // first test for permalinks in history
1610             if ( /^[0-9]{1,4}$/.test( HASH ) && Galleria.History ) {
1611                 this.show( HASH, undef, true );
1612 
1613             } else {
1614                 this.show( this._options.show );
1615             }
1616 
1617         });
1618 
1619         // build the gallery frame
1620         this.append({
1621             'info-text' :
1622                 ['info-title', 'info-description', 'info-author'],
1623             'info' :
1624                 ['info-text'],
1625             'image-nav' :
1626                 ['image-nav-right', 'image-nav-left'],
1627             'stage' :
1628                 ['images', 'loader', 'counter', 'image-nav'],
1629             'thumbnails-list' :
1630                 ['thumbnails'],
1631             'thumbnails-container' :
1632                 ['thumb-nav-left', 'thumbnails-list', 'thumb-nav-right'],
1633             'container' :
1634                 ['stage', 'thumbnails-container', 'info', 'tooltip']
1635         });
1636 
1637         Utils.hide( this.$( 'counter' ).append(
1638             this.get( 'current' ),
1639             ' / ',
1640             this.get( 'total' )
1641         ) );
1642 
1643         this.setCounter('–');
1644 
1645         // add images to the controls
1646         $.each( new Array(2), function(i) {
1647 
1648             // create a new Picture instance
1649             var image = new Galleria.Picture();
1650 
1651             // apply some styles
1652             $( image.container ).css({
1653                 position: 'absolute',
1654                 top: 0,
1655                 left: 0
1656             });
1657 
1658             // append the image
1659             self.$( 'images' ).append( image.container );
1660 
1661             // reload the controls
1662             self._controls[i] = image;
1663 
1664         });
1665 
1666         // some forced generic styling
1667         this.$( 'images' ).css({
1668             position: 'relative',
1669             top: 0,
1670             left: 0,
1671             width: '100%',
1672             height: '100%'
1673         });
1674 
1675         this.$( 'thumbnails, thumbnails-list' ).css({
1676             overflow: 'hidden',
1677             position: 'relative'
1678         });
1679 
1680         // bind image navigation arrows
1681         this.$( 'image-nav-right, image-nav-left' ).bind( CLICK(), function(e) {
1682 
1683             // tune the clicknext option
1684             if ( self._options.clicknext ) {
1685                 e.stopPropagation();
1686             }
1687 
1688             // pause if options is set
1689             if ( self._options.pause_on_interaction ) {
1690                 self.pause();
1691             }
1692 
1693             // navigate
1694             var fn = /right/.test( this.className ) ? 'next' : 'prev';
1695             self[ fn ]();
1696 
1697         });
1698 
1699         // hide controls if chosen to
1700         $.each( ['info','counter','image-nav'], function( i, el ) {
1701             if ( self._options[ 'show_' + el.replace(/-/, '') ] === false ) {
1702                 Utils.moveOut( self.get( el ) );
1703             }
1704         });
1705 
1706         // load up target content
1707         this.load();
1708 
1709         // now it's usually safe to remove the content
1710         // IE will never stop loading if we remove it, so let's keep it hidden for IE (it's usually fast enough anyway)
1711         if ( !this._options.keep_source && !IE ) {
1712             this._target.innerHTML = '';
1713         }
1714 
1715         // append the gallery frame
1716         this.$( 'target' ).append( this.get( 'container' ) );
1717 
1718         // parse the carousel on each thumb load
1719         if ( this._options.carousel ) {
1720             this.bind( Galleria.THUMBNAIL, function() {
1721                 this.updateCarousel();
1722             });
1723         }
1724 
1725         // bind on_image helper
1726         this.bind( Galleria.IMAGE, function( e ) {
1727             this._options.on_image.call( this, e.imageTarget, e.thumbTarget );
1728         });
1729 
1730         return this;
1731     },
1732 
1733     // the internal _run method should be called after loading data into galleria
1734     // creates thumbnails and makes sure the gallery has proper meassurements
1735     _run : function() {
1736         // shortcuts
1737         var self = this,
1738             o = this._options,
1739 
1740             // width/height for calculations
1741             width  = 0,
1742             height = 0,
1743 
1744             // cache the thumbnail option
1745             optval = typeof o.thumbnails == 'string' ? o.thumbnails.toLowerCase() : null;
1746 
1747         // loop through data and create thumbnails
1748         for( var i = 0; this._data[i]; i++ ) {
1749 
1750             var thumb,
1751                 data = this._data[i],
1752                 $container;
1753 
1754             if ( o.thumbnails === true ) {
1755 
1756                 // add a new Picture instance
1757                 thumb = new Galleria.Picture(i);
1758 
1759                 // get source from thumb or image
1760                 var src = data.thumb || data.image;
1761 
1762                 // append the thumbnail
1763                 this.$( 'thumbnails' ).append( thumb.container );
1764 
1765                 // cache the container
1766                 $container = $( thumb.container );
1767 
1768                 // move some data into the instance
1769                 thumb.data = {
1770                     width  : Utils.parseValue( $container.css('width') ),
1771                     height : Utils.parseValue( $container.css('height') ),
1772                     order  : i
1773                 };
1774 
1775                 // grab & reset size for smoother thumbnail loads
1776                 $container.css(( o.thumb_fit && o.thumb_crop !== true ) ?
1777                     { width: 0, height: 0 } :
1778                     { width: thumb.data.width, height: thumb.data.height });
1779 
1780                 // load the thumbnail
1781                 thumb.load( src, function( thumb ) {
1782 
1783                     // scale when ready
1784                     thumb.scale({
1785                         width:    thumb.data.width,
1786                         height:   thumb.data.height,
1787                         crop:     o.thumb_crop,
1788                         margin:   o.thumb_margin,
1789                         complete: function( thumb ) {
1790 
1791                             // shrink thumbnails to fit
1792                             var top = ['left', 'top'];
1793                             var arr = ['Width', 'Height'];
1794 
1795                             // calculate shrinked positions
1796                             $.each(arr, function( i, meassure ) {
1797                                 var m = meassure.toLowerCase();
1798                                 if ( (o.thumb_crop !== true || o.thumb_crop == m ) && o.thumb_fit ) {
1799                                     var css = {};
1800                                     css[m] = thumb[m];
1801                                     $( thumb.container ).css( css );
1802                                     css = {};
1803                                     css[top[i]] = 0;
1804                                     $( thumb.image ).css( css);
1805                                 }
1806 
1807                                 // cache outer meassures
1808                                 thumb['outer' + meassure] = $( thumb.container )['outer' + meassure]( true );
1809                             });
1810 
1811                             // set high quality if downscale is moderate
1812                             Utils.toggleQuality( thumb.image,
1813                                 o.thumb_quality === true ||
1814                                 ( o.thumb_quality == 'auto' && thumb.original.width < thumb.width * 3 )
1815                             );
1816 
1817                             // trigger the THUMBNAIL event
1818                             self.trigger({
1819                                 type: Galleria.THUMBNAIL,
1820                                 thumbTarget: thumb.image,
1821                                 thumbOrder: thumb.data.order
1822                             });
1823                         }
1824                     });
1825                 });
1826 
1827                 // preload all images here
1828                 if ( o.preload == 'all' ) {
1829                     thumb.add( data.image );
1830                 }
1831 
1832             // create empty spans if thumbnails is set to 'empty'
1833             } else if ( optval == 'empty' || optval == 'numbers' ) {
1834 
1835                 thumb = {
1836                     container:  Utils.create( 'galleria-image' ),
1837                     image: Utils.create( 'img', 'span' ),
1838                     ready: true
1839                 };
1840 
1841                 // create numbered thumbnails
1842                 if ( optval == 'numbers' ) {
1843                     $( thumb.image ).text( i + 1 );
1844                 }
1845 
1846                 $( thumb.container ).append( thumb.image );
1847                 this.$( 'thumbnails' ).append( thumb.container );
1848 
1849                 self.trigger({
1850                     type: Galleria.THUMBNAIL,
1851                     thumbTarget: thumb.image,
1852                     thumbOrder: i
1853                 });
1854 
1855             // create null object to silent errors
1856             } else {
1857                 thumb = {
1858                     container: null,
1859                     image: null
1860                 };
1861             }
1862 
1863             // add events for thumbnails
1864             // you can control the event type using thumb_event_type
1865             // we'll add the same event to the source if it's kept
1866 
1867             $( thumb.container ).add( o.keep_source && o.link_source_images ? data.original : null )
1868                 .data('index', i).bind(o.thumb_event_type, function(e) {
1869                     // pause if option is set
1870                     if ( o.pause_on_interaction ) {
1871                         self.pause();
1872                     }
1873 
1874                     // extract the index from the data
1875                     var index = $( e.currentTarget ).data( 'index' );
1876                     if ( self.getIndex() !== index ) {
1877                         self.show( index );
1878                     }
1879 
1880                     e.preventDefault();
1881             });
1882 
1883             this._thumbnails.push( thumb );
1884         }
1885 
1886         // make sure we have a stageHeight && stageWidth
1887 
1888         Utils.wait({
1889 
1890             until: function() {
1891                 self._stageWidth  = self.$( 'stage' ).width();
1892                 self._stageHeight = self.$( 'stage' ).height();
1893                 return( self._stageWidth && self._stageHeight > 50 ); // what is an acceptable height?
1894             },
1895 
1896             success: function() {
1897                 self.trigger( Galleria.READY );
1898             },
1899 
1900             error: function() {
1901                 Galleria.raise('stage meassures not found');
1902             }
1903 
1904         });
1905     },
1906 
1907     /**
1908         Loads data into the gallery.
1909         You can call this method on an existing gallery to reload the gallery with new data.
1910 
1911         @param {Array or String} source Optional JSON array of data or selector of where to find data in the document.
1912         Defaults to the Galleria target or data_source option.
1913 
1914         @param {String} selector Optional element selector of what elements to parse.
1915         Defaults to 'img'.
1916 
1917         @param {Function} config Optional function to modify the data extraction proceedure from the selector.
1918         See the data_config option for more information.
1919 
1920         @returns {Galleria}
1921     */
1922 
1923     load : function( source, selector, config ) {
1924 
1925         var self = this;
1926 
1927         // empty the data array
1928         this._data = [];
1929 
1930         // empty the thumbnails
1931         this._thumbnails = [];
1932         this.$('thumbnails').empty();
1933 
1934         // shorten the arguments
1935         if ( typeof selector == 'function' ) {
1936             config = selector;
1937             selector = null;
1938         }
1939 
1940         // use the source set by target
1941         source = source || this._options.data_source;
1942 
1943         // use selector set by option
1944         selector = selector || this._options.data_selector;
1945 
1946         // use the data_config set by option
1947         config = config || this._options.data_config;
1948 
1949         // check if the data is an array already
1950         if ( source.constructor == Array ) {
1951             if ( this.validate( source) ) {
1952                 this._data = source;
1953                 this.trigger( Galleria.DATA );
1954             } else {
1955                 Galleria.raise( 'Load failed: JSON Array not valid.' );
1956             }
1957             return this;
1958         }
1959         // loop through images and set data
1960         $( source ).find( selector ).each( function( i, img ) {
1961             var data = {},
1962                 img = $( img ),
1963                 parent = img.parent(),
1964                 href = parent.attr( 'href' );
1965 
1966             // check if it's a link to another image
1967             if ( /\.(png|gif|jpg|jpeg)$/i.test(href) ) {
1968                 data.image = href;
1969 
1970             // else assign the href as a link if it exists
1971             } else if ( href ) {
1972                 data.link = href;
1973             }
1974 
1975             // mix default extractions with the hrefs and config
1976             // and push it into the data array
1977             self._data.push( $.extend({
1978 
1979                 title:       img.attr('title'),
1980                 thumb:       img.attr('src'),
1981                 image:       img.attr('src'),
1982                 description: img.attr('alt'),
1983                 link:        img.attr('longdesc'),
1984                 original:    img.get(0) // saved as a reference
1985 
1986             }, data, config( img ) ) );
1987 
1988         });
1989         // trigger the DATA event and return
1990         if ( this.getDataLength() ) {
1991             this.trigger( Galleria.DATA );
1992         } else {
1993             Galleria.raise('Load failed: no data found.');
1994         }
1995         return this;
1996 
1997     },
1998 
1999     _getActive: function() {
2000         return this._controls.getActive();
2001     },
2002 
2003     validate : function( data ) {
2004         // todo: validate a custom data array
2005         return true;
2006     },
2007 
2008     /**
2009         Bind any event to Galleria
2010 
2011         @param {String} type The Event type to listen for
2012         @param {Function} fn The function to execute when the event is triggered
2013 
2014         @example this.bind( Galleria.IMAGE, function() { Galleria.log('image shown') });
2015 
2016         @returns {Galleria}
2017     */
2018 
2019     bind : function(type, fn) {
2020         this.$( 'container' ).bind( type, this.proxy(fn) );
2021         return this;
2022     },
2023 
2024     /**
2025         Unbind any event to Galleria
2026 
2027         @param {String} type The Event type to forget
2028 
2029         @returns {Galleria}
2030     */
2031 
2032     unbind : function(type) {
2033         this.$( 'container' ).unbind( type );
2034         return this;
2035     },
2036 
2037     /**
2038         Manually trigger a Galleria event
2039 
2040         @param {String} type The Event to trigger
2041 
2042         @returns {Galleria}
2043     */
2044 
2045     trigger : function( type ) {
2046         type = typeof type == 'object' ?
2047             $.extend( type, { scope: this } ) :
2048             { type: type, scope: this };
2049         this.$( 'container' ).trigger( type );
2050         return this;
2051     },
2052 
2053     /**
2054         Assign an "idle state" to any element.
2055         The idle state will be applied after a certain amount of idle time
2056         Useful to hide f.ex navigation when the gallery is inactive
2057 
2058         @param {HTML Element or String} elem The Dom node or selector to apply the idle state to
2059         @param {Object} styles the CSS styles to apply
2060 
2061         @example addIdleState( this.get('image-nav'), { opacity: 0 });
2062         @example addIdleState( '.galleria-image-nav', { top: -200 });
2063 
2064         @returns {Galleria}
2065     */
2066 
2067     addIdleState: function( elem, styles ) {
2068         this._idle.add.apply( this._idle, Utils.array( arguments ) );
2069         return this;
2070     },
2071 
2072     /**
2073         Removes any idle state previously set using addIdleState()
2074 
2075         @param {HTML Element or String} elem The Dom node or selector to remove the idle state from.
2076 
2077         @returns {Galleria}
2078     */
2079 
2080     removeIdleState: function( elem ) {
2081         this._idle.remove.apply( this._idle, Utils.array( arguments ) );
2082         return this;
2083     },
2084 
2085     /**
2086         Force Galleria to enter idle mode.
2087 
2088         @returns {Galleria}
2089     */
2090 
2091     enterIdleMode: function() {
2092         this._idle.hide();
2093         return this;
2094     },
2095 
2096     /**
2097         Force Galleria to exit idle mode.
2098 
2099         @returns {Galleria}
2100     */
2101 
2102     exitIdleMode: function() {
2103         this.idle._show();
2104         return this;
2105     },
2106 
2107     /**
2108         Enter FullScreen mode
2109 
2110         @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
2111 
2112         @returns {Galleria}
2113     */
2114 
2115     enterFullscreen: function( callback ) {
2116         this._fullscreen.enter.apply( this, Utils.array( arguments ) );
2117         return this;
2118     },
2119 
2120     /**
2121         Exits FullScreen mode
2122 
2123         @param {Function} callback the function to be executed when the fullscreen mode is fully applied.
2124 
2125         @returns {Galleria}
2126     */
2127 
2128     exitFullscreen: function( callback ) {
2129         this._fullscreen.exit.apply( this, Utils.array( arguments ) );
2130         return this;
2131     },
2132 
2133     /**
2134         Adds a tooltip to any element.
2135         You can also call this method with an object as argument with elemID:value pairs to apply tooltips to (see examples)
2136 
2137         @param {HTML Element} elem The DOM Node to attach the event to
2138         @param {String or Function} value The tooltip message. Can also be a function that returns a string.
2139 
2140         @example this.bindTooltip( this.get('thumbnails'), 'My thumbnails');
2141         @example this.bindTooltip( this.get('thumbnails'), function() { return 'My thumbs' });
2142         @example this.bindTooltip( { image_nav: 'Navigation' });
2143 
2144         @returns {Galleria}
2145     */
2146 
2147     bindTooltip: function( elem, value ) {
2148         this._tooltip.bind.apply( this._tooltip, Utils.array(arguments) );
2149         return this;
2150     },
2151 
2152     /**
2153         Redefine a tooltip
2154         Use this if you want to change the tooltip value at runtime
2155 
2156         @param {HTML Element} elem The DOM Node to attach the event to
2157         @param {String or Function} value The tooltip message. Can also be a function that returns a string.
2158 
2159         @returns {Galleria}
2160     */
2161 
2162     defineTooltip: function( elem, value ) {
2163         this._tooltip.define.apply( this._tooltip, Utils.array(arguments) );
2164         return this;
2165     },
2166 
2167     // leave this out of the API for now
2168 
2169     refreshTooltip: function() {
2170         this._tooltip.refresh.apply( this._tooltip, Utils.array(arguments) );
2171         return this;
2172     },
2173 
2174     /**
2175         Open a pre-designed lightbox with the currently active image.
2176         You can control some visuals using gallery options.
2177 
2178         @returns {Galleria}
2179     */
2180 
2181     openLightbox: function() {
2182         this._lightbox.show.apply( this._lightbox, Utils.array( arguments ) );
2183         return this;
2184     },
2185 
2186     /**
2187         Close the lightbox.
2188 
2189         @returns {Galleria}
2190     */
2191 
2192     closeLightbox: function() {
2193         this._lightbox.hide.apply( this._lightbox, Utils.array( arguments ) );
2194         return this;
2195     },
2196 
2197     /**
2198         Get the currently active image element.
2199 
2200         @returns {HTML Element} The image element
2201     */
2202 
2203     getActiveImage: function() {
2204         return this._getActive().image || undef;
2205     },
2206 
2207     /**
2208         Get the currently active thumbnail element.
2209 
2210         @returns {HTML Element} The thumbnail element
2211     */
2212 
2213     getActiveThumb: function() {
2214         return this._thumbnails[ this._active ].image || undef;
2215     },
2216 
2217     /**
2218         Get the mouse position relative to the gallery container
2219 
2220         @param e The mouse event
2221 
2222         @example
2223 
2224 var gallery = this;
2225 $(document).mousemove(function(e) {
2226     console.log( gallery.getMousePosition(e).x );
2227 });
2228 
2229         @returns {Object} Object with x & y of the relative mouse postion
2230     */
2231 
2232     getMousePosition : function(e) {
2233         return {
2234             x: e.pageX - this.$( 'stage' ).offset().left,
2235             y: e.pageY - this.$( 'stage' ).offset().top
2236         };
2237     },
2238 
2239     /**
2240         Adds a panning effect to the image
2241 
2242         @param img The optional image element. If not specified it takes the currently active image
2243 
2244         @returns {Galleria}
2245     */
2246 
2247     addPan : function( img ) {
2248 
2249         if ( this._options.image_crop === false ) {
2250             return;
2251         }
2252 
2253         img = $( img || this.getActiveImage() );
2254 
2255         // define some variables and methods
2256         var self   = this,
2257             x      = img.width() / 2,
2258             y      = img.height() / 2,
2259             curX   = destX = parseInt( img.css( 'left' ) ) || 0,
2260             curY   = destY = parseInt( img.css( 'top' ) ) || 0,
2261             distX  = 0,
2262             distY  = 0,
2263             active = false,
2264             ts     = Utils.timestamp(),
2265             cache  = 0,
2266             move   = 0,
2267 
2268             // positions the image
2269             position = function( dist, cur, pos ) {
2270                 if ( dist > 0 ) {
2271                     move = Math.round( Math.max( dist * -1, Math.min( 0, cur ) ) );
2272                     if ( cache != move ) {
2273 
2274                         cache = move;
2275 
2276                         if ( IE == 8 ) { // scroll is faster for IE
2277                             img.parent()[ 'scroll' + pos ]( move * -1 );
2278                         } else {
2279                             var css = {};
2280                             css[ pos.toLowerCase() ] = move;
2281                             img.css(css);
2282                         }
2283                     }
2284                 }
2285             },
2286 
2287             // calculates mouse position after 50ms
2288             calculate = function(e) {
2289                 if (Utils.timestamp() - ts < 50) {
2290                     return;
2291                 }
2292                 active = true;
2293                 x = self.getMousePosition(e).x;
2294                 y = self.getMousePosition(e).y;
2295             },
2296 
2297             // the main loop to check
2298             loop = function(e) {
2299 
2300                 if (!active) {
2301                     return;
2302                 }
2303 
2304                 distX = img.width() - self._stageWidth;
2305                 distY = img.height() - self._stageHeight;
2306                 destX = x / self._stageWidth * distX * -1;
2307                 destY = y / self._stageHeight * distY * -1;
2308                 curX += ( destX - curX ) / self._options.image_pan_smoothness;
2309                 curY += ( destY - curY ) / self._options.image_pan_smoothness;
2310 
2311                 position( distY, curY, 'Top' );
2312                 position( distX, curX, 'Left' );
2313 
2314             };
2315 
2316         // we need to use scroll in IE8 to speed things up
2317         if ( IE == 8 ) {
2318 
2319             img.parent().scrollTop( curY * -1 ).scrollLeft( curX * -1 );
2320             img.css({
2321                 top: 0,
2322                 left: 0
2323             });
2324 
2325         }
2326 
2327         // unbind and bind event
2328         this.$( 'stage' ).unbind( 'mousemove', calculate ).bind( 'mousemove', calculate );
2329 
2330         // loop the loop
2331         Utils.addTimer('pan', loop, 50, true);
2332 
2333         return this;
2334     },
2335 
2336     /**
2337         Brings the scope into any callback
2338 
2339         @param fn The callback to bring the scope into
2340         @param scope Optional scope to bring
2341 
2342         @example $('#fullscreen').click( this.proxy(function() { this.enterFullscreen(); }) )
2343 
2344         @returns {Function} Return the callback with the gallery scope
2345     */
2346 
2347     proxy : function( fn, scope ) {
2348         if ( typeof fn !== 'function' ) {
2349             return function() {};
2350         }
2351         scope = scope || this;
2352         return function() {
2353             return fn.apply( scope, Utils.array( arguments ) );
2354         };
2355     },
2356 
2357     /**
2358         Removes the panning effect set by addPan()
2359 
2360         @returns {Galleria}
2361     */
2362 
2363     removePan: function() {
2364 
2365         if ( IE == 8 ) {
2366             // todo: doublecheck this
2367         }
2368         this.$( 'stage' ).unbind( 'mousemove' );
2369 
2370         Utils.clearTimer('pan');
2371 
2372         this.rescale();
2373 
2374         return this;
2375     },
2376 
2377     /**
2378         Adds an element to the Galleria DOM array.
2379         When you add an element here, you can access it using element ID in many API calls
2380 
2381         @param {String} id The element ID you wish to use. You can add many elements by adding more arguments.
2382 
2383         @example addElement('mybutton');
2384         @example addElement('mybutton','mylink');
2385 
2386         @returns {Galleria}
2387     */
2388 
2389     addElement : function( id ) {
2390 
2391         var dom = this._dom;
2392 
2393         $.each( Utils.array(arguments), function( i, blueprint ) {
2394            dom[ blueprint ] = Utils.create( 'galleria-' + blueprint );
2395         });
2396 
2397         return this;
2398     },
2399 
2400     /**
2401         Attach keyboard events to Galleria
2402 
2403         @param {Object} map The map object of events.
2404         Possible keys are 'UP', 'DOWN', 'LEFT', 'RIGHT', 'RETURN', 'ESCAPE' and 'BACKSPACE'.
2405 
2406         @example
2407 
2408 this.attachKeyboard({
2409     right: this.next,
2410     left: this.prev,
2411     up: function() {
2412         console.log( 'up key pressed' )
2413     }
2414 });
2415 
2416         @returns {Galleria}
2417     */
2418 
2419     attachKeyboard : function( map ) {
2420         this._keyboard.attach.apply( this._keyboard, Utils.array( arguments ) );
2421         return this;
2422     },
2423 
2424     /**
2425         Detach all keyboard events to Galleria
2426 
2427         @returns {Galleria}
2428     */
2429 
2430     detachKeyboard : function() {
2431         this._keyboard.detach.apply( this._keyboard, Utils.array( arguments ) );
2432         return this;
2433     },
2434 
2435     /**
2436         Fast helper for appending galleria elements that you added using addElement()
2437 
2438         @param {String} parentID The parent element ID where the element will be appended
2439         @param {String} childID the element ID that should be appended
2440 
2441         @example this.addElement('myElement');
2442         this.appendChild( 'info', 'myElement' );
2443 
2444         @returns {Galleria}
2445     */
2446 
2447     appendChild : function( parentID, childID ) {
2448         this.$( parentID ).append( this.get( childID ) || childID );
2449         return this;
2450     },
2451 
2452     /**
2453         Fast helper for appending galleria elements that you added using addElement()
2454 
2455         @param {String} parentID The parent element ID where the element will be preppended
2456         @param {String} childID the element ID that should be preppended
2457 
2458         @example
2459 
2460 this.addElement('myElement');
2461 this.prependChild( 'info', 'myElement' );
2462 
2463         @returns {Galleria}
2464     */
2465 
2466     prependChild : function( parenID, childID ) {
2467         this.$( parentID ).prepend( this.get( childID ) || childID );
2468         return this;
2469     },
2470 
2471     /**
2472         Remove an element by blueprint
2473 
2474         @param {String} elemID The element to be removed.
2475         You can remove multiple elements by adding arguments.
2476 
2477         @returns {Galleria}
2478     */
2479 
2480     remove : function( elemID ) {
2481         this.$( Utils.array( arguments ).join(',') ).remove();
2482         return this;
2483     },
2484 
2485     // a fast helper for building dom structures
2486     // leave this out of the API for now
2487 
2488     append : function( data ) {
2489         for( var i in data) {
2490             if ( data[i].constructor == Array ) {
2491                 for( var j = 0; data[i][j]; j++ ) {
2492                     this.appendChild( i, data[i][j] );
2493                 }
2494             } else {
2495                 this.appendChild( i, data[i] );
2496             }
2497         }
2498         return this;
2499     },
2500 
2501     // an internal helper for scaling according to options
2502     _scaleImage : function( image, options ) {
2503 
2504         options = $.extend({
2505             width:    this._stageWidth,
2506             height:   this._stageHeight,
2507             crop:     this._options.image_crop,
2508             max:      this._options.max_scale_ratio,
2509             min:      this._options.min_scale_ratio,
2510             margin:   this._options.image_margin,
2511             position: this._options.image_position
2512         }, options );
2513 
2514        ( image || this._controls.getActive() ).scale( options );
2515 
2516         return this;
2517     },
2518 
2519     /**
2520         Updates the carousel,
2521         useful if you resize the gallery and want to re-check if the carousel nav is needed.
2522 
2523         @returns {Galleria}
2524     */
2525 
2526     updateCarousel : function() {
2527         this._carousel.update();
2528         return this;
2529     },
2530 
2531     /**
2532         Rescales the gallery
2533 
2534         @param {Number} width The target width
2535         @param {Number} height The target height
2536         @param {Function} complete The callback to be called when the scaling is complete
2537 
2538         @returns {Galleria}
2539     */
2540 
2541     rescale : function( width, height, complete ) {
2542 
2543         var self = this;
2544 
2545         // allow rescale(fn)
2546         if ( typeof width == 'function' ) {
2547             complete = width;
2548             width = undef;
2549         }
2550 
2551         var scale = function() {
2552 
2553             // shortcut
2554             var o = self._options;
2555 
2556             // set stagewidth
2557             self._stageWidth = width || self.$( 'stage' ).width();
2558             self._stageHeight = height || self.$( 'stage' ).height();
2559 
2560             // scale the active image
2561             self._scaleImage();
2562 
2563             if ( self._options.carousel ) {
2564                 self.updateCarousel();
2565             }
2566 
2567             self.trigger( Galleria.RESCALE );
2568 
2569             if ( typeof complete == 'function' ) {
2570                 complete.call( self );
2571             }
2572         };
2573 
2574         if ( Galleria.WEBKIT && !width && !height ) {
2575             Utils.addTimer( 'scale', scale, 5 );// webkit is too fast
2576         } else {
2577             scale.call( self );
2578         }
2579 
2580         return this;
2581     },
2582     
2583     /**
2584         Refreshes the gallery.
2585         Useful if you change image options at runtime and want to apply the changes to the active image.
2586 
2587         @returns {Galleria}
2588     */
2589     
2590     refreshImage : function() {
2591         this._scaleImage();
2592         if ( this._options.image_pan ) {
2593             this.addPan();
2594         }
2595         return this;
2596     },
2597 
2598     /**
2599         Shows an image by index
2600 
2601         @param {Number} index The index to show
2602         @param {Boolean} rewind A boolean that should be true if you want the transition to go back
2603 
2604         @returns {Galleria}
2605     */
2606 
2607     show : function( index, rewind, _history ) {
2608         // do nothing if queue is false and transition is in progress
2609         if ( !this._options.queue && this._queue.stalled ) {
2610             return;
2611         }
2612         index = Math.max( 0, Math.min( parseInt(index), this.getDataLength() - 1 ) );
2613 
2614         rewind = typeof rewind != 'undefined' ? !!rewind : index < this.getIndex();
2615 
2616         _history = _history || false;
2617 
2618         // do the history thing and return
2619         if ( !_history && Galleria.History ) {
2620             Galleria.History.value( index.toString() );
2621             return;
2622         }
2623 
2624         this._active = index;
2625 
2626         Array.prototype.push.call( this._queue, {
2627             index : index,
2628             rewind : rewind
2629         });
2630         if ( !this._queue.stalled ) {
2631             this._show();
2632         }
2633 
2634         return this;
2635     },
2636 
2637     // the internal _show method does the actual showing
2638     _show : function() {
2639 
2640         // shortcuts
2641         var self   = this,
2642             queue  = this._queue[ 0 ],
2643             data   = this.getData( queue.index ),
2644             src    = data.image,
2645             active = this._controls.getActive(),
2646             next   = this._controls.getNext(),
2647             cached = next.isCached( src ),
2648             thumb  = this._thumbnails[ queue.index ];
2649 
2650             // to be fired when loading & transition is complete:
2651         var complete = function() {
2652 
2653             // remove stalled
2654             self._queue.stalled = false;
2655 
2656             // optimize quality
2657             Utils.toggleQuality( next.image, self._options.image_quality );
2658 
2659             // swap
2660             $( active.container ).css({
2661                 zIndex: 0,
2662                 opacity: 0
2663             });
2664             $( next.container ).css({
2665                 zIndex: 1,
2666                 opacity: 1
2667             });
2668             self._controls.swap();
2669 
2670             // add pan according to option
2671             if ( self._options.image_pan ) {
2672                 self.addPan( next.image );
2673             }
2674 
2675             // make the image link
2676             if ( data.link ) {
2677                 $( next.image ).css({
2678                     cursor: 'pointer'
2679                 }).bind( CLICK(), function() {
2680 
2681                     // popup link
2682                     if ( self._options.popup_links ) {
2683                         var win = window.open( data.link, '_blank' );
2684                     } else {
2685                         window.location.href = data.link;
2686                     }
2687                 });
2688             }
2689 
2690             // remove the queued image
2691             Array.prototype.shift.call( self._queue );
2692 
2693             // if we still have images in the queue, show it
2694             if ( self._queue.length ) {
2695                 self._show();
2696             }
2697 
2698             // check if we are playing
2699             self._playCheck();
2700 
2701             // trigger IMAGE event
2702             self.trigger({
2703                 type:        Galleria.IMAGE,
2704                 index:       queue.index,
2705                 imageTarget: next.image,
2706                 thumbTarget: thumb.image
2707             });
2708         };
2709 
2710         // let the carousel follow
2711         if ( this._options.carousel && this._options.carousel_follow ) {
2712             this._carousel.follow( queue.index );
2713         }
2714 
2715         // preload images
2716         if ( this._options.preload ) {
2717 
2718             var p,
2719                 n = this.getNext();
2720 
2721             try {
2722                 for ( var i = this._options.preload; i > 0; i-- ) {
2723                     p = new Galleria.Picture();
2724                     p.add( self.getData( n ).image );
2725                     n = self.getNext( n );
2726                 }
2727             } catch(e) {}
2728         }
2729 
2730         // show the next image, just in case
2731         Utils.show( next.container );
2732 
2733         // add active classes
2734         $( self._thumbnails[ queue.index ].container )
2735             .addClass( 'active' )
2736             .siblings( '.active' )
2737             .removeClass( 'active' );
2738 
2739         // trigger the LOADSTART event
2740         self.trigger( {
2741             type: Galleria.LOADSTART,
2742             cached: cached,
2743             index: queue.index,
2744             imageTarget: next.image,
2745             thumbTarget: thumb.image
2746         });
2747 
2748         // begin loading the next image
2749         next.load( src, function( next ) {
2750             self._scaleImage( next, {
2751 
2752                 complete: function( next ) {
2753 
2754                     Utils.show( next.container );
2755 
2756                     // toggle low quality for IE
2757                     if ( 'image' in active ) {
2758                         Utils.toggleQuality( active.image, false );
2759                     }
2760                     Utils.toggleQuality( next.image, false );
2761 
2762                     // stall the queue
2763                     self._queue.stalled = true;
2764 
2765                     // remove the image panning, if applied
2766                     self.removePan();
2767 
2768                     // set the captions and counter
2769                     self.setInfo( queue.index );
2770                     self.setCounter( queue.index );
2771 
2772                     // trigger the LOADFINISH event
2773                     self.trigger({
2774                         type: Galleria.LOADFINISH,
2775                         cached: cached,
2776                         index: queue.index,
2777                         imageTarget: next.image,
2778                         thumbTarget: self._thumbnails[ queue.index ].image
2779                     });
2780 
2781                     var transition = active.image === null && self._options.transition_initial ?
2782                         self._options.transition_initial : self._options.transition;
2783 
2784                     // validate the transition
2785                     if ( transition in _transitions === false ) {
2786 
2787                         complete();
2788 
2789                     } else {
2790                         var params = {
2791                             prev:   active.image,
2792                             next:   next.image,
2793                             rewind: queue.rewind,
2794                             speed:  self._options.transition_speed || 400
2795                         };
2796 
2797                         // call the transition function and send some stuff
2798                         _transitions[ transition ].call(self, params, complete );
2799 
2800                     }
2801                 }
2802             });
2803         });
2804     },
2805 
2806     /**
2807         Gets the next index
2808 
2809         @param {Number} base Optional starting point
2810 
2811         @returns {Number} the next index, or the first if you are at the first (looping)
2812     */
2813 
2814     getNext : function( base ) {
2815         base = typeof base == 'number' ? base : this.getIndex();
2816         return base == this.getDataLength() - 1 ? 0 : base + 1;
2817     },
2818 
2819     /**
2820         Gets the previous index
2821 
2822         @param {Number} base Optional starting point
2823 
2824         @returns {Number} the previous index, or the last if you are at the first (looping)
2825     */
2826 
2827     getPrev : function( base ) {
2828         base = typeof base == 'number' ? base : this.getIndex();
2829         return base === 0 ? this.getDataLength() - 1 : base - 1;
2830     },
2831 
2832     /**
2833         Shows the next image in line
2834 
2835         @returns {Galleria}
2836     */
2837 
2838     next : function() {
2839         if ( this.getDataLength() > 1 ) {
2840             this.show( this.getNext(), false );
2841         }
2842         return this;
2843     },
2844 
2845     /**
2846         Shows the previous image in line
2847 
2848         @returns {Galleria}
2849     */
2850 
2851     prev : function() {
2852         if ( this.getDataLength() > 1 ) {
2853             this.show( this.getPrev(), true );
2854         }
2855         return this;
2856     },
2857 
2858     /**
2859         Retrieve a DOM element by element ID
2860 
2861         @param {String} elemId The delement ID to fetch
2862 
2863         @returns {HTML Element} The elements DOM node or null if not found.
2864     */
2865 
2866     get : function( elemId ) {
2867         return elemId in this._dom ? this._dom[ elemId ] : null;
2868     },
2869 
2870     /**
2871         Retrieve a data object
2872 
2873         @param {Number} index The data index to retrieve.
2874         If no index specified it will take the currently active image
2875 
2876         @returns {Object} The data object
2877     */
2878 
2879     getData : function( index ) {
2880         return index in this._data ?
2881             this._data[ index ] : this._data[ this._active ];
2882     },
2883 
2884     /**
2885         Retrieve the number of data items
2886 
2887         @returns {Number} The data length
2888     */
2889     getDataLength : function() {
2890         return this._data.length;
2891     },
2892 
2893     /**
2894         Retrieve the currently active index
2895 
2896         @returns {Number} The active index
2897     */
2898 
2899     getIndex : function() {
2900         return typeof this._active === 'number' ? this._active : 0;
2901     },
2902 
2903     /**
2904         Retrieve the stage height
2905 
2906         @returns {Number} The stage height
2907     */
2908 
2909     getStageHeight : function() {
2910         return this._stageHeight;
2911     },
2912 
2913     /**
2914         Retrieve the stage width
2915 
2916         @returns {Number} The stage width
2917     */
2918 
2919     getStageWidth : function() {
2920         return this._stageWidth;
2921     },
2922 
2923     /**
2924         Retrieve the option
2925 
2926         @param {String} key The option key to retrieve. If no key specified it will return all options in an object.
2927 
2928         @returns option or options
2929     */
2930 
2931     getOptions : function( key ) {
2932         return typeof key == 'undefined' ? this._options : this._options[ key ];
2933     },
2934 
2935     /**
2936         Set options to the instance.
2937         You can set options using a key & value argument or a single object argument (see examples)
2938 
2939         @param {String} key The option key
2940         @param {String} value the the options value
2941 
2942         @example setOptions( 'autoplay', true )
2943         @example setOptions({ autoplay: true });
2944 
2945         @returns {Galleria}
2946     */
2947 
2948     setOptions : function( key, value ) {
2949         if ( typeof key == 'object' ) {
2950             $.extend( this._options, key );
2951         } else {
2952             this._options[ key ] = value;
2953         }
2954         return this;
2955     },
2956 
2957     /**
2958         Starts playing the slideshow
2959 
2960         @param {Number} delay Sets the slideshow interval in milliseconds.
2961         If you set it once, you can just call play() and get the same interval the next time.
2962 
2963         @returns {Galleria}
2964     */
2965 
2966     play : function( delay ) {
2967 
2968         this.trigger( Galleria.PLAY );
2969 
2970         this._playing = true;
2971         this._playtime = delay || this._playtime;
2972 
2973         this._playCheck();
2974 
2975         return this;
2976     },
2977 
2978     /**
2979         Stops the slideshow if currently playing
2980 
2981         @returns {Galleria}
2982     */
2983 
2984     pause : function() {
2985         this.trigger( Galleria.PAUSE );
2986         this._playing = false;
2987         return this;
2988     },
2989 
2990     _playCheck : function() {
2991         var self = this,
2992             played = 0,
2993             interval = 20,
2994             now = Utils.timestamp();
2995 
2996         if ( this._playing ) {
2997 
2998             Utils.clearTimer('play');
2999             var fn = function() {
3000 
3001                 played = Utils.timestamp() - now;
3002                 if ( played >= self._playtime && self._playing ) {
3003                     Utils.clearTimer('play');
3004                     self.next();
3005                     return;
3006                 }
3007                 if ( self._playing ) {
3008 
3009                     // trigger the PROGRESS event
3010                     self.trigger({
3011                         type:         Galleria.PROGRESS,
3012                         percent:      Math.ceil( played / self._playtime * 100 ),
3013                         seconds:      Math.floor( played / 1000 ),
3014                         milliseconds: played
3015                     });
3016 
3017                     Utils.addTimer( 'play', fn, interval );
3018                 }
3019             };
3020             Utils.addTimer( 'play', fn, interval );
3021         }
3022     },
3023 
3024     setIndex: function( val ) {
3025         this._active = val;
3026         return this;
3027     },
3028 
3029     /**
3030         Manually modify the counter
3031 
3032         @param {Number} index Optional data index to fectch,
3033         if no index found it assumes the currently active index
3034 
3035         @returns {Galleria}
3036     */
3037 
3038     setCounter: function( index ) {
3039 
3040         if ( typeof index == 'number' ) {
3041             index++;
3042         } else if ( typeof index == 'undefined' ) {
3043             index = this.getIndex()+1;
3044         }
3045 
3046         this.get( 'current' ).innerHTML = index;
3047 
3048         if ( IE == 8 ) { // weird IE8 bug
3049 
3050             var opacity = this.$( 'counter' ).css( 'opacity' );
3051             this.$( 'counter' ).css( 'opacity', opacity );
3052 
3053         }
3054 
3055         return this;
3056     },
3057 
3058     /**
3059         Manually set captions
3060 
3061         @param {Number} index Optional data index to fectch and apply as caption,
3062         if no index found it assumes the currently active index
3063 
3064         @returns {Galleria}
3065     */
3066 
3067     setInfo : function( index ) {
3068 
3069         var self = this,
3070             data = this.getData( index );
3071 
3072         $.each( ['title','description','author'], function( i, type ) {
3073 
3074             var elem = self.$( 'info-' + type );
3075 
3076             if ( !!data[type] ) {
3077                 elem[ data[ type ].length ? 'show' : 'hide' ]().html( data[ type ] );
3078             } else {
3079                elem.empty().hide();
3080             }
3081         });
3082 
3083         return this;
3084     },
3085 
3086     /**
3087         Checks if the data contains any captions
3088 
3089         @param {Number} index Optional data index to fectch,
3090         if no index found it assumes the currently active index.
3091 
3092         @returns {Boolean}
3093     */
3094 
3095     hasInfo : function( index ) {
3096 
3097         var d = this.getData( index );
3098         var check = 'title description'.split(' ');
3099         for ( var i = 0; check[i]; i++ ) {
3100             if ( !!this.getData( index )[ check[i] ] ) {
3101                 return true;
3102             }
3103         }
3104         return false;
3105 
3106     },
3107 
3108     jQuery : function( str ) {
3109 
3110         var self = this,
3111             ret = [];
3112 
3113         $.each( str.split(','), function( i, elemId ) {
3114             elemId = $.trim( elemId );
3115 
3116             if ( self.get( elemId ) ) {
3117                 ret.push( elemId );
3118             }
3119         });
3120 
3121         var jQ = $( self.get( ret.shift() ) );
3122 
3123         $.each( ret, function( i, elemId ) {
3124             jQ = jQ.add( self.get( elemId ) );
3125         });
3126 
3127         return jQ;
3128 
3129     },
3130 
3131     /**
3132         Converts element IDs into a jQuery collection
3133         You can call for multiple IDs separated with commas.
3134 
3135         @param {String} str One or more element IDs (comma-separated)
3136 
3137         @returns {jQuery}
3138 
3139         @example this.$('info,container').hide();
3140     */
3141 
3142     $ : function() {
3143         return this.jQuery.apply( this, Utils.array( arguments ) );
3144     }
3145 
3146 };
3147 
3148 // End of Galleria prototype
3149 
3150 $.extend( Galleria, {
3151 
3152     // Event placeholders
3153     DATA:             'g_data',
3154     READY:            'g_ready',
3155     THUMBNAIL:        'g_thumbnail',
3156     LOADSTART:        'g_loadstart',
3157     LOADFINISH:       'g_loadfinish',
3158     IMAGE:            'g_image',
3159     THEMELOAD:        'g_themeload',
3160     PLAY:             'g_play',
3161     PAUSE:            'g_pause',
3162     PROGRESS:         'g_progress',
3163     FULLSCREEN_ENTER: 'g_fullscreen_enter',
3164     FULLSCREEN_EXIT:  'g_fullscreen_exit',
3165     IDLE_ENTER:       'g_idle_enter',
3166     IDLE_EXIT:        'g_idle_exit',
3167     RESCALE:          'g_rescale',
3168     LIGHTBOX_OPEN:    'g_lightbox_open',
3169     LIGHTBOX_CLOSE:   'g_lightbox_close',
3170     LIGHTBOX_IMAGE:   'g_lightbox_image',
3171 
3172     // Browser helpers
3173     IE9:     IE == 9,
3174     IE8:     IE == 8,
3175     IE7:     IE == 7,
3176     IE6:     IE == 6,
3177     IE:      !!IE,
3178     WEBKIT:  /webkit/.test( NAV ),
3179     SAFARI:  /safari/.test( NAV ),
3180     CHROME:  /chrome/.test( NAV ),
3181     QUIRK:   ( IE && doc.compatMode && doc.compatMode == "BackCompat" ),
3182     MAC:     /mac/.test( navigator.platform.toLowerCase() ),
3183     OPERA:   !!window.opera,
3184     IPHONE:  /iphone/.test( NAV ),
3185     IPAD:    /ipad/.test( NAV ),
3186     ANDROID: /android/.test( NAV ),
3187 
3188     // Todo detect touch devices in a better way, possibly using event detection
3189     TOUCH:   !!( /iphone/.test( NAV ) || /ipad/.test( NAV ) || /android/.test( NAV ) )
3190 
3191 });
3192 
3193 // Galleria static methods
3194 
3195 /**
3196     Adds a theme that you can use for your Gallery
3197 
3198     @param {Object} theme Object that should contain all your theme settings.
3199     <ul>
3200         <li>name – name of the theme</li>
3201         <li>author - name of the author</li>
3202         <li>version - version number</li>
3203         <li>css - css file name (not path)</li>
3204         <li>defaults - default options to apply, including theme-specific options</li>
3205         <li>init - the init function</li>
3206     </ul>
3207 
3208     @returns {Object} theme
3209 */
3210 
3211 Galleria.addTheme = function( theme ) {
3212 
3213     // make sure we have a name
3214     if ( !!theme['name'] === false ) {
3215         Galleria.raise('No theme name specified');
3216     }
3217 
3218     if ( typeof theme.defaults != 'object' ) {
3219         theme.defaults = {};
3220     }
3221 
3222     if ( typeof theme.css == 'string' ) {
3223 
3224         var css;
3225 
3226         // look for the absolute path
3227         $('script').each(function( i, script ) {
3228 
3229             // look for the theme script
3230             var reg = new RegExp( 'galleria\\.' + theme.name.toLowerCase() + '\\.' );
3231             if( reg.test( script.src )) {
3232 
3233                 // we have a match
3234                 css = script.src.replace(/[^\/]*$/, '') + theme.css;
3235 
3236                 Utils.addTimer( "css", function() {
3237                     Utils.loadCSS( css, 'galleria-theme', function() {
3238                         Galleria.theme = theme;
3239                         $doc.trigger( Galleria.THEMELOAD );
3240                     });
3241                 }, 1);
3242 
3243             }
3244         });
3245 
3246         if ( !css ) {
3247             Galleria.raise('No theme CSS loaded');
3248         }
3249     }
3250     return theme;
3251 };
3252 
3253 /**
3254     loadTheme loads a theme js file and attaches a load event to Galleria
3255 
3256     @param {String} src The relative path to the theme source file
3257 
3258     @param {Object} option Optional options you want to apply
3259 */
3260 
3261 Galleria.loadTheme = function( src, options ) {
3262 
3263     var loaded = false,
3264         length = _galleries.length;
3265 
3266     // first clear the current theme, if exists
3267     Galleria.theme = undef;
3268 
3269     // load the theme
3270     Utils.loadScript( src, function() {
3271         loaded = true;
3272     } );
3273 
3274     // set a 1 sec timeout, then display a hard error if no theme is loaded
3275     Utils.wait({
3276         until: function() {
3277             return loaded;
3278         },
3279         error: function() {
3280             Galleria.raise( "Theme at " + src + " could not load, check theme path.", true );
3281         },
3282         success: function() {
3283 
3284             // check for existing galleries and reload them with the new theme
3285             if ( length ) {
3286 
3287                 // temporary save the new galleries
3288                 var refreshed = [];
3289 
3290                 // refresh all instances
3291                 // when adding a new theme to an existing gallery, all options will be resetted but the data will be kept
3292                 // you can apply new options as a second argument
3293                 $.each( Galleria.get(), function(i, instance) {
3294 
3295                     // mix the old data and options into the new instance
3296                     var op = $.extend( instance._original.options, {
3297                         data_source: instance._data
3298                     }, options);
3299 
3300                     // remove the old container
3301                     instance.$('container').remove();
3302 
3303                     // create a new instance
3304                     var g = new Galleria();
3305 
3306                     // move the id
3307                     g._id = instance._id;
3308 
3309                     // initialize the new instance
3310                     g.init( instance._original.target, op );
3311 
3312                     // push the new instance
3313                     refreshed.push( g );
3314                 });
3315 
3316                 // now overwrite the old holder with the new instances
3317                 _galleries = refreshed;
3318             }
3319         },
3320         timeout: 1000
3321     });
3322 };
3323 
3324 /**
3325     Retrieves a Galleria instance.
3326 
3327     @param {Number} index Optional index to retrieve.
3328     If no index is supplied, the method will return all instances in an array.
3329 
3330     @returns {Galleria or Array}
3331 */
3332 
3333 Galleria.get = function( index ) {
3334     if ( !!_galleries[ index ] ) {
3335         return _galleries[ index ];
3336     } else if ( typeof index !== 'number' ) {
3337         return _galleries;
3338     } else {
3339         Galleria.raise('Gallery index ' + index + ' not found');
3340     }
3341 };
3342 
3343 /**
3344     Creates a transition to be used in your gallery
3345 
3346     @param {String} name The name of the transition that you will use as an option
3347 
3348     @param {Function} fn The function to be executed in the transition.
3349     The function contains two arguments, params and complete.
3350     Use the params Object to integrate the transition, and then call complete when you are done.
3351 
3352 */
3353 
3354 Galleria.addTransition = function( name, fn ) {
3355     _transitions[name] = fn;
3356 };
3357 
3358 Galleria.utils = Utils;
3359 
3360 /**
3361     A helper metod for cross-browser logging.
3362     It uses the console log if available otherwise it falls back to the opera
3363     debugger and finally <code>alert()</code>
3364 
3365     @example Galleria.log("hello", document.body, [1,2,3]);
3366 */
3367 
3368 Galleria.log = function() {
3369     try {
3370         window.console.log.apply( window.console, Utils.array(arguments) );
3371     } catch( e ) {
3372         try {
3373             opera.postError.apply( opera, arguments );
3374         } catch( er ) {
3375               alert( Utils.array(arguments).split(', ') );
3376         }
3377     }
3378 };
3379 
3380 /**
3381     Method for raising errors
3382 
3383     @param {String} msg The message to throw
3384 
3385     @param {Boolean} fatal Set this to true to override debug settings and display a fatal error
3386 */
3387 
3388 Galleria.raise = function( msg, fatal ) {
3389     if ( DEBUG || fatal ) {
3390         var type = fatal ? 'Fatal error' : 'Error';
3391         throw new Error( type + ': ' + msg );
3392     }
3393 };
3394 
3395 /**
3396     Adds preload, cache, scale and crop functionality
3397 
3398     @constructor
3399 
3400     @requires jQuery
3401 
3402     @param {Number} id Optional id to keep track of instances
3403 */
3404 
3405 Galleria.Picture = function( id ) {
3406 
3407     // save the id
3408     this.id = id || null;
3409 
3410     // the image should be null until loaded
3411     this.image = null;
3412 
3413     // Create a new container
3414     this.container = Utils.create('galleria-image');
3415 
3416     // add container styles
3417     $( this.container ).css({
3418         overflow: 'hidden',
3419         position: 'relative' // for IE Standards mode
3420     });
3421 
3422     // saves the original meassurements
3423     this.original = {
3424         width: 0,
3425         height: 0
3426     };
3427 
3428     // flag when the image is ready
3429     this.ready = false;
3430 
3431     // flag when the image is loaded
3432     this.loaded = false;
3433 
3434 };
3435 
3436 Galleria.Picture.prototype = {
3437 
3438     // the inherited cache object
3439     cache: {},
3440 
3441     // creates a new image and adds it to cache when loaded
3442     add: function( src ) {
3443 
3444         var self = this;
3445 
3446         // create the image
3447         var image = new Image();
3448 
3449         // force a block display
3450         $( image ).css( 'display', 'block');
3451 
3452         if ( self.cache[ src ] ) {
3453             // no need to onload if the image is cached
3454             image.src = src;
3455             self.loaded = true;
3456             self.original = {
3457                 height: image.height,
3458                 width: image.width
3459             };
3460             return image;
3461         }
3462 
3463         // begin preload and insert in cache when done
3464         image.onload = function() {
3465             self.original = {
3466                 height: this.height,
3467                 width: this.width
3468             };
3469             self.cache[ src ] = src; // will override old cache
3470             self.loaded = true;
3471         };
3472 
3473         image.src = src;
3474         return image;
3475 
3476     },
3477 
3478     // show the image on stage
3479     show: function() {
3480         Utils.show( this.image );
3481     },
3482 
3483     // hide the image
3484     hide: function() {
3485         Utils.moveOut( this.image );
3486     },
3487 
3488     clear: function() {
3489         this.image = null;
3490     },
3491 
3492     /**
3493         Checks if an image is in cache
3494 
3495         @param {String} src The image source path, ex '/path/to/img.jpg'
3496 
3497         @returns {Boolean}
3498     */
3499 
3500     isCached: function( src ) {
3501         return !!this.cache[src];
3502     },
3503 
3504     /**
3505         Loads an image and call the callback when ready.
3506         Will also add the image to cache.
3507 
3508         @param {String} src The image source path, ex '/path/to/img.jpg'
3509         @param {Function} callback The function to be executed when the image is loaded & scaled
3510 
3511         @returns {jQuery} The image container object
3512     */
3513 
3514     load: function(src, callback) {
3515 
3516         // save the instance
3517         var self = this;
3518 
3519         $( this.container ).empty(true);
3520 
3521         // add the image to cache and hide it
3522         this.image = this.add( src );
3523         Utils.hide( this.image );
3524 
3525         // append the image into the container
3526         $( this.container ).append( this.image );
3527 
3528         // check for loaded image using a timeout
3529         Utils.wait({
3530             until: function() {
3531                 // TODO this should be properly tested in Opera
3532                 return self.loaded && self.image.complete && self.image.width;
3533             },
3534             success: function() {
3535                 // call success
3536                 window.setTimeout(function() { callback.call( self, self ); }, 1 );
3537             },
3538             error: function() {
3539                 window.setTimeout(function() { callback.call( self, self ); }, 1 );
3540                 Galleria.raise('image not loaded in 10 seconds: '+ src);
3541             },
3542             timeout: 10000
3543         });
3544 
3545         // return the container
3546         return this.container;
3547     },
3548 
3549     /**
3550         Scales and crops the image
3551 
3552         @param {Object} options The method takes an object with a number of options:
3553 
3554         <ul>
3555             <li>width - width of the container</li>
3556             <li>height - height of the container</li>
3557             <li>min - minimum scale ratio</li>
3558             <li>max - maximum scale ratio</li>
3559             <li>margin - distance in pixels from the image border to the container</li>
3560             <li>complete - a callback that fires when scaling is complete</li>
3561             <li>position - positions the image, works like the css background-image property.</li>
3562             <li>crop - defines how to crop. Can be true, false, 'width' or 'height'</li>
3563         </ul>
3564 
3565         @returns {jQuery} The image container object
3566     */
3567 
3568     scale: function( options ) {
3569 
3570         // extend some defaults
3571         options = $.extend({
3572             width: 0,
3573             height: 0,
3574             min: undef,
3575             max: undef,
3576             margin: 0,
3577             complete: function() {},
3578             position: 'center',
3579             crop: false
3580         }, options);
3581 
3582         // return the element if no image found
3583         if (!this.image) {
3584             return this.container;
3585         }
3586 
3587         // store locale variables of width & height
3588         var width,
3589             height,
3590             self = this,
3591             $container = $( self.container );
3592 
3593         // wait for the width/height
3594         Utils.wait({
3595             until: function() {
3596 
3597                 width  = options.width
3598                     || $container.width()
3599                     || Utils.parseValue( $container.css('width') );
3600 
3601                 height = options.height
3602                     || $container.height()
3603                     || Utils.parseValue( $container.css('height') );
3604 
3605                 return width && height;
3606             },
3607             success: function() {
3608                 // calculate some cropping
3609                 var newWidth = ( width - options.margin * 2 ) / self.original.width,
3610                     newHeight = ( height - options.margin * 2 ) / self.original.height,
3611                     cropMap = {
3612                         'true'  : Math.max( newWidth, newHeight ),
3613                         'width' : newWidth,
3614                         'height': newHeight,
3615                         'false' : Math.min( newWidth, newHeight )
3616                     },
3617                     ratio = cropMap[ options.crop.toString() ];
3618 
3619                 // allow max_scale_ratio
3620                 if ( options.max ) {
3621                     ratio = Math.min( options.max, ratio );
3622                 }
3623 
3624                 // allow min_scale_ratio
3625                 if ( options.min ) {
3626                     ratio = Math.max( options.min, ratio );
3627                 }
3628 
3629                 $( self.container ).width( width ).height( height );
3630 
3631                 // round up the width / height
3632                 $.each( ['width','height'], function( i, m ) {
3633                     $( self.image )[ m ]( self[ m ] = Math.ceil( self.original[ m ] * ratio ) );
3634                 });
3635 
3636                 // calculate image_position
3637                 var pos = {},
3638                     mix = {},
3639                     getPosition = function(value, meassure, margin) {
3640                         var result = 0;
3641                         if (/\%/.test(value)) {
3642                             var flt = parseInt(value) / 100;
3643                             result = Math.ceil( $( self.image )[ meassure ]() * -1 * flt + margin * flt );
3644                         } else {
3645                             result = Utils.parseValue( value );
3646                         }
3647                         return result;
3648                     },
3649                     positionMap = {
3650                         'top': { top: 0 },
3651                         'left': { left: 0 },
3652                         'right': { left: '100%' },
3653                         'bottom': { top: '100%' }
3654                     };
3655 
3656                 $.each( options.position.toLowerCase().split(' '), function( i, value ) {
3657                     if ( value == 'center' ) {
3658                         value = '50%';
3659                     }
3660                     pos[i ? 'top' : 'left'] = value;
3661                 });
3662 
3663                 $.each( pos, function( i, value ) {
3664                     if ( positionMap.hasOwnProperty( value ) ) {
3665                         $.extend( mix, positionMap[ value ] );
3666                     }
3667                 });
3668 
3669                 pos = pos.top ? $.extend( pos, mix ) : mix;
3670 
3671                 pos = $.extend({
3672                     top: '50%',
3673                     left: '50%'
3674                 }, pos);
3675 
3676                 // apply position
3677                 $( self.image ).css({
3678                     position : 'relative',
3679                     top :  getPosition(pos.top, 'height', height),
3680                     left : getPosition(pos.left, 'width', width)
3681                 });
3682 
3683                 // show the image
3684                 self.show();
3685 
3686                 // flag ready and call the callback
3687                 self.ready = true;
3688                 options.complete.call( self, self );
3689             },
3690             error: function() {
3691                 Galleria.raise('Could not scale image: '+self.image.src);
3692             },
3693             timeout: 1000
3694         });
3695         return this;
3696     }
3697 };
3698 
3699 // our own easings
3700 $.extend( $.easing, {
3701     galleria: function (_, t, b, c, d) {
3702         if ((t/=d/2) < 1) {
3703             return c/2*t*t*t*t + b;
3704         }
3705         return -c/2 * ((t-=2)*t*t*t - 2) + b;
3706     },
3707     galleriaIn: function (_, t, b, c, d) {
3708     return c*(t/=d)*t*t*t + b;
3709   },
3710   galleriaOut: function (_, t, b, c, d) {
3711     return -c * ((t=t/d-1)*t*t*t - 1) + b;
3712   }
3713 });
3714 
3715 // the plugin initializer
3716 $.fn.galleria = function( options ) {
3717 
3718     return this.each(function() {
3719 
3720         var gallery = new Galleria();
3721         gallery.init( this, options );
3722 
3723     });
3724 };
3725 
3726 // expose Galleria
3727 window.Galleria = Galleria;
3728 
3729 // phew
3730 
3731 })( jQuery );