aino/galleria — Issue #227: Update to mweichert's issue - #213 || If an image is taking longer to download than usual, then the "could not extract height/width" error was being displayed. It couldn't be extracted because the image wasn't done downloading yet.
Author: anandpathaksharma
Created: 2011-12-03 07:00:31+00:00
State: open

/**
* @preserve Galleria v 1.2.6a2 2011-08-08
* http://galleria.aino.se
*
* Copyright (c) 2011, Aino
* Licensed under the MIT license.
*/

/*global jQuery, navigator, Galleria:true, Image */

(function( $ ) {

// some references
var undef,
window = this,
doc = window.document,
$doc = $( doc ),
$win = $( window ),

// internal constants
VERSION = 1.26,
DEBUG = true,
TIMEOUT = 30000,
DUMMY = false,
NAV = navigator.userAgent.toLowerCase(),
HASH = window.location.hash.replace(/#\//, ''),
IE = (function() {

var v = 3,
div = doc.createElement( 'div' ),
all = div.getElementsByTagName( 'i' );

do {
div.innerHTML = '';
} while ( all[0] );

return v > 4 ? v : undef;

}() ),
DOM = function() {
return {
html: doc.documentElement,
body: doc.body,
head: doc.getElementsByTagName('head')[0],
title: doc.title
};
},

// list of Galleria events
_eventlist = 'data ready thumbnail loadstart loadfinish image play pause progress ' +
'fullscreen_enter fullscreen_exit idle_enter idle_exit rescale ' +
'lightbox_open lightbox_close lightbox_image',

_events = (function() {

var evs = [];

$.each( _eventlist.split(' '), function( i, ev ) {
evs.push( ev );

// legacy events
if ( /_/.test( ev ) ) {
evs.push( ev.replace( /_/g, '' ) );
}
});

return evs;

}()),

// legacy options
// allows the old my_setting syntax and converts it to camel case

_legacyOptions = function( options ) {

var n;

if ( typeof options !== 'object' ) {

// return whatever it was...
return options;
}

$.each( options, function( key, value ) {
if ( /^[a-z]+_/.test( key ) ) {
n = '';
$.each( key.split('_'), function( i, k ) {
n += i > 0 ? k.substr( 0, 1 ).toUpperCase() + k.substr( 1 ) : k;
});
options[ n ] = value;
delete options[ key ];
}
});

return options;
},

_patchEvent = function( type ) {

// allow 'image' instead of Galleria.IMAGE
if ( $.inArray( type, _events ) > -1 ) {
return Galleria[ type.toUpperCase() ];
}

return type;
},

// the internal timeouts object
// provides helper methods for controlling timeouts
_timeouts = {

trunk: {},

add: function( id, fn, delay, loop ) {
loop = loop || false;
this.clear( id );
if ( loop ) {
var old = fn;
fn = function() {
old();
_timeouts.add( id, fn, delay );
};
}
this.trunk[ id ] = window.setTimeout( fn, delay );
},

clear: function( id ) {

var del = function( i ) {
window.clearTimeout( this.trunk[ i ] );
delete this.trunk[ i ];
}, i;

if ( !!id && id in this.trunk ) {
del.call( _timeouts, id );

} else if ( typeof id === 'undefined' ) {
for ( i in this.trunk ) {
if ( this.trunk.hasOwnProperty( i ) ) {
del.call( _timeouts, i );
}
}
}
}
},

// the internal gallery holder
_galleries = [],

// the internal instance holder
_instances = [],

// flag for errors
_hasError = false,

// canvas holder
_canvas = false,

// instance pool, holds the galleries until themeLoad is triggered
_pool = [],

// themeLoad trigger
_themeLoad = function( theme ) {
Galleria.theme = theme;

// run the instances we have in the pool
$.each( _pool, function( i, instance ) {
if ( !instance._initialized ) {
instance._init.call( instance );
}
});
},

// the Utils singleton
Utils = (function() {

return {

array : function( obj ) {
return Array.prototype.slice.call(obj, 0);
},

create : function( className, nodeName ) {
nodeName = nodeName || 'div';
var elem = doc.createElement( nodeName );
elem.className = className;
return elem;
},

getScriptPath : function( src ) {

// the currently executing script is always the last
src = src || $('script:last').attr('src');
var slices = src.split('/');

if (slices.length == 1) {
return '';
}

slices.pop();

return slices.join('/') + '/';
},

// CSS3 transitions, added in 1.2.4
animate : (function() {

// detect transition
var transition = (function( style ) {
var props = 'transition WebkitTransition MozTransition OTransition'.split(' '),
i;

// disable css3 animations in opera until stable
if ( window.opera ) {
return false;
}

for ( i = 0; props[i]; i++ ) {
if ( typeof style[ props[ i ] ] !== 'undefined' ) {
return props[ i ];
}
}
return false;
}(( doc.body || doc.documentElement).style ));

// map transitionend event
var endEvent = {
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd',
WebkitTransition: 'webkitTransitionEnd',
transition: 'transitionend'
}[ transition ];

// map bezier easing conversions
var easings = {
_default: [0.25, 0.1, 0.25, 1],
galleria: [0.645, 0.045, 0.355, 1],
galleriaIn: [0.55, 0.085, 0.68, 0.53],
galleriaOut: [0.25, 0.46, 0.45, 0.94],
ease: [0.25, 0, 0.25, 1],
linear: [0.25, 0.25, 0.75, 0.75],
'ease-in': [0.42, 0, 1, 1],
'ease-out': [0, 0, 0.58, 1],
'ease-in-out': [0.42, 0, 0.58, 1]
};

// function for setting transition css for all browsers
var setStyle = function( elem, value, suffix ) {
var css = {};
suffix = suffix || 'transition';
$.each( 'webkit moz ms o'.split(' '), function() {
css[ '-' + this + '-' + suffix ] = value;
});
elem.css( css );
};

// clear styles
var clearStyle = function( elem ) {
setStyle( elem, 'none', 'transition' );
if ( Galleria.WEBKIT && Galleria.TOUCH ) {
setStyle( elem, 'translate3d(0,0,0)', 'transform' );
if ( elem.data('revert') ) {
elem.css( elem.data('revert') );
elem.data('revert', null);
}
}
};

// various variables
var change, strings, easing, syntax, revert, form, css;

// the actual animation method
return function( elem, to, options ) {

// extend defaults
options = $.extend({
duration: 400,
complete: function(){},
stop: false
}, options);

// cache jQuery instance
elem = $( elem );

if ( !options.duration ) {
elem.css( to );
options.complete.call( elem[0] );
return;
}

// fallback to jQuery's animate if transition is not supported
if ( !transition ) {
elem.animate(to, options);
return;
}

// stop
if ( options.stop ) {
// clear the animation
elem.unbind( endEvent );
clearStyle( elem );
}

// see if there is a change
change = false;
$.each( to, function( key, val ) {
css = elem.css( key );
if ( Utils.parseValue( css ) != Utils.parseValue( val ) ) {
change = true;
}
// also add computed styles for FF
elem.css( key, css );
});
if ( !change ) {
window.setTimeout( function() {
options.complete.call( elem[0] );
}, options.duration );
return;
}

// the css strings to be applied
strings = [];

// the easing bezier
easing = options.easing in easings ? easings[ options.easing ] : easings._default;

// the syntax
syntax = ' ' + options.duration + 'ms' + ' cubic-bezier(' + easing.join(',') + ')';

// add a tiny timeout so that the browsers catches any css changes before animating
window.setTimeout(function() {

// attach the end event
elem.one(endEvent, (function( elem ) {
return function() {
// clear the animation
clearStyle(elem);

// run the complete method
options.complete.call(elem[0]);
};
}( elem )));

// do the webkit translate3d for better performance on iOS
if( Galleria.WEBKIT && Galleria.TOUCH ) {

revert = {};
form = [0,0,0];

$.each( ['left', 'top'], function(i, m) {
if ( m in to ) {
form[ i ] = ( Utils.parseValue( to[ m ] ) - Utils.parseValue(elem.css( m )) ) + 'px';
revert[ m ] = to[ m ];
delete to[ m ];
}
});

if ( form[0] || form[1]) {

elem.data('revert', revert);

strings.push('-webkit-transform' + syntax);

// 3d animate
setStyle( elem, 'translate3d(' + form.join(',') + ')', 'transform');
}
}

// push the animation props
$.each(to, function( p, val ) {
strings.push(p + syntax);
});

// set the animation styles
setStyle( elem, strings.join(',') );

// animate
elem.css( to );

},1 );
};
}()),

removeAlpha : function( elem ) {
if ( IE < 9 && elem ) {

var style = elem.style,
currentStyle = elem.currentStyle,
filter = currentStyle && currentStyle.filter || style.filter || "";

if ( /alpha/.test( filter ) ) {
style.filter = filter.replace( /alpha\([^)]*\)/i, '' );
}
}
},

forceStyles : function( elem, styles ) {
elem = $(elem);
if ( elem.attr( 'style' ) ) {
elem.data( 'styles', elem.attr( 'style' ) ).removeAttr( 'style' );
}
elem.css( styles );
},

revertStyles : function() {
$.each( Utils.array( arguments ), function( i, elem ) {

elem = $( elem );
elem.removeAttr( 'style' );

elem.attr('style',''); // "fixes" webkit bug

if ( elem.data( 'styles' ) ) {
elem.attr( 'style', elem.data('styles') ).data( 'styles', null );
}
});
},

moveOut : function( elem ) {
Utils.forceStyles( elem, {
position: 'absolute',
left: -10000
});
},

moveIn : function() {
Utils.revertStyles.apply( Utils, Utils.array( arguments ) );
},

elem : function( elem ) {
if (elem instanceof $) {
return {
$: elem,
dom: elem[0]
};
} else {
return {
$: $(elem),
dom: elem
};
}
},

hide : function( elem, speed, callback ) {

callback = callback || function(){};

var el = Utils.elem( elem ),
$elem = el.$;

elem = el.dom;

// save the value if not exist
if (! $elem.data('opacity') ) {
$elem.data('opacity', $elem.css('opacity') );
}

// always hide
var style = { opacity: 0 };

if (speed) {

var complete = IE < 9 && elem ? function() {
Utils.removeAlpha( elem );
elem.style.visibility = 'hidden';
callback.call( elem );
} : callback;

Utils.animate( elem, style, {
duration: speed,
complete: complete,
stop: true
});
} else {
if ( IE < 9 && elem ) {
Utils.removeAlpha( elem );
elem.style.visibility = 'hidden';
} else {
$elem.css( style );
}
}
},

show : function( elem, speed, callback ) {

callback = callback || function(){};

var el = Utils.elem( elem ),
$elem = el.$;

elem = el.dom;

// bring back saved opacity
var saved = parseFloat( $elem.data('opacity') ) || 1,
style = { opacity: saved };

// animate or toggle
if (speed) {

if ( IE < 9 ) {
$elem.css('opacity', 0);
elem.style.visibility = 'visible';
}

var complete = IE < 9 && elem ? function() {
if ( style.opacity == 1 ) {
Utils.removeAlpha( elem );
}
callback.call( elem );
} : callback;

Utils.animate( elem, style, {
duration: speed,
complete: complete,
stop: true
});
} else {
if ( IE < 9 && style.opacity == 1 && elem ) {
Utils.removeAlpha( elem );
elem.style.visibility = 'visible';
} else {
$elem.css( style );
}
}
},

// enhanced click for mobile devices
// we bind a touchend and hijack any click event in the bubble
// then we execute the click directly and save it in a separate data object for later
optimizeTouch: (function() {

var node,
evs,
fakes,
travel,
evt = {},
handler = function( e ) {
e.preventDefault();
evt = $.extend({}, e, true);
},
attach = function() {
this.evt = evt;
},
fake = function() {
this.handler.call(node, this.evt);
};

return function( elem ) {

$(elem).bind('touchend', function( e ) {

node = e.target;
travel = true;

while( node.parentNode && node != e.currentTarget && travel ) {

evs = $(node).data('events');
fakes = $(node).data('fakes');

if (evs && 'click' in evs) {

travel = false;
e.preventDefault();

// fake the click and save the event object
$(node).click(handler).click();

// remove the faked click
evs.click.pop();

// attach the faked event
$.each( evs.click, attach);

// save the faked clicks in a new data object
$(node).data('fakes', evs.click);

// remove all clicks
delete evs.click;

} else if ( fakes ) {

travel = false;
e.preventDefault();

// fake all clicks
$.each( fakes, fake );
}

// bubble
node = node.parentNode;
}
});
};
}()),

addTimer : function() {
_timeouts.add.apply( _timeouts, Utils.array( arguments ) );
return this;
},

clearTimer : function() {
_timeouts.clear.apply( _timeouts, Utils.array( arguments ) );
return this;
},

wait : function(options) {
options = $.extend({
until : function() { return false; },
success : function() {},
error : function() { Galleria.raise('Could not complete wait function.'); },
timeout: 3000
}, options);

var start = Utils.timestamp(),
elapsed,
now,
fn = function() {
now = Utils.timestamp();
elapsed = now - start;
if ( options.until( elapsed ) ) {
options.success();
return false;
}

if (now >= start + options.timeout) {
options.error();
return false;
}
window.setTimeout(fn, 10);
};

window.setTimeout(fn, 10);
},

toggleQuality : function( img, force ) {

if ( ( IE !== 7 && IE !== 8 ) || !img ) {
return;
}

if ( typeof force === 'undefined' ) {
force = img.style.msInterpolationMode === 'nearest-neighbor';
}

img.style.msInterpolationMode = force ? 'bicubic' : 'nearest-neighbor';
},

insertStyleTag : function( styles ) {
var style = doc.createElement( 'style' );
DOM().head.appendChild( style );

if ( style.styleSheet ) { // IE
style.styleSheet.cssText = styles;
} else {
var cssText = doc.createTextNode( styles );
style.appendChild( cssText );
}
},

// a loadscript method that works for local scripts
loadScript: function( url, callback ) {

var done = false,
script = $('').attr({
src: url,
async: true
}).get(0);

// Attach handlers for all browsers
script.onload = script.onreadystatechange = function() {
if ( !done && (!this.readyState ||
this.readyState === 'loaded' || this.readyState === 'complete') ) {

done = true;

// Handle memory leak in IE
script.onload = script.onreadystatechange = null;

if (typeof callback === 'function') {
callback.call( this, this );
}
}
};

DOM().head.appendChild( script );
},

// parse anything into a number
parseValue: function( val ) {
if (typeof val === 'number') {
return val;
} else if (typeof val === 'string') {
var arr = val.match(/\-?\d|\./g);
return arr && arr.constructor === Array ? arr.join('')*1 : 0;
} else {
return 0;
}
},

// timestamp abstraction
timestamp: function() {
return new Date().getTime();
},

// this is pretty crap, but works for now
// it will add a callback, but it can't guarantee that the styles can be fetched
// using getComputedStyle further checking needed, possibly a dummy element
loadCSS : function( href, id, callback ) {

var link,
ready = false,
length;

// look for manual css
$('link[rel=stylesheet]').each(function() {
if ( new RegExp( href ).test( this.href ) ) {
link = this;
return false;
}
});

if ( typeof id === 'function' ) {
callback = id;
id = undef;
}

callback = callback || function() {}; // dirty

// if already present, return
if ( link ) {
callback.call( link, link );
return link;
}

// save the length of stylesheets to check against
length = doc.styleSheets.length;

// add timestamp if DEBUG is true
if ( DEBUG ) {
href += '?' + Utils.timestamp();
}

// check for existing id
if( $('#'+id).length ) {
$('#'+id).attr('href', href);
length--;
ready = true;
} else {
link = $( '' ).attr({
rel: 'stylesheet',
href: href,
id: id
}).get(0);

window.setTimeout(function() {
var styles = $('link[rel="stylesheet"], style');
if ( styles.length ) {
styles.get(0).parentNode.insertBefore( link, styles[0] );
} else {
DOM().head.appendChild( link );
}

if ( IE ) {

// IE has a limit of 31 stylesheets in one document
if( length >= 31 ) {
Galleria.raise( 'You have reached the browser stylesheet limit (31)', true );
return;
}

// todo: test if IE really needs the readyState
link.onreadystatechange = function(e) {
if ( !ready && (!this.readyState ||
this.readyState === 'loaded' || this.readyState === 'complete') ) {
ready = true;
}
};
} else {
// final test via ajax if not local
if ( !( new RegExp('file://','i').test( href ) ) ) {
$.ajax({
url: href,
success: function() {
ready = true;
},
error: function(e) {
// pass if origin is rejected in chrome for some reason
if( e.isRejected() && Galleria.WEBKIT ) {
ready = true;
}
}
});
} else {
ready = true;
}
}
}, 10);
}

if ( typeof callback === 'function' ) {

Utils.wait({
until: function() {
return ready && doc.styleSheets.length > length;
},
success: function() {
window.setTimeout( function() {
callback.call( link, link );
}, 100);
},
error: function() {
Galleria.raise( 'Theme CSS could not load', true );
},
timeout: 10000
});
}
return link;
}
};
}()),

// the transitions holder
_transitions = (function() {

var _slide = function(params, complete, fade, door) {

var easing = this.getOptions('easing'),
distance = this.getStageWidth(),
from = { left: distance * ( params.rewind ? -1 : 1 ) },
to = { left: 0 };

if ( fade ) {
from.opacity = 0;
to.opacity = 1;
} else {
from.opacity = 1;
}

$(params.next).css(from);

Utils.animate(params.next, to, {
duration: params.speed,
complete: (function( elems ) {
return function() {
complete();
elems.css({
left: 0
});
};
}( $( params.next ).add( params.prev ) )),
queue: false,
easing: easing
});

if (door) {
params.rewind = !params.rewind;
}

if (params.prev) {

from = { left: 0 };
to = { left: distance * ( params.rewind ? 1 : -1 ) };

if ( fade ) {
from.opacity = 1;
to.opacity = 0;
}

$(params.prev).css(from);
Utils.animate(params.prev, to, {
duration: params.speed,
queue: false,
easing: easing,
complete: function() {
$(this).css('opacity', 0);
}
});
}
};

return {

fade: function(params, complete) {
$(params.next).css('opacity',0).show();
Utils.animate(params.next, {
opacity: 1
},{
duration: params.speed,
complete: complete
});
if (params.prev) {
$(params.prev).css('opacity',1).show();
Utils.animate(params.prev, {
opacity: 0
},{
duration: params.speed
});
}
},

flash: function(params, complete) {
$(params.next).css('opacity', 0);
if (params.prev) {
Utils.animate( params.prev, {
opacity: 0
},{
duration: params.speed/2,
complete: function() {
Utils.animate( params.next, {
opacity:1
},{
duration: params.speed,
complete: complete
});
}
});
} else {
Utils.animate( params.next, {
opacity: 1
},{
duration: params.speed,
complete: complete
});
}
},

pulse: function(params, complete) {
if (params.prev) {
$(params.prev).hide();
}
$(params.next).css('opacity', 0).show();
Utils.animate(params.next, {
opacity:1
},{
duration: params.speed,
complete: complete
});
},

slide: function(params, complete) {
_slide.apply( this, Utils.array( arguments ) );
},

fadeslide: function(params, complete) {
_slide.apply( this, Utils.array( arguments ).concat( [true] ) );
},

doorslide: function(params, complete) {
_slide.apply( this, Utils.array( arguments ).concat( [false, true] ) );
}
};
}());

/**
The main Galleria class

@class
@constructor

@example var gallery = new Galleria();

@author http://aino.se

@requires jQuery

*/

Galleria = function() {

var self = this;

// the theme used
this._theme = undef;

// internal options
this._options = {};

// flag for controlling play/pause
this._playing = false;

// internal interval for slideshow
this._playtime = 5000;

// internal variable for the currently active image
this._active = null;

// the internal queue, arrayified
this._queue = { length: 0 };

// the internal data array
this._data = [];

// the internal dom collection
this._dom = {};

// the internal thumbnails array
this._thumbnails = [];

// the internal layers array
this._layers = [];

// internal init flag
this._initialized = false;

// internal firstrun flag
this._firstrun = false;

// global stagewidth/height
this._stageWidth = 0;
this._stageHeight = 0;

// target holder
this._target = undef;

// instance id
this._id = Math.random();

// add some elements
var divs = 'container stage images image-nav image-nav-left image-nav-right ' +
'info info-text info-title info-description ' +
'thumbnails thumbnails-list thumbnails-container thumb-nav-left thumb-nav-right ' +
'loader counter tooltip',
spans = 'current total';

$.each( divs.split(' '), function( i, elemId ) {
self._dom[ elemId ] = Utils.create( 'galleria-' + elemId );
});

$.each( spans.split(' '), function( i, elemId ) {
self._dom[ elemId ] = Utils.create( 'galleria-' + elemId, 'span' );
});

// the internal keyboard object
// keeps reference of the keybinds and provides helper methods for binding keys
var keyboard = this._keyboard = {

keys : {
'UP': 38,
'DOWN': 40,
'LEFT': 37,
'RIGHT': 39,
'RETURN': 13,
'ESCAPE': 27,
'BACKSPACE': 8,
'SPACE': 32
},

map : {},

bound: false,

press: function(e) {
var key = e.keyCode || e.which;
if ( key in keyboard.map && typeof keyboard.map[key] === 'function' ) {
keyboard.map[key].call(self, e);
}
},

attach: function(map) {

var key, up;

for( key in map ) {
if ( map.hasOwnProperty( key ) ) {
up = key.toUpperCase();
if ( up in keyboard.keys ) {
keyboard.map[ keyboard.keys[up] ] = map[key];
} else {
keyboard.map[ up ] = map[key];
}
}
}
if ( !keyboard.bound ) {
keyboard.bound = true;
$doc.bind('keydown', keyboard.press);
}
},

detach: function() {
keyboard.bound = false;
keyboard.map = {};
$doc.unbind('keydown', keyboard.press);
}
};

// internal controls for keeping track of active / inactive images
var controls = this._controls = {

0: undef,

1: undef,

active : 0,

swap : function() {
controls.active = controls.active ? 0 : 1;
},

getActive : function() {
return controls[ controls.active ];
},

getNext : function() {
return controls[ 1 - controls.active ];
}
};

// internal carousel object
var carousel = this._carousel = {

// shortcuts
next: self.$('thumb-nav-right'),
prev: self.$('thumb-nav-left'),

// cache the width
width: 0,

// track the current position
current: 0,

// cache max value
max: 0,

// save all hooks for each width in an array
hooks: [],

// update the carousel
// you can run this method anytime, f.ex on window.resize
update: function() {
var w = 0,
h = 0,
hooks = [0];

$.each( self._thumbnails, function( i, thumb ) {
if ( thumb.ready ) {
w += thumb.outerWidth || $( thumb.container ).outerWidth( true );
hooks[ i+1 ] = w;
h = Math.max( h, thumb.outerHeight || $( thumb.container).outerHeight( true ) );
}
});

self.$( 'thumbnails' ).css({
width: w,
height: h
});

carousel.max = w;
carousel.hooks = hooks;
carousel.width = self.$( 'thumbnails-list' ).width();
carousel.setClasses();

self.$( 'thumbnails-container' ).toggleClass( 'galleria-carousel', w > carousel.width );

// one extra calculation
carousel.width = self.$( 'thumbnails-list' ).width();

// todo: fix so the carousel moves to the left
},

bindControls: function() {

var i;

carousel.next.bind( 'click', function(e) {
e.preventDefault();

if ( self._options.carouselSteps === 'auto' ) {

for ( i = carousel.current; i < carousel.hooks.length; i++ ) {
if ( carousel.hooks[i] - carousel.hooks[ carousel.current ] > carousel.width ) {
carousel.set(i - 2);
break;
}
}

} else {
carousel.set( carousel.current + self._options.carouselSteps);
}
});

carousel.prev.bind( 'click', function(e) {
e.preventDefault();

if ( self._options.carouselSteps === 'auto' ) {

for ( i = carousel.current; i >= 0; i-- ) {
if ( carousel.hooks[ carousel.current ] - carousel.hooks[i] > carousel.width ) {
carousel.set( i + 2 );
break;
} else if ( i === 0 ) {
carousel.set( 0 );
break;
}
}
} else {
carousel.set( carousel.current - self._options.carouselSteps );
}
});
},

// calculate and set positions
set: function( i ) {
i = Math.max( i, 0 );
while ( carousel.hooks[i - 1] + carousel.width >= carousel.max && i >= 0 ) {
i--;
}
carousel.current = i;
carousel.animate();
},

// get the last position
getLast: function(i) {
return ( i || carousel.current ) - 1;
},

// follow the active image
follow: function(i) {

//don't follow if position fits
if ( i === 0 || i === carousel.hooks.length - 2 ) {
carousel.set( i );
return;
}

// calculate last position
var last = carousel.current;
while( carousel.hooks[last] - carousel.hooks[ carousel.current ] <
carousel.width && last <= carousel.hooks.length ) {
last ++;
}

// set position
if ( i - 1 < carousel.current ) {
carousel.set( i - 1 );
} else if ( i + 2 > last) {
carousel.set( i - last + carousel.current + 2 );
}
},

// helper for setting disabled classes
setClasses: function() {
carousel.prev.toggleClass( 'disabled', !carousel.current );
carousel.next.toggleClass( 'disabled', carousel.hooks[ carousel.current ] + carousel.width >= carousel.max );
},

// the animation method
animate: function(to) {
carousel.setClasses();
var num = carousel.hooks[ carousel.current ] * -1;

if ( isNaN( num ) ) {
return;
}

Utils.animate(self.get( 'thumbnails' ), {
left: num
},{
duration: self._options.carouselSpeed,
easing: self._options.easing,
queue: false
});
}
};

// tooltip control
// added in 1.2
var tooltip = this._tooltip = {

initialized : false,

open: false,

init: function() {

tooltip.initialized = true;

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' +
'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);}';

Utils.insertStyleTag(css);

self.$( 'tooltip' ).css('opacity', 0.8);
Utils.hide( self.get('tooltip') );

},

// move handler
move: function( e ) {
var mouseX = self.getMousePosition(e).x,
mouseY = self.getMousePosition(e).y,
$elem = self.$( 'tooltip' ),
x = mouseX,
y = mouseY,
height = $elem.outerHeight( true ) + 1,
width = $elem.outerWidth( true ),
limitY = height + 15;

var maxX = self.$( 'container').width() - width - 2,
maxY = self.$( 'container').height() - height - 2;

if ( !isNaN(x) && !isNaN(y) ) {

x += 10;
y -= 30;

x = Math.max( 0, Math.min( maxX, x ) );
y = Math.max( 0, Math.min( maxY, y ) );

if( mouseY < limitY ) {
y = limitY;
}

$elem.css({ left: x, top: y });
}
},

// bind elements to the tooltip
// you can bind multiple elementIDs using { elemID : function } or { elemID : string }
// you can also bind single DOM elements using bind(elem, string)
bind: function( elem, value ) {

// todo: revise if alternative tooltip is needed for mobile devices
if (Galleria.TOUCH) {
return;
}

if (! tooltip.initialized ) {
tooltip.init();
}

var hover = function( elem, value) {

tooltip.define( elem, value );

$( elem ).hover(function() {

Utils.clearTimer('switch_tooltip');
self.$('container').unbind( 'mousemove', tooltip.move ).bind( 'mousemove', tooltip.move ).trigger( 'mousemove' );
tooltip.show( elem );

Galleria.utils.addTimer( 'tooltip', function() {
self.$( 'tooltip' ).stop().show().animate({
opacity:1
});
tooltip.open = true;

}, tooltip.open ? 0 : 500);

}, function() {

self.$( 'container' ).unbind( 'mousemove', tooltip.move );
Utils.clearTimer( 'tooltip' );

self.$( 'tooltip' ).stop().animate({
opacity: 0
}, 200, function() {

self.$( 'tooltip' ).hide();

Utils.addTimer('switch_tooltip', function() {
tooltip.open = false;
}, 1000);
});
});
};

if ( typeof value === 'string' ) {
hover( ( elem in self._dom ? self.get( elem ) : elem ), value );
} else {
// asume elemID here
$.each( elem, function( elemID, val ) {
hover( self.get(elemID), val );
});
}
},

show: function( elem ) {

elem = $( elem in self._dom ? self.get(elem) : elem );

var text = elem.data( 'tt' ),
mouseup = function( e ) {

// attach a tiny settimeout to make sure the new tooltip is filled
window.setTimeout( (function( ev ) {
return function() {
tooltip.move( ev );
};
}( e )), 10);

elem.unbind( 'mouseup', mouseup );

};

text = typeof text === 'function' ? text() : text;

if ( ! text ) {
return;
}

self.$( 'tooltip' ).html( text.replace(/\s/, ' ') );

// trigger mousemove on mouseup in case of click
elem.bind( 'mouseup', mouseup );
},

define: function( elem, value ) {

// we store functions, not strings
if (typeof value !== 'function') {
var s = value;
value = function() {
return s;
};
}

elem = $( elem in self._dom ? self.get(elem) : elem ).data('tt', value);

tooltip.show( elem );

}
};

// internal fullscreen control
var fullscreen = this._fullscreen = {

scrolled: 0,

crop: undef,

transition: undef,

active: false,

keymap: self._keyboard.map,

enter: function(callback) {

fullscreen.active = true;

// hide the image until rescale is complete
Utils.hide( self.getActiveImage() );

self.$( 'container' ).addClass( 'fullscreen' );

fullscreen.scrolled = $win.scrollTop();

// begin styleforce
Utils.forceStyles(self.get('container'), {
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
zIndex: 10000
});

var htmlbody = {
height: '100%',
overflow: 'hidden',
margin:0,
padding:0
},

data = self.getData(),

options = self._options;

Utils.forceStyles( DOM().html, htmlbody );
Utils.forceStyles( DOM().body, htmlbody );

// temporarily attach some keys
// save the old ones first in a cloned object
fullscreen.keymap = $.extend({}, self._keyboard.map);

self.attachKeyboard({
escape: self.exitFullscreen,
right: self.next,
left: self.prev
});

// temporarily save the crop & transition
fullscreen.crop = options.imageCrop;
fullscreen.transition = options.transition;

// set fullscreen options
$.each({
'fullscreenCrop': 'imageCrop',
'fullscreenTransition': 'imageTransition'
}, function( key, val ) {
if ( options[ key ] != undef ) {
options[ val ] = options[ key ];
}
});

// swap to big image if it's different from the display image
if ( data && data.big && data.image !== data.big ) {
var big = new Galleria.Picture(),
cached = big.isCached( data.big ),
index = self.getIndex(),
thumb = self._thumbnails[ index ];

self.trigger( {
type: Galleria.LOADSTART,
cached: cached,
rewind: false,
index: index,
imageTarget: self.getActiveImage(),
thumbTarget: thumb
});

big.load( data.big, function( big ) {
self._scaleImage( big, {
complete: function( big ) {
self.trigger({
type: Galleria.LOADFINISH,
cached: cached,
index: index,
rewind: false,
imageTarget: big.image,
thumbTarget: thumb
});
var image = self._controls.getActive().image;
if ( image ) {
$( image ).width( big.image.width ).height( big.image.height )
.attr( 'style', $( big.image ).attr('style') )
.attr( 'src', big.image.src );
}
}
});
});
}

// init the first rescale and attach callbacks
self.rescale(function() {

Utils.addTimer('fullscreen_enter', function() {
// show the image after 50 ms
Utils.show( self.getActiveImage() );

if (typeof callback === 'function') {
callback.call( self );
}

}, 100);

self.trigger( Galleria.FULLSCREEN_ENTER );
});

// bind the scaling to the resize event
$win.resize( function() {
fullscreen.scale();
} );
},

scale : function() {
self.rescale();
},

exit: function(callback) {

fullscreen.active = false;

Utils.hide( self.getActiveImage() );

self.$('container').removeClass( 'fullscreen' );

// revert all styles
Utils.revertStyles( self.get('container'), DOM().html, DOM().body );

// scroll back
window.scrollTo(0, fullscreen.scrolled);

// detach all keyboard events and apply the old keymap
self.detachKeyboard();
self.attachKeyboard( fullscreen.keymap );

// bring back cached options
self._options.imageCrop = fullscreen.crop;
self._options.transition = fullscreen.transition;

// return to original image
var big = self.getData().big,
image = self._controls.getActive().image;

if ( big && big == image.src ) {

window.setTimeout(function(src) {
return function() {
image.src = src;
};
}( self.getData().image ), 1 );

}

self.rescale(function() {
Utils.addTimer('fullscreen_exit', function() {

// show the image after 50 ms
Utils.show( self.getActiveImage() );

if ( typeof callback === 'function' ) {
callback.call( self );
}

}, 50);

self.trigger( Galleria.FULLSCREEN_EXIT );
});

$win.unbind('resize', fullscreen.scale);
}
};

// the internal idle object for controlling idle states
var idle = this._idle = {

trunk: [],

bound: false,

add: function(elem, to) {
if (!elem) {
return;
}
if (!idle.bound) {
idle.addEvent();
}
elem = $(elem);

var from = {},
style;

for ( style in to ) {
if ( to.hasOwnProperty( style ) ) {
from[ style ] = elem.css( style );
}
}
elem.data('idle', {
from: from,
to: to,
complete: true,
busy: false
});
idle.addTimer();
idle.trunk.push(elem);
},

remove: function(elem) {

elem = jQuery(elem);

$.each(idle.trunk, function(i, el) {
if ( el.length && !el.not(elem).length ) {
self._idle.show(elem);
self._idle.trunk.splice(i, 1);
}
});

if (!idle.trunk.length) {
idle.removeEvent();
Utils.clearTimer('idle');
}
},

addEvent : function() {
idle.bound = true;
self.$('container').bind('mousemove click', idle.showAll );
},

removeEvent : function() {
idle.bound = false;
self.$('container').unbind('mousemove click', idle.showAll );
},

addTimer : function() {
Utils.addTimer('idle', function() {
self._idle.hide();
}, self._options.idleTime );
},

hide : function() {

if ( !self._options.idleMode ) {
return;
}

self.trigger( Galleria.IDLE_ENTER );

$.each( idle.trunk, function(i, elem) {

var data = elem.data('idle');

if (! data) {
return;
}

elem.data('idle').complete = false;

Utils.animate( elem, data.to, {
duration: self._options.idleSpeed
});
});
},

showAll : function() {

Utils.clearTimer('idle');

$.each(self._idle.trunk, function( i, elem ) {
self._idle.show( elem );
});
},

show: function(elem) {

var data = elem.data('idle');

if (!data.busy && !data.complete) {

data.busy = true;

self.trigger( Galleria.IDLE_EXIT );

Utils.clearTimer( 'idle' );

Utils.animate( elem, data.from, {
duration: self._options.idleSpeed/2,
complete: function() {
$(this).data('idle').busy = false;
$(this).data('idle').complete = true;
}
});

}
idle.addTimer();
}
};

// internal lightbox object
// creates a predesigned lightbox for simple popups of images in galleria
var lightbox = this._lightbox = {

width : 0,

height : 0,

initialized : false,

active : null,

image : null,

elems : {},

keymap: false,

init : function() {

// trigger the event
self.trigger( Galleria.LIGHTBOX_OPEN );

if ( lightbox.initialized ) {
return;
}
lightbox.initialized = true;

// create some elements to work with
var elems = 'overlay box content shadow title info close prevholder prev nextholder next counter image',
el = {},
op = self._options,
css = '',
abs = 'position:absolute;',
prefix = 'lightbox-',
cssMap = {
overlay: 'position:fixed;display:none;opacity:'+op.overlayOpacity+';filter:alpha(opacity='+(op.overlayOpacity*100)+
');top:0;left:0;width:100%;height:100%;background:'+op.overlayBackground+';z-index:99990',
box: 'position:fixed;display:none;width:400px;height:400px;top:50%;left:50%;margin-top:-200px;margin-left:-200px;z-index:99991',
shadow: abs+'background:#000;width:100%;height:100%;',
content: abs+'background-color:#fff;top:10px;left:10px;right:10px;bottom:10px;overflow:hidden',
info: abs+'bottom:10px;left:10px;right:10px;color:#444;font:11px/13px arial,sans-serif;height:13px',
close: abs+'top:10px;right:10px;height:20px;width:20px;background:#fff;text-align:center;cursor:pointer;color:#444;font:16px/22px arial,sans-serif;z-index:99999',
image: abs+'top:10px;left:10px;right:10px;bottom:30px;overflow:hidden;display:block;',
prevholder: abs+'width:50%;top:0;bottom:40px;cursor:pointer;',
nextholder: abs+'width:50%;top:0;bottom:40px;right:-1px;cursor:pointer;',
prev: abs+'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;left:20px;display:none;text-align:center;color:#000;font:bold 16px/36px arial,sans-serif',
next: abs+'top:50%;margin-top:-20px;height:40px;width:30px;background:#fff;right:20px;left:auto;display:none;font:bold 16px/36px arial,sans-serif;text-align:center;color:#000',
title: 'float:left',
counter: 'float:right;margin-left:8px;'
},
hover = function(elem) {
return elem.hover(
function() { $(this).css( 'color', '#bbb' ); },
function() { $(this).css( 'color', '#444' ); }
);
},
appends = {};

// IE8 fix for IE's transparent background event "feature"
if ( IE === 8 ) {
cssMap.nextholder += 'background:#000;filter:alpha(opacity=0);';
cssMap.prevholder += 'background:#000;filter:alpha(opacity=0);';
}

// create and insert CSS
$.each(cssMap, function( key, value ) {
css += '.galleria-'+prefix+key+'{'+value+'}';
});

Utils.insertStyleTag( css );

// create the elements
$.each(elems.split(' '), function( i, elemId ) {
self.addElement( 'lightbox-' + elemId );
el[ elemId ] = lightbox.elems[ elemId ] = self.get( 'lightbox-' + elemId );
});

// initiate the image
lightbox.image = new Galleria.Picture();

// append the elements
$.each({
box: 'shadow content close prevholder nextholder',
info: 'title counter',
content: 'info image',
prevholder: 'prev',
nextholder: 'next'
}, function( key, val ) {
var arr = [];
$.each( val.split(' '), function( i, prop ) {
arr.push( prefix + prop );
});
appends[ prefix+key ] = arr;
});

self.append( appends );

$( el.image ).append( lightbox.image.container );

$( DOM().body ).append( el.overlay, el.box );

Utils.optimizeTouch( el.box );

// add the prev/next nav and bind some controls

hover( $( el.close ).bind( 'click', lightbox.hide ).html('×') );

$.each( ['Prev','Next'], function(i, dir) {

var $d = $( el[ dir.toLowerCase() ] ).html( /v/.test( dir ) ? '‹ ' : ' ›' ),
$e = $( el[ dir.toLowerCase()+'holder'] );

$e.bind( 'click', function() {
lightbox[ 'show' + dir ]();
});

// IE7 and touch devices will simply show the nav
if ( IE < 8 || Galleria.TOUCH ) {
$d.show();
return;
}

$e.hover( function() {
$d.show();
}, function(e) {
$d.stop().fadeOut( 200 );
});

});
$( el.overlay ).bind( 'click', lightbox.hide );

// the lightbox animation is slow on ipad
if ( Galleria.IPAD ) {
self._options.lightboxTransitionSpeed = 0;
}

},

rescale: function(event) {

// calculate
var width = Math.min( $win.width()-40, lightbox.width ),
height = Math.min( $win.height()-60, lightbox.height ),
ratio = Math.min( width / lightbox.width, height / lightbox.height ),
destWidth = Math.round( lightbox.width * ratio ) + 40,
destHeight = Math.round( lightbox.height * ratio ) + 60,
to = {
width: destWidth,
height: destHeight,
'margin-top': Math.ceil( destHeight / 2 ) *- 1,
'margin-left': Math.ceil( destWidth / 2 ) *- 1
};

// if rescale event, don't animate
if ( event ) {
$( lightbox.elems.box ).css( to );
} else {
$( lightbox.elems.box ).animate( to, {
duration: self._options.lightboxTransitionSpeed,
easing: self._options.easing,
complete: function() {
var image = lightbox.image,
speed = self._options.lightboxFadeSpeed;

self.trigger({
type: Galleria.LIGHTBOX_IMAGE,
imageTarget: image.image
});

$( image.container ).show();

Utils.show( image.image, speed );
Utils.show( lightbox.elems.info, speed );
}
});
}
},

hide: function() {

// remove the image
lightbox.image.image = null;

$win.unbind('resize', lightbox.rescale);

$( lightbox.elems.box ).hide();

Utils.hide( lightbox.elems.info );

self.detachKeyboard();
self.attachKeyboard( lightbox.keymap );

lightbox.keymap = false;

Utils.hide( lightbox.elems.overlay, 200, function() {
$( this ).hide().css( 'opacity', self._options.overlayOpacity );
self.trigger( Galleria.LIGHTBOX_CLOSE );
});
},

showNext: function() {
lightbox.show( self.getNext( lightbox.active ) );
},

showPrev: function() {
lightbox.show( self.getPrev( lightbox.active ) );
},

show: function(index) {

lightbox.active = index = typeof index === 'number' ? index : self.getIndex();

if ( !lightbox.initialized ) {
lightbox.init();
}

// temporarily attach some keys
// save the old ones first in a cloned object
if ( !lightbox.keymap ) {

lightbox.keymap = $.extend({}, self._keyboard.map);

self.attachKeyboard({
escape: lightbox.hide,
right: lightbox.showNext,
left: lightbox.showPrev
});
}

$win.unbind('resize', lightbox.rescale );

var data = self.getData(index),
total = self.getDataLength(),
n = self.getNext( index ),
ndata, p, i;

Utils.hide( lightbox.elems.info );

try {
for ( i = self._options.preload; i > 0; i-- ) {
p = new Galleria.Picture();
ndata = self.getData( n );