/**
 * The Shadowbox class.
 *
 * This file is part of Shadowbox.
 *
 * Shadowbox is an online media viewer application that supports all of the
 * web's most popular media publishing formats. Shadowbox is written entirely
 * in JavaScript and CSS and is highly customizable. Using Shadowbox, website
 * authors can showcase a wide assortment of media in all major browsers without
 * navigating users away from the linking page.
 *
 * Shadowbox is released under version 3.0 of the Creative Commons Attribution-
 * Noncommercial-Share Alike license. This means that it is absolutely free
 * for personal, noncommercial use provided that you 1) make attribution to the
 * author and 2) release any derivative work under the same or a similar
 * license.
 *
 * If you wish to use Shadowbox for commercial purposes, licensing information
 * can be found at http://mjijackson.com/shadowbox/.
 *
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007-2008 Michael J. I. Jackson
 * @license     http://creativecommons.org/licenses/by-nc-sa/3.0/
 * @version     SVN: $Id: shadowbox.js 108 2008-07-11 04:19:01Z mjijackson $
 */

if(typeof Shadowbox == 'undefined'){
    throw 'Unable to load Shadowbox, no base library adapter found';
}

/**
 * The Shadowbox class. Used to display different media on a web page using a
 * Lightbox-like effect.
 *
 * Useful resources:
 *
 * - http://www.alistapart.com/articles/byebyeembed
 * - http://www.w3.org/TR/html401/struct/objects.html
 * - http://www.dyn-web.com/dhtml/iframes/
 * - http://www.apple.com/quicktime/player/specs.html
 * - http://www.apple.com/quicktime/tutorials/embed2.html
 * - http://www.howtocreate.co.uk/wrongWithIE/?chapter=navigator.plugins
 * - http://msdn.microsoft.com/en-us/library/ms532969.aspx
 * - http://support.microsoft.com/kb/316992
 *
 * @class       Shadowbox
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @singleton
 */
(function(){

    /**
     * The current version of Shadowbox.
     *
     * @var  String
     * @private
     */
    var version = '2.0';

    /**
     * Contains the default options for Shadowbox.
     *
     * @var  Object
     * @private
     */
    var options = {

 /**
  * Enable all animations besides fades.
  *
  * @var     Boolean
  */
 animate:     true,

 /**
  * Enable fade animations.
  *
  * @var     Boolean
  */
 animateFade: true,

 /**
  * Specifies the sequence of the height and width animations. May be
  * 'wh' (width then height), 'hw' (height then width), or 'sync' (both
  * at the same time). Of course this will only work if animate is true.
  *
  * @var     String
  */
 animSequence:       'wh',

 /**
  * The path to flvplayer.swf.
  *
  * @var     String
  */
 flvPlayer:   'flvplayer.swf',

 /**
  * Listen to the overlay for clicks. If the user clicks the overlay,
  * it will trigger Shadowbox.close().
  *
  * @var     Boolean
  */
 modal:       false,

 /**
  * The color to use for the modal overlay (in hex).
  *
  * @var     String
  */
 overlayColor:       '#000',

 /**
  * The opacity to use for the modal overlay.
  *
  * @var     Number
  */
 overlayOpacity:     0.8,

 /**
  * The default background color to use for Flash movies (in hex).
  *
  * @var     String
  */
 flashBgColor:       '#000000',

 /**
  * Automatically play movies.
  *
  * @var     Boolean
  */
 autoplayMovies:     true,

 /**
  * Enable movie controllers on movie players.
  *
  * @var     Boolean
  */
 showMovieControls:  true,

 /**
  * A delay (in seconds) to use for slideshows. If set to anything other
  * than 0, this value determines an interval at which Shadowbox will
  * automatically proceed to the next piece in the gallery.
  *
  * @var     Number
  */
 slideshowDelay:     0,

 /**
  * The duration of the resizing animations (in seconds).
  *
  * @var     Number
  */
 resizeDuration:     0.55,

 /**
  * The duration of the fading animations (in seconds).
  *
  * @var     Number
  */
 fadeDuration:       0.35,

 /**
  * Show the navigation controls.
  *
  * @var     Boolean
  */
 displayNav:  true,

 /**
  * Enable continuous galleries. When this is true, users will be able
  * to skip to the first gallery image from the last using next and vice
  * versa.
  *
  * @var     Boolean
  */
 continuous:  false,

 /**
  * Display the gallery counter.
  *
  * @var     Boolean
  */
 displayCounter:     true,

 /**
  * This option may be either 'default' or 'skip'. The default counter is
  * a simple '1 of 5' message. The skip counter displays a link for each
  * piece in the gallery that enables a user to skip directly to any
  * piece.
  *
  * @var     String
  */
 counterType: 'default',

 /**
  * Limits the number of counter links that will be displayed in a "skip"
  * style counter. If the actual number of gallery elements is greater
  * than this value, the counter will be restrained to the elements
  * immediately preceeding and following the current element.
  *
  * @var     Number
  */
 counterLimit:       10,

 /**
  * The amount of padding to maintain around the viewport edge (in
  * pixels). This only applies when the image is very large and takes up
  * the entire viewport.
  *
  * @var     Number
  */
 viewportPadding:    20,

 /**
  * How to handle content that is too large to display in its entirety
  * (and is resizable). A value of 'resize' will resize the content while
  * preserving aspect ratio and display it at the smaller resolution. If
  * the content is an image, a value of 'drag' will display the image at
  * its original resolution but it will be draggable within Shadowbox. A
  * value of 'none' will display the content at its original resolution
  * but it may be cropped.
  *
  * @var     String
  */
 handleOversize:     'resize',

 /**
  * An exception handling function that will be called whenever
  * Shadowbox should throw an exception. Will be passed the error
  * message as its first argument.
  *
  * @var     Function
  */
 handleException:    null,

 /**
  * The mode to use when handling unsupported media. May be either
  * 'remove' or 'link'. If it is 'remove', the unsupported gallery item
  * will merely be removed from the gallery. If it is the only item in
  * the gallery, the link will simply be followed. If it is 'link', a
  * link will be provided to the appropriate plugin page in place of the
  * gallery element.
  *
  * @var     String
  */
 handleUnsupported:  'link',

 /**
  * The initial height of Shadowbox (in pixels).
  *
  * @var     Number
  */
 initialHeight:      160,

 /**
  * The initial width of Shadowbox (in pixels).
  *
  * @var     Number
  */
 initialWidth:       320,

 /**
  * Enable keyboard control.
  *
  * @var     Boolean
  */
 enableKeys:  true,

 /**
  * A hook function to be fired when Shadowbox opens. The single argument
  * will be the current gallery element.
  *
  * @var     Function
  */
 onOpen:      null,

 /**
  * A hook function to be fired when Shadowbox finishes loading its
  * content. The single argument will be the current gallery element on
  * display.
  *
  * @var     Function
  */
 onFinish:    null,

 /**
  * A hook function to be fired when Shadowbox changes from one gallery
  * element to the next. The single argument will be the current gallery
  * element that is about to be displayed.
  *
  * @var     Function
  */
 onChange:    null,

 /**
  * A hook function that will be fired when Shadowbox closes. The single
  * argument will be the gallery element most recently displayed.
  *
  * @var     Function
  */
 onClose:     null,

 /**
  * Skips calling Shadowbox.setup() in init(). This means that it must
  * be called later manually.
  *
  * @var     Boolean
  */
 skipSetup:   false,

 /**
  * An object containing names of plugins and links to their respective
  * download pages.
  *
  * @var     Object
  */
 errors:  {

     fla: {
  name:   'Flash',
  url:    'http://www.adobe.com/products/flashplayer/'
     },

     qt:  {
  name:   'QuickTime',
  url:    'http://www.apple.com/quicktime/download/'
     },

     wmp: {
  name:   'Windows Media Player',
  url:    'http://www.microsoft.com/windows/windowsmedia/'
     },

     f4m: {
  name:   'Flip4Mac',
  url:    'http://www.flip4mac.com/wmv_download.htm'
     }

 },

 /**
  * A map of players to the file extensions they support. Each member of
  * this object is the name of a player (with one exception), whose value
  * is an array of file extensions that player will "play". The one
  * exception to this rule is the "qtwmp" member, which contains extensions
  * that may be played using either QuickTime or Windows Media Player.
  *
  * - img: Image file extensions
  * - swf: Flash SWF file extensions
  * - flv: Flash video file extensions (will be played by JW FLV player)
  * - qt: Movie file extensions supported by QuickTime
  * - wmp: Movie file extensions supported by Windows Media Player
  * - qtwmp: Movie file extensions supported by both QuickTime and Windows Media Player
  * - iframe: File extensions that will be display in an iframe
  *
  * IMPORTANT: If this object is to be modified, it must be copied in its
  * entirety and tweaked because it is not merged recursively with the
  * default. Also, any modifications must be passed into Shadowbox.init
  * for speed reasons.
  *
  * @var     Object      ext
  */
 ext:     {
     img: ['png', 'jpg', 'jpeg', 'gif', 'bmp'],
     swf: ['swf'],
     flv: ['flv'],
     qt:  ['dv', 'mov', 'moov', 'movie', 'mp4'],
     wmp: ['asf', 'wm', 'wmv'],
     qtwmp:      ['avi', 'mpg', 'mpeg'],
     iframe:     ['asp', 'aspx', 'cgi', 'cfm', 'htm', 'html', 'pl', 'php',
   'php3', 'php4', 'php5', 'phtml', 'rb', 'rhtml', 'shtml',
   'txt', 'vbs']
 }

    };

    // shorthand
    var SB = Shadowbox;
    var SL = SB.lib;

    /**
     * Stores the default set of options in case a custom set of options is used
     * on a link-by-link basis so we can restore them later.
     *
     * @var  Object
     * @private
     */
    var default_options;

    /**
     * An object containing some regular expressions we'll need later. Compiled
     * up front for speed.
     *
     * @var  Object
     * @private
     */
    var RE = {
 domain:  /:\/\/(.*?)[:\/]/, // domain prefix
 inline:  /#(.+)$/, // inline element id
 rel:     /^(light|shadow)box/i, // rel attribute format
 gallery: /^(light|shadow)box\[(.*?)\]/i, // rel attribute format for gallery link
 unsupported:    /^unsupported-(\w+)/, // unsupported media type
 param:   /\s*([a-z_]*?)\s*=\s*(.+)\s*/, // rel string parameter
 empty:   /^(?:br|frame|hr|img|input|link|meta|range|spacer|wbr|area|param|col)$/i // elements that don't have children
    };

    /**
     * A cache of options for links that have been set up for use with
     * Shadowbox.
     *
     * @var  Array
     * @private
     */
    var cache = [];

    /**
     * An array containing the gallery objects currently being viewed. In the
     * case of non-gallery items, this will only hold one object.
     *
     * @var  Array
     * @private
     */
    var gallery;

    /**
     * The array index of the current gallery that is currently being viewed.
     *
     * @var  Number
     * @private
     */
    var current;

    /**
     * The current content object.
     *
     * @var  Object
     * @private
     */
    var content;

    /**
     * The id to use for content objects.
     *
     * @var  String
     * @private
     */
    var content_id = 'shadowbox_content';

    /**
     * Holds the current dimensions of Shadowbox as calculated by
     * setDimensions(). Contains the following properties:
     *
     * - height: The total height of #shadowbox
     * - width: The total width of #shadowbox
     * - inner_h: The height of #shadowbox_body
     * - inner_w: The width of #shadowbox_body
     * - top: The top to use for #shadowbox
     * - resize_h: The height to use for resizable content
     * - resize_w: The width to use for resizable content
     * - drag: True if dragging should be enabled (oversized image)
     *
     * @var  Object
     * @private
     */
    var dims;

    /**
     * Keeps track of whether or not Shadowbox has been initialized. We never
     * want to initialize twice.
     *
     * @var  Boolean
     * @private
     */
    var initialized = false;

    /**
     * Keeps track of whether or not Shadowbox is activated.
     *
     * @var  Boolean
     * @private
     */
    var activated = false;

    /**
     * The timeout id for the slideshow transition function.
     *
     * @var  Number
     * @private
     */
    var slide_timer;

    /**
     * Keeps track of the time at which the current slideshow frame was
     * displayed.
     *
     * @var  Number
     * @private
     */
    var slide_start;

    /**
     * The delay on which the next slide will display.
     *
     * @var  Number
     * @private
     */
    var slide_delay = 0;

    /**
     * These parameters for simple browser detection. Adapted from Ext.js.
     *
     * @var  Object
     * @private
     */
    var ua = navigator.userAgent.toLowerCase();
    var client = {
 isStrict:   document.compatMode == 'CSS1Compat',
 isOpera:    ua.indexOf('opera') > -1,
 isIE:       ua.indexOf('msie') > -1,
 isIE7:      ua.indexOf('msie 7') > -1,
 isSafari:   /webkit|khtml/.test(ua),
 isWindows:  ua.indexOf('windows') != -1 || ua.indexOf('win32') != -1,
 isMac:      ua.indexOf('macintosh') != -1 || ua.indexOf('mac os x') != -1,
 isLinux:    ua.indexOf('linux') != -1
    };
    client.isBorderBox = client.isIE && !client.isStrict;
    client.isSafari3 = client.isSafari && !!(document.evaluate);
    client.isGecko = ua.indexOf('gecko') != -1 && !client.isSafari;

    /**
     * You're not sill using IE6 are you?
     *
     * @var  Boolean
     * @private
     */
    var ltIE7 = client.isIE && !client.isIE7;

    /**
     * Contains plugin support information. Each property of this object is a
     * boolean indicating whether that plugin is supported.
     *
     * - fla: Flash player
     * - qt: QuickTime player
     * - wmp: Windows Media player
     * - f4m: Flip4Mac plugin
     *
     * @var  Object
     * @private
     */
    var plugins;

    // detect plugin support
    if(navigator.plugins && navigator.plugins.length){
 var detectPlugin = function(plugin_name){
     var detected = false;
     for (var i = 0, len = navigator.plugins.length; i < len; ++i){
  if(navigator.plugins[i].name.indexOf(plugin_name) > -1){
      detected = true;
      break;
  }
     }
     return detected;
 };
 var f4m = detectPlugin('Flip4Mac');
 plugins = {
     fla:    detectPlugin('Shockwave Flash'),
     qt:     detectPlugin('QuickTime'),
     wmp:    !f4m && detectPlugin('Windows Media'), // if it's Flip4Mac, it's not really WMP
     f4m:    f4m
 };
    }else{
 var detectPlugin = function(plugin_name){
     var detected = false;
     try{
  var axo = new ActiveXObject(plugin_name);
  if(axo) detected = true;
     }catch(e){}
     return detected;
 };
 plugins = {
     fla:    detectPlugin('ShockwaveFlash.ShockwaveFlash'),
     qt:     detectPlugin('QuickTime.QuickTime'),
     wmp:    detectPlugin('wmplayer.ocx'),
     f4m:    false
 };
    }

    /**
     * Applies all properties of e to o.
     *
     * @param   Object      o       The original object
     * @param   Object      e       The extension object
     * @return  Object       The original object with all properties
     *         of the extension object applied
     * @private
     */
    var apply = function(o, e){
 for(var p in e) o[p] = e[p];
 return o;
    };

    /**
     * Determines if the given object is an anchor/area element.
     *
     * @param   mixed       el      The object to check
     * @return  Boolean      True if the object is a link element
     * @private
     */
    var isLink = function(el){
 return el && typeof el.tagName == 'string' && (el.tagName.toUpperCase() == 'A' || el.tagName.toUpperCase() == 'AREA');
    };

    /**
     * Gets the height of the viewport in pixels. Note: This function includes
     * scrollbars in Safari 3.
     *
     * @return  Number   The height of the viewport
     * @public
     * @static
     */
    SL.getViewportHeight = function(){
 var h = window.innerHeight; // Safari
 var mode = document.compatMode;
 if((mode || client.isIE) && !client.isOpera){
     h = client.isStrict ? document.documentElement.clientHeight : document.body.clientHeight;
 }
 return h;
    };

    /**
     * Gets the width of the viewport in pixels. Note: This function includes
     * scrollbars in Safari 3.
     *
     * @return  Number   The width of the viewport
     * @public
     * @static
     */
    SL.getViewportWidth = function(){
 var w = window.innerWidth; // Safari
 var mode = document.compatMode;
 if(mode || client.isIE){
     w = client.isStrict ? document.documentElement.clientWidth : document.body.clientWidth;
 }
 return w;
    };

    /**
     * Creates an HTML string from an object representing HTML elements. Based
     * on Ext.DomHelper's createHtml.
     *
     * @param   Object      obj     The HTML definition object
     * @return  String       An HTML string
     * @public
     * @static
     */
    SL.createHTML = function(obj){
 var html = '<' + obj.tag;
 for(var attr in obj){
     if(attr == 'tag' || attr == 'html' || attr == 'children') continue;
     if(attr == 'cls'){
  html += ' class="' + obj['cls'] + '"';
     }else{
  html += ' ' + attr + '="' + obj[attr] + '"';
     }
 }
 if(RE.empty.test(obj.tag)){
     html += '/>';
 }else{
     html += '>';
     var cn = obj.children;
     if(cn){
  for(var i = 0, len = cn.length; i < len; ++i){
      html += this.createHTML(cn[i]);
  }
     }
     if(obj.html) html += obj.html;
     html += '</' + obj.tag + '>';
 }
 return html;
    };

    /**
     * Easing function used for animations. Based on a cubic polynomial.
     *
     * @param   Number      x       The state of the animation (% complete)
     * @return  Number       The adjusted easing value
     * @private
     * @static
     */
    var ease = function(x){
 return 1 + Math.pow(x - 1, 3);
    };

    /**
     * Animates any numeric (not color) style of the given element from its
     * current state to the given value. Defaults to using pixel-based
     * measurements.
     *
     * @param   HTMLElement     el      The DOM element to animate
     * @param   String   p       The property to animate (in camelCase)
     * @param   mixed    to      The value to animate to
     * @param   Number   d       The duration of the animation (in
     *      seconds)
     * @param   Function cb      A callback function to call when the
     *      animation completes
     * @return  void
     * @private
     * @static
     */
    var animate = function(el, p, to, d, cb){
 var from = parseFloat(SL.getStyle(el, p));
 if(isNaN(from)) from = 0;

 if(from == to){
     if(typeof cb == 'function') cb();
     return; // nothing to animate
 }

 var delta = to - from;
 var op = p == 'opacity';
 var unit = op ? '' : 'px'; // default unit is px
 var fn = function(ease){
     SL.setStyle(el, p, from + ease * delta + unit);
 };

 // cancel the animation here if set in the options
 if(!options.animate && !op || op && !options.animateFade){
     fn(1);
     if(typeof cb == 'function') cb();
     return;
 }

 d *= 1000; // convert to milliseconds
 var begin = new Date().getTime();
 var end = begin + d;

 var timer = setInterval(function(){
     var time = new Date().getTime();
     if(time >= end){ // end of animation
  clearInterval(timer);
  fn(1);
  if(typeof cb == 'function') cb();
     }else{
  fn(ease((time - begin) / d));
     }
 }, 10); // 10 ms interval is minimum on WebKit
    };

    /**
     * A utility function used by the fade functions to clear the opacity
     * style setting of the given element. Required in some cases for IE.
     *
     * @param   HTMLElement     el      The DOM element
     * @return  void
     * @private
     */
    var clearOpacity = function(el){
 var s = el.style;
 if(client.isIE){
     if(typeof s.filter == 'string' && (/alpha/i).test(s.filter)){
  // careful not to overwrite other filters!
  s.filter = s.filter.replace(/[\w\.]*alpha\(.*?\);?/i, '');
     }
 }else{
     s.opacity = '';
     s['-moz-opacity'] = '';
     s['-khtml-opacity'] = '';
 }
    };

    /**
     * Gets the computed height of the given element, including padding and
     * borders.
     *
     * @param   HTMLElement     el  The element
     * @return  Number       The computed height of the element
     * @private
     */
    var getComputedHeight = function(el){
 var h = Math.max(el.offsetHeight, el.clientHeight);
 if(!h){
     h = parseInt(SL.getStyle(el, 'height'), 10) || 0;
     if(!client.isBorderBox){
  h += parseInt(SL.getStyle(el, 'padding-top'), 10)
      + parseInt(SL.getStyle(el, 'padding-bottom'), 10)
      + parseInt(SL.getStyle(el, 'border-top-width'), 10)
      + parseInt(SL.getStyle(el, 'border-bottom-width'), 10);
     }
 }
 return h;
    };

    /**
     * Determines the player needed to display the file at the given URL. If
     * the file type is not supported, the return value will be 'unsupported'.
     * If the file type is not supported but the correct player can be
     * determined, the return value will be 'unsupported-*' where * will be the
     * player abbreviation (e.g. 'qt' = QuickTime).
     *
     * @param   String   url     The url of the file
     * @return  String    The name of the player to use
     * @private
     */
    var getPlayer = function(url){
 var m = url.match(RE.domain);
 var d = m && document.domain == m[1]; // same domain
 if(url.indexOf('#') > -1 && d) return 'inline';
 var q = url.indexOf('?');
 if(q > -1) url = url.substring(0, q); // strip query string for player detection purposes
 if(RE.img.test(url)) return 'img';
 if(RE.swf.test(url)) return plugins.fla ? 'swf' : 'unsupported-swf';
 if(RE.flv.test(url)) return plugins.fla ? 'flv' : 'unsupported-flv';
 if(RE.qt.test(url)) return plugins.qt ? 'qt' : 'unsupported-qt';
 if(RE.wmp.test(url)){
     if(plugins.wmp) return 'wmp';
     if(plugins.f4m) return 'qt';
     if(client.isMac) return plugins.qt ? 'unsupported-f4m' : 'unsupported-qtf4m';
     return 'unsupported-wmp';
 }else if(RE.qtwmp.test(url)){
     if(plugins.qt) return 'qt';
     if(plugins.wmp) return 'wmp';
     return client.isMac ? 'unsupported-qt' : 'unsupported-qtwmp';
 }else if(!d || RE.iframe.test(url)){
     return 'iframe';
 }
 return 'unsupported'; // same domain, not supported
    };

    /**
     * Handles all clicks on links that have been set up to work with Shadowbox
     * and cancels the default event behavior when appropriate.
     *
     * @param   {Event}  ev   The click event object
     * @return  void
     * @private
     */
    var handleClick = function(ev){
 // get anchor/area element
 var link;
 if(isLink(this)){
     link = this; // jQuery, Prototype, YUI
 }else{
     link = SL.getTarget(ev); // Ext, standalone
     while(!isLink(link) && link.parentNode){
  link = link.parentNode;
     }
 }

 //SL.preventDefault(ev); // good for debugging

 if(link){
     SB.open(link);
     if(gallery.length) SL.preventDefault(ev); // stop event
 }
    };

    /**
     * Toggles the display of the nav control with the given id on and off.
     *
     * @param   String      id      The id of the navigation control
     * @param   Boolean     on      True to toggle on, false to toggle off
     * @return  void
     * @private
     */
    var toggleNav = function(id, on){
 var el = SL.get('shadowbox_nav_' + id);
 if(el) el.style.display = on ? '' : 'none';
    };

    /**
     * Builds the content for the title and information bars.
     *
     * @param   Function    cb      A callback function to execute after the
     *         bars are built
     * @return  void
     * @private
     */
    var buildBars = function(cb){
 var obj = gallery[current];
 var title_i = SL.get('shadowbox_title_inner');

 // build the title
 title_i.innerHTML = obj.title || '';

 // build the nav
 var nav = SL.get('shadowbox_nav');
 if(nav){
     var c, n, pl, pa, p;

     // need to build the nav?
     if(options.displayNav){
  c = true;
  // next & previous links
  var len = gallery.length;
  if(len > 1){
      if(options.continuous){
   n = p = true; // show both
      }else{
   n = (len - 1) > current; // not last in gallery, show next
   p = current > 0; // not first in gallery, show previous
      }
  }
  // in a slideshow?
  if(options.slideshowDelay > 0 && hasNext()){
      pa = slide_timer != 'paused';
      pl = !pa;
  }
     }else{
  c = n = pl = pa = p = false;
     }

     toggleNav('close', c);
     toggleNav('next', n);
     toggleNav('play', pl);
     toggleNav('pause', pa);
     toggleNav('previous', p);
 }

 // build the counter
 var counter = SL.get('shadowbox_counter');
 if(counter){
     var co = '';

     // need to build the counter?
     if(options.displayCounter && gallery.length > 1){
  if(options.counterType == 'skip'){
      // limit the counter?
      var i = 0, len = gallery.length, end = len;
      var limit = parseInt(options.counterLimit);
      if(limit < len){ // support large galleries
   var h = Math.round(limit / 2);
   i = current - h;
   if(i < 0) i += len;
   end = current + (limit - h);
   if(end > len) end -= len;
      }
      while(i != end){
   if(i == len) i = 0;
   co += '<a onclick="Shadowbox.change(' + i + ');"';
   if(i == current) co += ' class="shadowbox_counter_current"';
   co += '>' + (++i) + '</a>';
      }
  }else{ // default
      co = (current + 1) + ' ' + SB.LANG.of + ' ' + len;
  }
     }

     counter.innerHTML = co;
 }

 cb();
    };

    /**
     * Hides the title and info bars.
     *
     * @param   Boolean     anim    True to animate the transition
     * @param   Function    cb      A callback function to execute after the
     *         animation completes
     * @return  void
     * @private
     */
    var hideBars = function(anim, cb){
 var obj = gallery[current];
 var title = SL.get('shadowbox_title');
 var info = SL.get('shadowbox_info');
 var title_i = SL.get('shadowbox_title_inner');
 var info_i = SL.get('shadowbox_info_inner');

 // build bars after they are hidden
 var fn = function(){
     buildBars(cb);
 };

 var title_h = getComputedHeight(title);
 var info_h = getComputedHeight(info) * -1;
 if(anim){
     // animate the transition
     animate(title_i, 'margin-top', title_h, 0.35);
     animate(info_i, 'margin-top', info_h, 0.35, fn);
 }else{
     SL.setStyle(title_i, 'margin-top', title_h + 'px');
     SL.setStyle(info_i, 'margin-top', info_h + 'px');
     fn();
 }
    };

    /**
     * Shows the title and info bars.
     *
     * @param   Function    cb      A callback function to execute after the
     *         animation completes
     * @return  void
     * @private
     */
    var showBars = function(cb){
 var title_i = SL.get('shadowbox_title_inner');
 var info_i = SL.get('shadowbox_info_inner');
 var t = title_i.innerHTML != ''; // is there a title to display?

 if(t) animate(title_i, 'margin-top', 0, 0.35);
 animate(info_i, 'margin-top', 0, 0.35, cb);
    };

    /**
     * Loads the Shadowbox with the current piece.
     *
     * @return  void
     * @private
     */
    var loadContent = function(){
 var obj = gallery[current];
 if(!obj) return; // invalid

 var changing = false;
 if(content){
     content.remove(); // remove old content first
     changing = true; // changing from some previous content
 }

 // determine player, inline is really just HTML
 var p = obj.player == 'inline' ? 'html' : obj.player;

 // make sure player is loaded
 if(typeof SB[p] != 'function'){
     SB.raise('Unknown player ' + obj.player);
 }
 content = new SB[p](content_id, obj); // instantiate new content object

 listenKeys(false); // disable the keyboard temporarily
 toggleLoading(true);

 hideBars(changing, function(){ // if changing, animate the bars transition
     if(!content) return;

     // if opening, clear #shadowbox display
     if(!changing){
   SL.get('shadowbox').style.display = '';
     }

     var fn = function(){
  resizeContent(function(){
      if(!content) return;


      showBars(function(){
   if(!content) return;

   // append content just before hiding the loading layer
   SL.get('shadowbox_body_inner').innerHTML = SL.createHTML(content.markup(dims));

   toggleLoading(false, function(){
       if(!content) return;

       if(typeof content.onLoad == 'function'){
    content.onLoad(); // call onLoad callback if present
       }
       if(options.onFinish && typeof options.onFinish == 'function'){
    options.onFinish(gallery[current]); // fire onFinish handler
       }
       if(slide_timer != 'paused'){
    SB.play(); // kick off next slide
       }
       listenKeys(true); // re-enable the keyboard
   });
      });
  });
     };

     if(typeof content.ready != 'undefined'){ // does the object have a ready property?
  var id = setInterval(function(){ // if so, wait for the object to be ready
      if(content){
   if(content.ready){
       clearInterval(id); // clean up
       id = null;
       fn();
   }
      }else{ // content has been removed
   clearInterval(id);
   id = null;
      }
  }, 100);
     }else{
  fn();
     }
 });

 // preload neighboring gallery images
 if(gallery.length > 1){
     var next = gallery[current + 1] || gallery[0];
     if(next.player == 'img'){
  var a = new Image();
  a.src = next.content;
     }
     var prev = gallery[current - 1] || gallery[gallery.length - 1];
     if(prev.player == 'img'){
  var b = new Image();
  b.src = prev.content;
     }
 }
    };

    /**
     * Calculates the dimensions for Shadowbox, taking into account the borders
     * and surrounding elements of the shadowbox_body. If the height/width
     * combination is too large for Shadowbox and handleOversize option is set
     * to 'resize', the resized dimensions will be returned (preserving the
     * original aspect ratio). Otherwise, the originally calculated dimensions
     * will be used. Stores all dimensions in the private dims variable.
     *
     * @param   Number      height      The content player height
     * @param   Number      width       The content player width
     * @param   Boolean     resizable   True if the content is able to be
     *      resized. Defaults to false.
     * @return  void
     * @private
     */
    var setDimensions = function(height, width, resizable){
 resizable = resizable || false;

 var sb = SL.get('shadowbox_body');
 var h = height = parseInt(height);
 var w = width = parseInt(width);
 var view_h = SL.getViewportHeight();
 var view_w = SL.getViewportWidth();

 // calculate the max width
 var border_w = parseInt(SL.getStyle(sb, 'border-left-width'), 10)
     + parseInt(SL.getStyle(sb, 'border-right-width'), 10);
 var extra_w = border_w + 2 * options.viewportPadding;
 if(w + extra_w >= view_w){
     w = view_w - extra_w;
 }

 // calculate the max height
 var border_h = parseInt(SL.getStyle(sb, 'border-top-width'), 10)
     + parseInt(SL.getStyle(sb, 'border-bottom-width'), 10);
 var bar_h = getComputedHeight(SL.get('shadowbox_title'))
     + getComputedHeight(SL.get('shadowbox_info'));
 var extra_h = border_h + 2 * options.viewportPadding + bar_h;
 if(h + extra_h >= view_h){
     h = view_h - extra_h;
 }

 // handle oversized content
 var drag = false;
 var resize_h = height;
 var resize_w = width;
 var handle = options.handleOversize;
 if(resizable && (handle == 'resize' || handle == 'drag')){
     var change_h = (height - h) / height;
     var change_w = (width - w) / width;
     if(handle == 'resize'){
  if(change_h > change_w){
      w = Math.round((width / height) * h);
  }else if(change_w > change_h){
      h = Math.round((height / width) * w);
  }
  // adjust resized height or width accordingly
  resize_w = w;
  resize_h = h;
     }else{
  // drag on oversized images only
  var link = gallery[current];
  if(link) drag = link.player == 'img' && (change_h > 0 || change_w > 0);
     }
 }

 // update dims
 dims = {
     height:     h + border_h + bar_h,
     width:      w + border_w,
     inner_h:    h,
     inner_w:    w,
     top: (view_h - (h + extra_h)) / 2 + options.viewportPadding,
     resize_h:   resize_h,
     resize_w:   resize_w,
     drag:       drag
 };
    };

    /**
     * Resizes Shadowbox to the given height and width. If the callback
     * parameter is given, the transition will be animated and the callback
     * function will be called when the animation completes. Note: The private
     * content variable must be updated before calling this function.
     *
     * @param   Function    cb      A callback function to execute after the
     *         content has been resized
     * @return  void
     * @private
     */
    var resizeContent = function(cb){
 if(!content) return; // no content

 // set new dimensions
 setDimensions(content.height, content.width, content.resizable);

 if(cb){
     switch(options.animSequence){
  case 'hw':
      adjustHeight(dims.inner_h, dims.top, true, function(){
   adjustWidth(dims.width, true, cb);
      });
      break;
  case 'wh':
      adjustWidth(dims.width, true, function(){
   adjustHeight(dims.inner_h, dims.top, true, cb);
      });
      break;
  case 'sync':
  default:
      adjustWidth(dims.width, true);
      adjustHeight(dims.inner_h, dims.top, true, cb);
     }
 }else{ // window resize
     adjustWidth(dims.width, false);
     adjustHeight(dims.inner_h, dims.top, false);
     var c = SL.get(content_id);
     if(c){
  // resize resizable content when in resize mode
  if(content.resizable && options.handleOversize == 'resize'){
      c.height = dims.resize_h;
      c.width = dims.resize_w;
  }
  // fix draggable positioning if enlarging viewport
  if(gallery[current].player == 'img' && options.handleOversize == 'drag'){
      var top = parseInt(SL.getStyle(c, 'top'));
      if(top + content.height < dims.inner_h){
   SL.setStyle(c, 'top', dims.inner_h - content.height + 'px');
      }
      var left = parseInt(SL.getStyle(c, 'left'));
      if(left + content.width < dims.inner_w){
   SL.setStyle(c, 'left', dims.inner_w - content.width + 'px');
      }
  }
     }
 }
    };

    /**
     * Adjusts the height of #shadowbox_body and centers #shadowbox vertically
     * in the viewport.
     *
     * @param   Number      height      The height to use for #shadowbox_body
     * @param   Number      top  The top to use for #shadowbox
     * @param   Boolean     anim True to animate the transition
     * @param   Function    cb   A callback to use when the animation
     *      completes
     * @return  void
     * @private
     */
    var adjustHeight = function(height, top, anim, cb){
 height = parseInt(height);

 // adjust the height
 var sb = SL.get('shadowbox_body');
 if(anim){
     animate(sb, 'height', height, options.resizeDuration);
 }else{
     SL.setStyle(sb, 'height', height + 'px');
 }

 // adjust the top
 var s = SL.get('shadowbox');
 if(anim){
     animate(s, 'top', top, options.resizeDuration, cb);
 }else{
     SL.setStyle(s, 'top', top + 'px');
     if(typeof cb == 'function') cb();
 }
    };

    /**
     * Adjusts the width of #shadowbox.
     *
     * @param   Number      width       The width to use for #shadowbox
     * @param   Boolean     anim True to animate the transition
     * @param   Function    cb   A callback to use when the animation
     *      completes
     * @return  void
     * @private
     */
    var adjustWidth = function(width, anim, cb){
 width = parseInt(width);

 // adjust the width
 var s = SL.get('shadowbox');
 if(anim){
     animate(s, 'width', width, options.resizeDuration, cb);
 }else{
     SL.setStyle(s, 'width', width + 'px');
     if(typeof cb == 'function') cb();
 }
    };

    /**
     * Sets up a listener on the document for keystrokes.
     *
     * @param   Boolean     on      True to enable the listener, false to turn
     *         it off
     * @return  void
     * @private
     */
    var listenKeys = function(on){
 if(!options.enableKeys) return;
 SL[(on ? 'add' : 'remove') + 'Event'](document, 'keydown', handleKey);
    };

    /**
     * A listener function that is fired when a key is pressed.
     *
     * @param   mixed       e       The event object
     * @return  void
     * @private
     */
    var handleKey = function(e){
 var code = SL.keyCode(e);

 // attempt to prevent default key action
 SL.preventDefault(e);

 if(code == 81 || code == 88 || code == 27){ // q, x, or esc
     SB.close();
 }else if(code == 37){ // left arrow
     SB.previous();
 }else if(code == 39){ // right arrow
     SB.next();
 }else if(code == 32){ // space bar
     SB[(typeof slide_timer == 'number' ? 'pause' : 'play')]();
 }
    };

    /**
     * Toggles the visibility of the "loading" layer.
     *
     * @param   Boolean     on      True to toggle on, false to toggle off
     * @param   Function    cb      The callback function to call when toggling
     *         completes
     * @return  void
     * @private
     */
    var toggleLoading = function(on, cb){
 var loading = SL.get('shadowbox_loading');
 if(on){
     loading.style.display = '';
     if(typeof cb == 'function') cb();
 }else{
     var p = gallery[current].player;
     var anim = (p == 'img' || p == 'html'); // fade on images & html
     var fn = function(){
  loading.style.display = 'none';
  clearOpacity(loading);
  if(typeof cb == 'function') cb();
     };
     if(anim){
  animate(loading, 'opacity', 0, options.fadeDuration, fn);
     }else{
  fn();
     }
 }
    };

    /**
     * Sets the top of the container element. This is only necessary in IE6
     * where the container uses absolute positioning instead of fixed.
     *
     * @return  void
     * @private
     */
    var fixTop = function(){
 SL.get('shadowbox_container').style.top = document.documentElement.scrollTop + 'px';
    };

    /**
     * Sets the height of the overlay element to the full viewport height. This
     * is only necessary in IE6 where the container uses absolute positioning
     * instead of fixed, thus restricting the size of the overlay element.
     *
     * @return  void
     * @private
     */
    var fixHeight = function(){
 SL.get('shadowbox_overlay').style.height = SL.getViewportHeight() + 'px';
    };

    /**
     * Determines if there is a next piece to display in the current gallery.
     *
     * @return  bool     True if there is another piece, false otherwise
     * @private
     */
    var hasNext = function(){
 return gallery.length > 1 && (current != gallery.length - 1 || options.continuous);
    };

    /**
     * Toggles the visibility of #shadowbox_container and sets its size (if on
     * IE6). Also toggles the visibility of elements (<select>, <object>, and
     * <embed>) that are troublesome for semi-transparent modal overlays. IE has
     * problems with <select> elements, while Firefox has trouble with
     * <object>s.
     *
     * @param   Function    cb      A callback to call after toggling on, absent
     *         when toggling off
     * @return  void
     * @private
     */
    var toggleVisible = function(cb){
 var els, v = (cb) ? 'hidden' : 'visible';
 var hide = ['select', 'object', 'embed']; // tags to hide
 for(var i = 0; i < hide.length; ++i){
     els = document.getElementsByTagName(hide[i]);
     for(var j = 0, len = els.length; j < len; ++j){
  els[j].style.visibility = v;
     }
 }

 // resize & show container
 var so = SL.get('shadowbox_overlay');
 var sc = SL.get('shadowbox_container');
 var sb = SL.get('shadowbox');
 if(cb){
     // set overlay color/opacity
     SL.setStyle(so, {
  backgroundColor: options.overlayColor,
  opacity: 0
     });
     if(!options.modal) SL.addEvent(so, 'click', SB.close);
     if(ltIE7){
  // fix container top & overlay height before showing
  fixTop();
  fixHeight();
  SL.addEvent(window, 'scroll', fixTop);
     }

     // fade in animation
     sb.style.display = 'none'; // will be cleared in loadContent()
     sc.style.visibility = 'visible';
     animate(so, 'opacity', parseFloat(options.overlayOpacity), options.fadeDuration, cb);
 }else{
     SL.removeEvent(so, 'click', SB.close);
     if(ltIE7) SL.removeEvent(window, 'scroll', fixTop);

     // fade out effect
     sb.style.display = 'none';
     animate(so, 'opacity', 0, options.fadeDuration, function(){
  sc.style.visibility = 'hidden';
  sb.style.display = '';
  clearOpacity(so);
     });
 }
    };

    /**
     * Initializes the Shadowbox environment. Loads the skin (if necessary),
     * compiles the player matching regular expressions, and sets up the
     * window resize listener.
     *
     * @param   Object      opts    (optional) The default options to use
     * @return  void
     * @public
     * @static
     */
    Shadowbox.init = function(opts){
 // don't initialize twice
 if(initialized) return;

 // make sure language is loaded
 if(typeof SB.LANG == 'undefined'){
     SB.raise('No Shadowbox language loaded');
     return;
 }
 // make sure skin is loaded
 if(typeof SB.SKIN == 'undefined'){
     SB.raise('No Shadowbox skin loaded');
     return;
 }

 // apply custom options
 apply(options, opts || {});

 // add markup
 var markup = SB.SKIN.markup.replace(/\{(\w+)\}/g, function(m, p){
     return SB.LANG[p];
 });
 var bd = document.body || document.documentElement;
 SL.append(bd, markup);

 // several fixes for IE6
 if(ltIE7){
     // give the container absolute positioning
     SL.setStyle(SL.get('shadowbox_container'), 'position', 'absolute');
     // give shadowbox_body "layout"...whatever that is
     SL.get('shadowbox_body').style.zoom = 1;
     // use AlphaImageLoader for transparent PNG support
     var png = SB.SKIN.png_fix;
     if(png && png.constructor == Array){
  for(var i = 0; i < png.length; ++i){
      var el = SL.get(png[i]);
      if(el){
   var match = SL.getStyle(el, 'background-image').match(/url\("(.*\.png)"\)/);
   if(match){
       SL.setStyle(el, {
    backgroundImage: 'none',
    filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true,src=' + match[1] + ',sizingMethod=scale);'
       });
   }
      }
  }
     }
 }

 // compile file type regular expressions here for speed
 for(var e in options.ext){
     RE[e] = new RegExp('\.(' + options.ext[e].join('|') + ')\s*$', 'i');
 }

 // set up window resize event handler
 var id;
 SL.addEvent(window, 'resize', function(){
     // use 50 ms event buffering to prevent jerky window resizing
     if(id){
  clearTimeout(id);
  id = null;
     }
     id = setTimeout(function(){
  if(ltIE7) fixHeight();
  resizeContent();
     }, 50);
 });

 if(!options.skipSetup) SB.setup();
 initialized = true;
    };

    /**
     * Dynamically loads the specified skin for use with Shadowbox. If the skin
     * is included already in the page via the appropriate <script> and <link>
     * tags, this function does not need to be called. Otherwise, this function
     * must be called before window.onload.
     *
     * @param   String      skin The directory where the skin is located
     * @param   String      dir  The directory where the Shadowbox skin
     *      files are located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadSkin = function(skin, dir){
 if(!(/\/$/.test(dir))) dir += '/';
 skin = dir + skin + '/';

 // Safari 2.0 fails using DOM, use document.write instead
 document.write('<link rel="stylesheet" type="text/css" href="' + skin + 'skin.css">');
 document.write('<scr' + 'ipt type="text/javascript" src="' + skin + 'skin.js"><\/script>');
    };

    /**
     * Dynamically loads the specified language file to be used with Shadowbox.
     * If the language file is included already in the page via the appropriate
     * <script> tag, this function does not need to be called. Otherwise, this
     * function must be called before window.onload.
     *
     * @param   String      lang The language abbreviation (e.g. en)
     * @param   String      dir  The directory where the Shadowbox
     *      language file(s) is located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadLanguage = function(lang, dir){
 if(!(/\/$/.test(dir))) dir += '/';

 // Safari 2.0 fails using DOM, use document.write instead
 document.write('<scr' + 'ipt type="text/javascript" src="' + dir + 'shadowbox-' + lang + '.js"><\/script>');
    };

    /**
     * Dynamically loads the specified player(s) to be used with Shadowbox. If
     * the needed player(s) is already included in the page via the appropriate
     * <script> tag(s), this function does not need to be called. Otherwise,
     * this function must be called before window.onload.
     *
     * @param   Array       players     The player(s) to load
     * @param   String      dir  The director where the Shadowbox player
     *      file(s) is located
     * @return  void
     * @public
     * @static
     */
    Shadowbox.loadPlayer = function(players, dir){
 if(typeof players == 'string') players = [players];
 if(!(/\/$/.test(dir))) dir += '/';

 for(var i = 0, len = players.length; i < len; ++i){
     // Safari 2.0 fails using DOM, use document.write instead
     document.write('<scr' + 'ipt type="text/javascript" src="' + dir + 'shadowbox-' + players[i] + '.js"><\/script>');
 }
    };

    /**
     * Sets up listeners on the given links that will trigger Shadowbox. If no
     * links are given, this method will set up every anchor element on the page
     * with the appropriate rel attribute. Note: Because AREA elements do not
     * support the rel attribute, they must be explicitly passed to this method.
     *
     * @param   Array       links       An array (or array-like) list of anchor
     *      and/or area elements to set up
     * @param   Object      opts Some options to use for the given links
     * @return  void
     * @public
     * @static
     */
    Shadowbox.setup = function(links, opts){
 // get links if none specified
 if(!links){
     var links = [];
     var a = document.getElementsByTagName('a'), rel;
     for(var i = 0, len = a.length; i < len; ++i){
  rel = a[i].getAttribute('rel');
  if(rel && RE.rel.test(rel)) links[links.length] = a[i];
     }
 }else if(!links.length){
     links = [links]; // one link
 }

 var link;
 for(var i = 0, len = links.length; i < len; ++i){
     link = links[i];
     if(typeof link.shadowboxCacheKey == 'undefined'){
  // assign cache key expando
  // use integer primitive to avoid memory leak in IE
  link.shadowboxCacheKey = cache.length;
  SL.addEvent(link, 'click', handleClick); // add listener
     }
     cache[link.shadowboxCacheKey] = this.buildCacheObj(link, opts);
 }
    };

    /**
     * Builds an object from the original link element data to store in cache.
     * These objects contain (most of) the following keys:
     *
     * - el: the link element
     * - title: the linked file title
     * - player: the player to use for the linked file
     * - content: the linked file's URL
     * - gallery: the gallery the file belongs to (optional)
     * - height: the height of the linked file (only necessary for movies)
     * - width: the width of the linked file (only necessary for movies)
     * - options: custom options to use (optional)
     *
     * @param   HTMLElement     link    The link element to process
     * @return  Object    An object representing the link
     * @public
     * @static
     */
    Shadowbox.buildCacheObj = function(link, opts){
 var href = link.href; // don't use getAttribute() here
 var o = {
     el:  link,
     title:      link.getAttribute('title'),
     player:     getPlayer(href),
     options:    apply({}, opts || {}), // break the reference
     content:    href
 };

 // remove link-level options from top-level options
 var opt, l_opts = ['player', 'title', 'height', 'width', 'gallery'];
 for(var i = 0, len = l_opts.length; i < len; ++i){
     opt = l_opts[i];
     if(typeof o.options[opt] != 'undefined'){
  o[opt] = o.options[opt];
  delete o.options[opt];
     }
 }

 // HTML options always trump JavaScript options, so do these last
 var rel = link.getAttribute('rel');
 if(rel){
     // extract gallery name from shadowbox[name] format
     var match = rel.match(RE.gallery);
     if(match) o.gallery = escape(match[2]);

     // other parameters
     var params = rel.split(';');
     for(var i = 0, len = params.length; i < len; ++i){
  match = params[i].match(RE.param);
  if(match){
      if(match[1] == 'options'){
   eval('apply(o.options, ' + match[2] + ')');
      }else{
   o[match[1]] = match[2];
      }
  }
     }
 }

 return o;
    };

    /**
     * Applies the given set of options to those currently in use. Note: Options
     * will be reset on Shadowbox.open() so this function is only useful after
     * it has already been called (while Shadowbox is open).
     *
     * @param   Object      opts The options to apply
     * @return  void
     * @public
     * @static
     */
    Shadowbox.applyOptions = function(opts){
 if(opts){
     // use apply here to break references
     default_options = apply({}, options); // store default options
     options = apply(options, opts); // apply options
 }
    };

    /**
     * Reverts Shadowbox' options to the last default set in use before
     * Shadowbox.applyOptions() was called.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.revertOptions = function(){
 if(default_options){
     options = default_options; // revert to default options
     default_options = null; // erase for next time
 }
    };

    /**
     * Opens the given object in Shadowbox. This object may be either an
     * anchor/area element, or an object similar to the one created by
     * Shadowbox.buildCacheObj().
     *
     * @param   mixed       obj  The object or link element that defines
     *      what to display
     * @return  void
     * @public
     * @static
     */
    Shadowbox.open = function(obj, opts){
 // revert options
 this.revertOptions();

 // is it a link?
 if(isLink(obj)){
     if(typeof obj.shadowboxCacheKey == 'undefined' || typeof cache[obj.shadowboxCacheKey] == 'undefined'){
  // link element that hasn't been set up before
  // create on-the-fly object
  obj = this.buildCacheObj(obj, opts);
     }else{
  // link element that has been set up before, get from cache
  obj = cache[obj.shadowboxCacheKey];
     }
 }

 // is it already a gallery?
 if(obj.constructor == Array){
     gallery = obj;
     current = 0;
 }else{
     // create a copy so it doesn't get modified later
     var copy = apply({}, obj);

     // is it part of a gallery?
     if(!obj.gallery){ // single item, no gallery
  gallery = [copy];
  current = 0;
     }else{
  current = null; // reset current
  gallery = []; // clear the current gallery
  var ci;
  for(var i = 0, len = cache.length; i < len; ++i){
      ci = cache[i];
      if(ci.gallery){
   if(ci.content == obj.content
       && ci.gallery == obj.gallery
       && ci.title == obj.title){ // compare content, gallery, & title
    current = gallery.length; // key element found
   }
   if(ci.gallery == obj.gallery){
       gallery.push(apply({}, ci));
   }
      }
  }
  // if not found in cache, prepend to front of gallery
  if(current == null){
      gallery.unshift(copy);
      current = 0;
  }
     }
 }

 obj = gallery[current];

 // apply custom options
 if(obj.options || opts){
     // use apply here to break references
     this.applyOptions(apply(apply({}, obj.options || {}), opts || {}));
 }

 // filter gallery for unsupported elements
 var match, r;
 for(var i = 0, len = gallery.length; i < len; ++i){
     r = false; // remove the element?
     if(gallery[i].player == 'unsupported'){ // don't support this at all
  r = true;
     }else if(match = RE.unsupported.exec(gallery[i].player)){ // handle unsupported elements
  if(options.handleUnsupported == 'link'){
      gallery[i].player = 'html';
      // generate a link to the appropriate plugin download page(s)
      var s, a, oe = options.errors;
      switch(match[1]){
   case 'qtwmp':
       s = 'either';
       a = [oe.qt.url, oe.qt.name, oe.wmp.url, oe.wmp.name];
   break;
   case 'qtf4m':
       s = 'shared';
       a = [oe.qt.url, oe.qt.name, oe.f4m.url, oe.f4m.name];
   break;
   default:
       s = 'single';
       if(match[1] == 'swf' || match[1] == 'flv') match[1] = 'fla';
       a = [oe[match[1]].url, oe[match[1]].name];
      }
      var msg = SB.LANG.errors[s].replace(/\{(\d+)\}/g, function(m, i){
   return a[i];
      });
      gallery[i].content = '<div class="shadowbox_message">' + msg + '</div>';
  }else{
      r = true;
  }
     }else if(gallery[i].player == 'inline'){ // handle inline elements
  // retrieve the innerHTML of the inline element
  var match = RE.inline.exec(gallery[i].content);
  if(match){
      var el;
      if(el = SL.get(match[1])){
   gallery[i].content = el.innerHTML;
      }else{
   SB.raise('Cannot find element with id ' + match[1]);
      }
  }else{
      SB.raise('Cannot find element id for inline content');
  }
     }
     if(r){
  gallery.splice(i, 1); // remove the element from the gallery
  if(i < current){
      --current;
  }else if(i == current){
      // if current is unsupported, look for supported neighbor
      current = i > 0 ? current - 1 : i;
  }
  --i; // decrement to account for splice
  len = gallery.length; // gallery.length has changed!
     }
 }

 // anything left?
 if(gallery.length){
     // fire onOpen hook
     if(options.onOpen && typeof options.onOpen == 'function'){
  options.onOpen(obj);
     }

     if(!activated){
  // set initial dimensions & load
  setDimensions(options.initialHeight, options.initialWidth);
  adjustHeight(dims.inner_h, dims.top, false);
  adjustWidth(dims.width, false);
  toggleVisible(loadContent);
     } else {
  loadContent();
     }

     activated = true;
 }
    };

    /**
     * Jumps to the piece in the current gallery with index num.
     *
     * @param   Number      num     The gallery index to view
     * @return  void
     * @public
     * @static
     */
    Shadowbox.change = function(num){
 if(!gallery) return; // no current gallery
 if(!gallery[num]){ // index does not exist
     if(!options.continuous){
  return;
     }else{
  num = num < 0 ? (gallery.length - 1) : 0; // loop
     }
 }

 if(typeof slide_timer == 'number'){
     clearTimeout(slide_timer);
     slide_timer = null;
     slide_delay = slide_start = 0; // reset slideshow variables
 }
 current = num; // update current

 if(options.onChange && typeof options.onChange == 'function'){
     options.onChange(gallery[current]); // fire onChange handler
 }

 loadContent();
    };

    /**
     * Jumps to the next piece in the gallery.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.next = function(){
 this.change(current + 1);
    };

    /**
     * Jumps to the previous piece in the gallery.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.previous = function(){
 this.change(current - 1);
    };

    /**
     * Sets the timer for the next image in the slideshow to be displayed.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.play = function(){
 if(!hasNext()) return;
 if(!slide_delay) slide_delay = options.slideshowDelay * 1000;
 if(slide_delay){
     slide_start = new Date().getTime();
     slide_timer = setTimeout(function(){
  slide_delay = slide_start = 0; // reset slideshow
  SB.next();
     }, slide_delay);

     // change play nav to pause
     toggleNav('play', false);
     toggleNav('pause', true);
 }
    };

    /**
     * Pauses the current slideshow.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.pause = function(){
 if(typeof slide_timer == 'number'){
     var time = new Date().getTime();
     slide_delay = Math.max(0, slide_delay - (time - slide_start));

     // any delay left on current slide? if so, stop the timer
     if(slide_delay){
  clearTimeout(slide_timer);
  slide_timer = 'paused';
     }

     // change pause nav to play
     toggleNav('pause', false);
     toggleNav('play', true);
 }
    };

    /**
     * Deactivates Shadowbox.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.close = function(){
 if(!activated) return; // already closed

 // stop listening for keys
 listenKeys(false);
 // hide
 toggleVisible(false);
 // remove the content
 if(content){
     content.remove();
     content = null;
 }

 // clear slideshow variables
 if(typeof slide_timer == 'number') clearTimeout(slide_timer);
 slide_timer = null;
 slide_delay = 0;

 // fire onClose handler
 if(options.onClose && typeof options.onClose == 'function'){
     options.onClose(gallery[current]);
 }

 activated = false;
    };

    /**
     * Clears Shadowbox' cache and removes listeners and expandos from all
     * cached link elements. May be used to completely reset Shadowbox in case
     * links on a page change.
     *
     * @return  void
     * @public
     * @static
     */
    Shadowbox.clearCache = function(){
 for(var i = 0, len = cache.length; i < len; ++i){
     if(cache[i].el){
  SL.removeEvent(cache[i].el, 'click', handleClick);
  delete cache[i].el.shadowboxCacheKey; // remove expando
     }
 }
 cache = [];
    };

    /**
     * Gets an object that lists which plugins are supported by the client. The
     * keys of this object will be:
     *
     * - fla: Adobe Flash Player
     * - qt: QuickTime Player
     * - wmp: Windows Media Player
     * - f4m: Flip4Mac QuickTime Player
     *
     * @return  Object   The plugins object
     * @public
     * @static
     */
    Shadowbox.getPlugins = function(){
 return plugins;
    };

    /**
     * Gets the current options object in use.
     *
     * @return  Object   The options object
     * @public
     * @static
     */
    Shadowbox.getOptions = function(){
 return options;
    };

    /**
     * Gets the current gallery object.
     *
     * @return  Object   The current gallery item
     * @public
     * @static
     */
    Shadowbox.getCurrent = function(){
 return gallery[current];
    };

    /**
     * Gets the current version number of Shadowbox.
     *
     * @return  String   The current version
     * @public
     * @static
     */
    Shadowbox.getVersion = function(){
 return version;
    };

    /**
     * Returns an object containing information about the current client
     * configuration.
     *
     * @return  Object   The object containing client data
     * @public
     * @static
     */
    Shadowbox.getClient = function(){
 return client;
    };

    /**
     * Returns the current content object in use.
     *
     * @return  Object   The current content object
     * @public
     * @static
     */
    Shadowbox.getContent = function(){
 return content;
    };

    /**
     * Gets the current dimensions of Shadowbox as calculated by
     * setDimensions().
     *
     * @return  Object   The current dimensions of Shadowbox
     * @public
     * @static
     */
    Shadowbox.getDimensions = function(){
 return dims;
    };

    /**
     * Handles all Shadowbox exceptions (errors). Calls the exception
     * handler callback if one is present (see handleException option) or
     * throws a new exception.
     *
     * @param   String      e       The error message
     * @return  void
     * @public
     * @static
     */
    Shadowbox.raise = function(e){
 if(typeof options.handleException == 'function'){
     options.handleException(e);
 }else{
     throw e;
 }
    };

})();
