/**
 * A media-viewer script for web pages that allows content to be viewed without
 * navigating away from the original linking page.
 * 
 * This file is part of Shadowbox.
 * 
 * Shadowbox is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * Shadowbox is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Shadowbox. If not, see <http://www.gnu.org/licenses/>.
 * 
 * @author Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright 2007 Michael J. I. Jackson
 * @license http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version SVN: $Id: shadowbox.js 75 2008-02-21 16:51:29Z 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://support.microsoft.com/kb/316992 -
 * http://www.apple.com/quicktime/player/specs.html -
 * http://www.howtocreate.co.uk/wrongWithIE/?chapter=navigator.plugins
 * 
 * @class Shadowbox
 * @author Michael J. I. Jackson <mjijackson@gmail.com>
 * @singleton
 */
( function() {

	/**
	 * The current version of Shadowbox.
	 * 
	 * @property {String} version
	 * @private
	 */
	var version = '1.0';

	/**
	 * Contains the default options for Shadowbox. This object is almost
	 * entirely customizable.
	 * 
	 * @property {Object} options
	 * @private
	 */
	var options = {

		/**
		 * A base URL that will be prepended to the loadingImage, flvPlayer, and
		 * overlayBgImage options to save on typing.
		 * 
		 * @var {String} assetURL
		 */
		assetURL :'',

		/**
		 * The path to the image to display while loading.
		 * 
		 * @var {String} loadingImage
		 */
		loadingImage :'images/loading.gif',

		/**
		 * Enable animations.
		 * 
		 * @var {Boolean} animate
		 */
		animate :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
		 */
		animSequence :'wh',

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

		/**
		 * The background color and opacity of the overlay. Note: When viewing
		 * movie files on FF Mac, the default background image will be used
		 * because that browser has problems displaying movies above layers that
		 * aren't 100% opaque.
		 * 
		 * @var {String} overlayColor
		 */
		overlayColor :'#000',

		/**
		 * The background opacity to use for the overlay.
		 * 
		 * @var {Number} overlayOpacity
		 */
		overlayOpacity :0.90,

		/**
		 * A background image to use for browsers such as FF Mac that don't
		 * support displaying movie content over backgrounds that aren't 100%
		 * opaque.
		 * 
		 * @var {String} overlayBgImage
		 */
		overlayBgImage :'images/overlay-85.png',

		/**
		 * Listen to the overlay for clicks. If the user clicks the overlay, it
		 * will trigger Shadowbox.close().
		 * 
		 * @var {Boolean} listenOverlay
		 */
		listenOverlay :true,

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

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

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

		/**
		 * The duration of the overlay fade animation (in seconds).
		 * 
		 * @var {Number} fadeDuration
		 */
		fadeDuration :0.35,

		/**
		 * Show the navigation controls.
		 * 
		 * @var {Boolean} displayNav
		 */
		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
		 */
		continuous :false,

		/**
		 * Display the gallery counter.
		 * 
		 * @var {Boolean} displayCounter
		 */
		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
		 */
		counterType :'default',

		/**
		 * 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
		 */
		viewportPadding :20,

		/**
		 * How to handle images that are too large for the viewport. 'resize'
		 * will resize the image while preserving aspect ratio and display it at
		 * the smaller resolution. 'drag' will display the image at its native
		 * resolution but it will be draggable within the Shadowbox. 'none' will
		 * display the image at its native resolution but it may be cropped.
		 * 
		 * @var {String} handleLgImages
		 */
		handleLgImages :'resize',

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

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

		/**
		 * Enable keyboard control. Note: If you disable the keys, you may want
		 * to change the visual styles for the navigation elements that suggest
		 * keyboard shortcuts.
		 * 
		 * @var {Boolean} enableKeys
		 */
		enableKeys :true,

		/**
		 * The keys used to control Shadowbox. Note: In order to use these,
		 * enableKeys must be true. Key values or key codes may be used.
		 * 
		 * @var {Array}
		 */
		keysClose : [ 'f', 's', 27 ], // c, q, or esc
		keysNext : [ 'p', 39 ], // n or right arrow
		keysPrev : [ 'a', 37 ], // p or left arrow
		keysPrint : [ 'i', 40 ], // i to print

		/**
		 * 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,

		/**
		 * 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
		 */
		handleUnsupported :'link',

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

		/**
		 * Text messages to use for Shadowbox. These are provided so they may be
		 * translated into different languages.
		 * 
		 * @var {Object} text
		 */
		text : {

			cancel :'Cancelar',

			loading :'Carregando',

			print :'<span class="shortcut">I</span>mprimir',

			close :'<span class="shortcut">F</span>echar',

			next :'<span class="shortcut">P</span>roximo',

			prev :'<span class="shortcut">A</span>nterior',

			errors : {
				single :'You must install the <a href="{0}">{1}</a> browser plugin to view this content.',
				shared :'You must install both the <a href="{0}">{1}</a> and <a href="{2}">{3}</a> browser plugins to view this content.',
				either :'You must install either the <a href="{0}">{1}</a> or the <a href="{2}">{3}</a> browser plugin to view this content.'
			}

		},

		/**
		 * An object containing names of plugins and links to their respective
		 * download pages.
		 * 
		 * @var {Object} errors
		 */
		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'
			}

		},

		/**
		 * The HTML markup to use for Shadowbox. Note: The script depends on
		 * most of these elements being present, so don't modify this variable
		 * unless you know what you're doing.
		 * 
		 * @var {Object} skin
		 */
		skin : {

			main :'<div id="shadowbox_overlay"></div>'
					+ '<div id="shadowbox_container">' + '<div id="shadowbox">'
					+ '<div id="shadowbox_title">'
					+ '<div id="shadowbox_title_inner"></div>' + '</div>'
					+ '<div id="shadowbox_body">'
					+ '<div id="shadowbox_body_inner"></div>'
					+ '<div id="shadowbox_loading"></div>' + '</div>'
					+ '<div id="shadowbox_toolbar">'
					+ '<div id="shadowbox_toolbar_inner"></div>' + '</div>'
					+ '</div>' + '</div>',

			loading :'<img src="{0}" alt="{1}" />' + '<span><a href="javascript:Shadowbox.close();">{2}</a></span>',

			counter :'<div id="shadowbox_counter">{0}</div>',

			close :'<div id="shadowbox_nav_close">' + '<a href="javascript:Shadowbox.close();">{0}</a>' + '</div>',

			print :'<div id="shadowbox_nav_print">' + '<a href="javascript:Shadowbox.print();">{0}</a>' + '</div>',

			next :'<div id="shadowbox_nav_next">' + '<a href="javascript:Shadowbox.next();">{0}</a>' + '</div>',

			prev :'<div id="shadowbox_nav_previous">' + '<a href="javascript:Shadowbox.previous();">{0}</a>' + '</div>'

		},

		/**
		 * An object containing arrays of all supported file extensions. Each
		 * property of this object contains an array. If this object is to be
		 * modified, it must be done before calling init().
		 *  - img: Supported image file extensions - 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
		 * 
		 * @var {Object} ext
		 */
		ext : {
			img : [ 'png', 'jpg', 'jpeg', 'gif', 'bmp' ],
			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' ]
		}

	};

	/**
	 * 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.
	 * 
	 * @property {Object} default_options
	 * @private
	 */
	var default_options = null;

	/**
	 * Shorthand for Shadowbox.lib.
	 * 
	 * @property {Object} SL
	 * @private
	 */
	var SL = Shadowbox.lib;

	/**
	 * An object containing some regular expressions we'll need later. Compiled
	 * up front for speed.
	 * 
	 * @property {Object} RE
	 * @private
	 */
	var RE = {
		resize :/(img|swf|flv)/, // file types to resize
		overlay :/(img|iframe|html|inline)/, // content types to not use an
												// overlay image for on FF Mac
		swf :/\.swf\s*$/i, // swf file extension
		flv :/\.flv\s*$/i, // flv file extension
		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.
	 * 
	 * @property {Array} cache
	 * @private
	 */
	var cache = [];

	/**
	 * An array of pieces currently being viewed. In the case of non-gallery
	 * pieces, this will only hold one object.
	 * 
	 * @property {Array} current_gallery
	 * @private
	 */
	var current_gallery;

	/**
	 * The array index of the current_gallery that is currently being viewed.
	 * 
	 * @property {Number} current
	 * @private
	 */
	var current;

	/**
	 * Keeps track of the current optimal height of the box. We use this so that
	 * if the user resizes the browser window to get a better view, and we're
	 * currently at a size smaller than the optimal, we can resize easily.
	 * 
	 * @see resizeContent()
	 * @property {Number} optimal_height
	 * @private
	 */
	var optimal_height = options.initialHeight;

	/**
	 * Keeps track of the current optimal width of the box. See optimal_height
	 * explanation (above).
	 * 
	 * @property {Number} optimal_width
	 * @private
	 */
	var optimal_width = options.initialWidth;

	/**
	 * Keeps track of the current height of the box. This is useful in drag
	 * calculations.
	 * 
	 * @property {Number} current_height
	 * @private
	 */
	var current_height = 0;

	/**
	 * Keeps track of the current width of the box. Useful in drag calculations.
	 * 
	 * @property {Number} current_width
	 * @private
	 */
	var current_width = 0;

	/**
	 * Resource used to preload images. It's class-level so that when a new
	 * image is requested, the same resource can be reassigned, cancelling the
	 * original's callback.
	 * 
	 * @property {HTMLElement} preloader
	 * @private
	 */
	var preloader;

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

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

	/**
	 * Keeps track of 4 floating values (x, y, start_x, & start_y) that are used
	 * in the drag calculations.
	 * 
	 * @property {Object} drag
	 * @private
	 */
	var drag;

	/**
	 * Holds the draggable element so we don't have to fetch it every time the
	 * mouse moves.
	 * 
	 * @property {HTMLElement} draggable
	 * @private
	 */
	var draggable;

	/**
	 * Keeps track of whether or not we're currently using the overlay
	 * background image to display the current gallery. We do this because we
	 * use different methods for fading the overlay in and out. The color fill
	 * overlay fades in and out nicely, but the image overlay stutters. By
	 * keeping track of the type of overlay in use, we don't have to check again
	 * what type of overlay we're using when it's time to get rid of it later.
	 * 
	 * @property {Boolean} overlay_img_needed
	 * @private
	 */
	var overlay_img_needed;

	/**
	 * These parameters for simple browser detection. Used in Ext.js.
	 * 
	 * @ignore
	 */
	var ua = navigator.userAgent.toLowerCase();
	var isStrict = document.compatMode == 'CSS1Compat', isOpera = ua
			.indexOf("opera") > -1, isIE = ua.indexOf('msie') > -1, isIE7 = ua
			.indexOf('msie 7') > -1, isBorderBox = isIE && !isStrict, isSafari = (/webkit|khtml/)
			.test(ua), isSafari3 = isSafari && !!(document.evaluate), isGecko = !isSafari
			&& ua.indexOf('gecko') > -1, 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);

	/**
	 * Do we need to hack the position to make Shadowbox appear fixed? We could
	 * hack this using CSS, but let's just get over all the hacks and let IE6
	 * users get what they deserve! Down with hacks! Hmm...now that I think
	 * about it, I should just flash all kinds of alerts and annoying popups on
	 * their screens, and then redirect them to some foreign spyware site that
	 * will upload a nasty virus...
	 * 
	 * @property {Boolean} absolute_pos
	 * @private
	 */
	var absolute_pos = isIE && !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
	 * 
	 * @property {Object} plugins
	 * @private
	 */
	var plugins = null;

	// 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');
		var 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;
		};
		var plugins = {
			fla :detectPlugin('ShockwaveFlash.ShockwaveFlash'),
			qt :detectPlugin('QuickTime.QuickTime'),
			wmp :detectPlugin('wmplayer.ocx'),
			f4m :false
		};
	}

	/**
	 * Applies all properties of e to o. This function is recursive so that if
	 * any properties of e are themselves objects, those objects will be applied
	 * to objects with the same key that may exist in 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 (deep)
	 * @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 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 height = window.innerHeight; // Safari
		var mode = document.compatMode;
		if ((mode || isIE) && !isOpera) {
			height = isStrict ? document.documentElement.clientHeight
					: document.body.clientHeight;
		}
		return height;
	};

	/**
	 * 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 width = window.innerWidth; // Safari
		var mode = document.compatMode;
		if (mode || isIE) {
			width = isStrict ? document.documentElement.clientWidth
					: document.body.clientWidth;
		}
		return width;
	};

	/**
	 * Gets the height of the document (body and its margins) in pixels.
	 * 
	 * @return {Number} The height of the document
	 * @public
	 * @static
	 */
	SL.getDocumentHeight = function() {
		var scrollHeight = isStrict ? document.documentElement.scrollHeight
				: document.body.scrollHeight;
		return Math.max(scrollHeight, SL.getViewportHeight());
	};

	/**
	 * Gets the width of the document (body and its margins) in pixels.
	 * 
	 * @return {Number} The width of the document
	 * @public
	 * @static
	 */
	SL.getDocumentWidth = function() {
		var scrollWidth = isStrict ? document.documentElement.scrollWidth
				: document.body.scrollWidth;
		return Math.max(scrollWidth, SL.getViewportWidth());
	};

	/**
	 * A utility function used by the fade functions to clear the opacity style
	 * setting of the given element. Required in some cases for IE. Based on
	 * Ext.Element's clearOpacity.
	 * 
	 * @param {HTMLElement}
	 *            el The DOM element
	 * @return void
	 * @private
	 */
	var clearOpacity = function(el) {
		if (isIE) {
			if (typeof el.style.filter == 'string'
					&& (/alpha/i).test(el.style.filter)) {
				el.style.filter = '';
			}
		} else {
			el.style.opacity = '';
			el.style['-moz-opacity'] = '';
			el.style['-khtml-opacity'] = '';
		}
	};

	/**
	 * Fades the given element from 0 to the specified opacity.
	 * 
	 * @param {HTMLElement}
	 *            el The DOM element to fade
	 * @param {Number}
	 *            endingOpacity The final opacity to animate to
	 * @param {Number}
	 *            duration The duration of the animation (in seconds)
	 * @param {Function}
	 *            callback A callback function to call when the animation
	 *            completes
	 * @return void
	 * @private
	 */
	var fadeIn = function(el, endingOpacity, duration, callback) {
		if (options.animate) {
			SL.setStyle(el, 'opacity', 0);
			el.style.visibility = 'visible';
			SL.animate(el, {
				opacity : {
					to :endingOpacity
				}
			}, duration, function() {
				if (endingOpacity == 1)
					clearOpacity(el);
				if (typeof callback == 'function')
					callback();
			});
		} else {
			if (endingOpacity == 1) {
				clearOpacity(el);
			} else {
				SL.setStyle(el, 'opacity', endingOpacity);
			}
			el.style.visibility = 'visible';
			if (typeof callback == 'function')
				callback();
		}
	};

	/**
	 * Fades the given element from its current opacity to 0.
	 * 
	 * @param {HTMLElement}
	 *            el The DOM element to fade
	 * @param {Number}
	 *            duration The duration of the fade animation
	 * @param {Function}
	 *            callback A callback function to call when the animation
	 *            completes
	 * @return void
	 * @private
	 */
	var fadeOut = function(el, duration, callback) {
		var cb = function() {
			el.style.visibility = 'hidden';
			clearOpacity(el);
			if (typeof callback == 'function')
				callback();
		};
		if (options.animate) {
			SL.animate(el, {
				opacity : {
					to :0
				}
			}, duration, cb);
		} else {
			cb();
		}
	};

	/**
	 * Appends an HTML fragment to the given element.
	 * 
	 * @param {String/HTMLElement}
	 *            el The element to append to
	 * @param {String}
	 *            html The HTML fragment to use
	 * @return {HTMLElement} The newly appended element
	 * @private
	 */
	var appendHTML = function(el, html) {
		el = SL.get(el);
		if (el.insertAdjacentHTML) {
			el.insertAdjacentHTML('BeforeEnd', html);
			return el.lastChild;
		}
		if (el.lastChild) {
			var range = el.ownerDocument.createRange();
			range.setStartAfter(el.lastChild);
			var frag = range.createContextualFragment(html);
			el.appendChild(frag);
			return el.lastChild;
		} else {
			el.innerHTML = html;
			return el.lastChild;
		}
	};

	/**
	 * Overwrites the HTML of the given element.
	 * 
	 * @param {String/HTMLElement}
	 *            el The element to overwrite
	 * @param {String}
	 *            html The new HTML to use
	 * @return {HTMLElement} The new firstChild element
	 * @private
	 */
	var overwriteHTML = function(el, html) {
		el = SL.get(el);
		el.innerHTML = html;
		return el.firstChild;
	};

	/**
	 * Gets either the offsetHeight or the height of the given element plus
	 * padding and borders (when offsetHeight is not available). Based on
	 * Ext.Element's getComputedHeight.
	 * 
	 * @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 (!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;
	};

	/**
	 * Gets either the offsetWidth or the width of the given element plus
	 * padding and borders (when offsetWidth is not available). Based on
	 * Ext.Element's getComputedWidth.
	 * 
	 * @return {Number} The computed width of the element
	 * @private
	 */
	var getComputedWidth = function(el) {
		var w = Math.max(el.offsetWidth, el.clientWidth);
		if (!w) {
			w = parseInt(SL.getStyle(el, 'width'), 10) || 0;
			if (!isBorderBox) {
				w += parseInt(SL.getStyle(el, 'padding-left'), 10)
						+ parseInt(SL.getStyle(el, 'padding-right'), 10)
						+ parseInt(SL.getStyle(el, 'border-left-width'), 10)
						+ parseInt(SL.getStyle(el, 'border-right-width'), 10);
			}
		}
		return w;
	};

	/**
	 * 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 getPlayerType = function(url) {
		if (RE.img.test(url))
			return 'img';
		var match = url.match(RE.domain);
		var this_domain = match ? document.domain == match[1] : false;
		if (url.indexOf('#') > -1 && this_domain)
			return 'inline';
		var q_index = url.indexOf('?');
		if (q_index > -1)
			url = url.substring(0, q_index); // strip query string for player
												// detection purposes
		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';
			} else if (plugins.f4m) {
				return 'qt';
			} else {
				return isMac ? (plugins.qt ? 'unsupported-f4m'
						: 'unsupported-qtf4m') : 'unsupported-wmp';
			}
		} else if (RE.qtwmp.test(url)) {
			if (plugins.qt) {
				return 'qt';
			} else if (plugins.wmp) {
				return 'wmp';
			} else {
				return isMac ? 'unsupported-qt' : 'unsupported-qtwmp';
			}
		} else if (!this_domain || RE.iframe.test(url)) {
			return 'iframe';
		}
		return 'unsupported';
	};

	/**
	 * 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
			while (!isLink(link) && link.parentNode) {
				link = link.parentNode;
			}
		}

		Shadowbox.open(link);
		if (current_gallery.length)
			SL.preventDefault(ev);
	};

	/**
	 * Sets up the current gallery for the given object. Modifies the current
	 * and current_gallery variables to contain the appropriate information.
	 * Also, checks to see if there are any gallery pieces that are not
	 * supported by the client's browser/plugins. If there are, they will be
	 * handled according to the handleUnsupported option.
	 * 
	 * @param {Object}
	 *            obj The content to get the gallery for
	 * @return void
	 * @private
	 */
	var setupGallery = function(obj) {
		// 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
			current_gallery = [ copy ];
			current = 0;
		} else {
			current_gallery = []; // clear the current gallery
			var index, 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
						index = current_gallery.length; // key element found
					}
					if (ci.gallery == obj.gallery) {
						current_gallery.push(apply( {}, ci));
					}
				}
			}
			// if not found in cache, prepend to front of gallery
			if (index == null) {
				current_gallery.unshift(copy);
				index = 0;
			}
			current = index;
		}

		// are any media in the current gallery supported?
		var match, r;
		for ( var i = 0, len = current_gallery.length; i < len; ++i) {
			r = false;
			if (current_gallery[i].type == 'unsupported') { // don't support
															// this at all
				r = true;
			} else if (match = RE.unsupported.exec(current_gallery[i].type)) { // handle
																				// unsupported
																				// elements
				if (options.handleUnsupported == 'link') {
					current_gallery[i].type = 'html';
					// generate a link to the appropriate plugin download
					// page(s)
					var m;
					switch (match[1]) {
					case 'qtwmp':
						m = String
								.format(options.text.errors.either,
										options.errors.qt.url,
										options.errors.qt.name,
										options.errors.wmp.url,
										options.errors.wmp.name);
						break;
					case 'qtf4m':
						m = String
								.format(options.text.errors.shared,
										options.errors.qt.url,
										options.errors.qt.name,
										options.errors.f4m.url,
										options.errors.f4m.name);
						break;
					default:
						if (match[1] == 'swf' || match[1] == 'flv')
							match[1] = 'fla';
						m = String.format(options.text.errors.single,
								options.errors[match[1]].url,
								options.errors[match[1]].name);
					}
					current_gallery[i] = apply(
							current_gallery[i],
							{
								height :160, // error messages are short so
												// they
								width :320, // only need a small box to display
											// properly
								content :'<div class="shadowbox_message">' + m + '</div>'
							});
				} else {
					r = true;
				}
			} else if (current_gallery[i].type == 'inline') { // handle inline
																// elements
				// retrieve the innerHTML of the inline element
				var match = RE.inline.exec(current_gallery[i].content);
				if (match) {
					var el;
					if (el = SL.get(match[1])) {
						current_gallery[i].content = el.innerHTML;
					} else {
						throw 'No element found with id ' + match[1];
					}
				} else {
					throw 'No element id found for inline content';
				}
			}
			if (r) {
				// remove the element from the gallery
				current_gallery.splice(i, 1);
				if (i < current)
					--current;
				--i;
			}
		}
	};

	/**
	 * Hides the title bar and toolbar and populates them with the proper
	 * content.
	 * 
	 * @return void
	 * @private
	 */
	var buildBars = function() {
		var link = current_gallery[current];
		if (!link)
			return; // nothing to build

		// build the title
		var title_i = SL.get('shadowbox_title_inner');
		title_i.innerHTML = (link.title) ? link.title : '';
		// empty the toolbar
		var tool_i = SL.get('shadowbox_toolbar_inner');
		tool_i.innerHTML = '';

		// build the nav
		if (options.displayNav) {
			tool_i.innerHTML = String.format(options.skin.close, options.text.close);
			appendHTML(tool_i, String.format(options.skin.print, options.text.print));
			if (current_gallery.length > 1) {
				if (options.continuous) {
					// show both
					appendHTML(tool_i, String.format(options.skin.next, options.text.next));
					appendHTML(tool_i, String.format(options.skin.prev, options.text.prev));
				} else {
					// not last in the gallery, show the next link
					if ((current_gallery.length - 1) > current) {
						appendHTML(tool_i, String.format(options.skin.next, options.text.next));
					}
					// not first in the gallery, show the previous link
					if (current > 0) {
						appendHTML(tool_i, String.format(options.skin.prev, options.text.prev));
					}
				}
			}
		}

		// build the counter
		if (current_gallery.length > 1 && options.displayCounter) {
			// append the counter div
			var counter = '';
			if (options.counterType == 'skip') {
				for ( var i = 0, len = current_gallery.length; i < len; ++i) {
					counter += '<a href="javascript:Shadowbox.change(' + i + ');"';
					if (i == current) {
						counter += ' class="shadowbox_counter_current"';
					}
					counter += '>' + (i + 1) + '</a>';
				}
			} else {
				counter = (current + 1) + ' of ' + current_gallery.length;
			}
			appendHTML(tool_i, String.format(options.skin.counter, counter));
		}
	};

	/**
	 * Hides the title and tool bars.
	 * 
	 * @param {Function}
	 *            callback A function to call on finish
	 * @return void
	 * @private
	 */
	var hideBars = function(callback) {
		var title_m = getComputedHeight(SL.get('shadowbox_title'));
		var tool_m = 0 - getComputedHeight(SL.get('shadowbox_toolbar'));
		var title_i = SL.get('shadowbox_title_inner');
		var tool_i = SL.get('shadowbox_toolbar_inner');

		if (options.animate && callback) {
			// animate the transition
			SL.animate(title_i, {
				marginTop : {
					to :title_m
				}
			}, 0.2);
			SL.animate(tool_i, {
				marginTop : {
					to :tool_m
				}
			}, 0.2, callback);
		} else {
			SL.setStyle(title_i, 'marginTop', title_m + 'px');
			SL.setStyle(tool_i, 'marginTop', tool_m + 'px');
		}
	};

	/**
	 * Shows the title and tool bars.
	 * 
	 * @param {Function}
	 *            callback A callback function to execute after the animation
	 *            completes
	 * @return void
	 * @private
	 */
	var showBars = function(callback) {
		var title_i = SL.get('shadowbox_title_inner');
		if (options.animate) {
			if (title_i.innerHTML != '') {
				SL.animate(title_i, {
					marginTop : {
						to :0
					}
				}, 0.35);
			}
			SL.animate(SL.get('shadowbox_toolbar_inner'), {
				marginTop : {
					to :0
				}
			}, 0.35, callback);
		} else {
			if (title_i.innerHTML != '') {
				SL.setStyle(title_i, 'margin-top', '0px');
			}
			SL.setStyle(SL.get('shadowbox_toolbar_inner'), 'margin-top', '0px');
			callback();
		}
	};

	/**
	 * Resets the class drag variable.
	 * 
	 * @return void
	 * @private
	 */
	var resetDrag = function() {
		drag = {
			x :0,
			y :0,
			start_x :null,
			start_y :null
		};
	};

	/**
	 * Toggles the drag function on and off.
	 * 
	 * @param {Boolean}
	 *            on True to toggle on, false to toggle off
	 * @return void
	 * @private
	 */
	var toggleDrag = function(on) {
		if (on) {
			resetDrag();
			// add drag layer to prevent browser dragging of actual image
			var styles = [ 'position:absolute',
					'cursor:' + (isGecko ? '-moz-grab' : 'move') ];
			// make drag layer transparent
			styles.push(isIE ? 'background-color:#fff;filter:alpha(opacity=0)'
					: 'background-color:transparent');
			appendHTML(
					'shadowbox_body_inner',
					'<div id="shadowbox_drag_layer" style="' + styles.join(';') + '"></div>');
			SL
					.addEvent(SL.get('shadowbox_drag_layer'), 'mousedown',
							listenDrag);
		} else {
			var d = SL.get('shadowbox_drag_layer');
			if (d) {
				SL.removeEvent(d, 'mousedown', listenDrag);
				SL.remove(d);
			}
		}
	};

	/**
	 * Sets up a drag listener on the document. Called when the mouse button is
	 * pressed (mousedown).
	 * 
	 * @param {mixed}
	 *            ev The mousedown event
	 * @return void
	 * @private
	 */
	var listenDrag = function(ev) {
		drag.start_x = ev.clientX;
		drag.start_y = ev.clientY;
		draggable = SL.get('shadowbox_content');
		SL.addEvent(document, 'mousemove', positionDrag);
		SL.addEvent(document, 'mouseup', unlistenDrag);
		if (isGecko)
			SL.setStyle(SL.get('shadowbox_drag_layer'), 'cursor',
					'-moz-grabbing');
	};

	/**
	 * Removes the drag listener. Called when the mouse button is released
	 * (mouseup).
	 * 
	 * @return void
	 * @private
	 */
	var unlistenDrag = function() {
		SL.removeEvent(document, 'mousemove', positionDrag);
		SL.removeEvent(document, 'mouseup', unlistenDrag); // clean up
		if (isGecko)
			SL.setStyle(SL.get('shadowbox_drag_layer'), 'cursor', '-moz-grab');
	};

	/**
	 * Positions an oversized image on drag.
	 * 
	 * @param {mixed}
	 *            ev The drag event
	 * @return void
	 * @private
	 */
	var positionDrag = function(ev) {
		var move_y = ev.clientY - drag.start_y;
		drag.start_y = drag.start_y + move_y;
		drag.y = Math.max(Math.min(0, drag.y + move_y), current_height
				- optimal_height); // y boundaries
		SL.setStyle(draggable, 'top', drag.y + 'px');
		var move_x = ev.clientX - drag.start_x;
		drag.start_x = drag.start_x + move_x;
		drag.x = Math.max(Math.min(0, drag.x + move_x), current_width
				- optimal_width); // x boundaries
		SL.setStyle(draggable, 'left', drag.x + 'px');
	};

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

		buildBars();

		switch (obj.type) {
		case 'img':
			// preload the image
			preloader = new Image();
			preloader.onload = function() {
				// images default to image height and width
				var h = obj.height ? parseInt(obj.height, 10)
						: preloader.height;
				var w = obj.width ? parseInt(obj.width, 10) : preloader.width;
				resizeContent(h, w,
						function(dims) {
							showBars( function() {
								setContent( {
									tag :'img',
									height :dims.i_height,
									width :dims.i_width,
									src :obj.content,
									style :'position:absolute'
								});
								if (dims.enableDrag
										&& options.handleLgImages == 'drag') {
									// listen for drag
						toggleDrag(true);
						SL.setStyle(SL.get('shadowbox_drag_layer'), {
							height :dims.i_height + 'px',
							width :dims.i_width + 'px'
						});
					}
					finishContent();
				});
			}	);

				preloader.onload = function() {
				}; // clear onload for IE
			};
			preloader.src = obj.content;
			break;

		case 'swf':
		case 'flv':
		case 'qt':
		case 'wmp':
			var markup = Shadowbox.movieMarkup(obj);
			resizeContent(markup.height, markup.width, function() {
				showBars( function() {
					setContent(markup);
					finishContent();
				});
			});
			break;

		case 'iframe':
			// iframes default to full viewport height and width
			var h = obj.height ? parseInt(obj.height, 10) : SL
					.getViewportHeight();
			var w = obj.width ? parseInt(obj.width, 10) : SL.getViewportWidth();
			var content = {
				tag :'iframe',
				name :'shadowbox_content',
				height :'100%',
				width :'100%',
				frameborder :'0',
				marginwidth :'0',
				marginheight :'0',
				scrolling :'auto'
			};

			resizeContent(
					h,
					w,
					function(dims) {
						showBars( function() {
							setContent(content);
							var win = (isIE) ? SL.get('shadowbox_content').contentWindow
									: window.frames['shadowbox_content'];
							win.location = obj.content;
							finishContent();
						});
					});
			break;

		case 'html':
		case 'inline':
			// HTML content defaults to full viewport height and width
			var h = obj.height ? parseInt(obj.height, 10) : SL
					.getViewportHeight();
			var w = obj.width ? parseInt(obj.width, 10) : SL.getViewportWidth();
			var content = {
				tag :'div',
				cls :'html', /* give special class to make scrollable */
				html :obj.content
			};
			resizeContent(h, w, function() {
				showBars( function() {
					setContent(content);
					finishContent();
				});
			});
			break;

		default:
			// should never happen
			throw 'Shadowbox cannot open content of type ' + obj.type;
		}

		// preload neighboring images
		if (current_gallery.length > 0) {
			var next = current_gallery[current + 1];
			if (!next) {
				next = current_gallery[0];
			}
			if (next.type == 'img') {
				var preload_next = new Image();
				preload_next.src = next.href;
			}

			var prev = current_gallery[current - 1];
			if (!prev) {
				prev = current_gallery[current_gallery.length - 1];
			}
			if (prev.type == 'img') {
				var preload_prev = new Image();
				preload_prev.src = prev.href;
			}
		}
	};

	/**
	 * Removes old content and sets the new content of the Shadowbox.
	 * 
	 * @param {Object}
	 *            obj The content to set (appropriate to pass directly to
	 *            Shadowbox.createHTML())
	 * @return {HTMLElement} The newly appended element (or null if none is
	 *         provided)
	 * @private
	 */
	var setContent = function(obj) {
		var id = 'shadowbox_content';
		var content = SL.get(id);
		if (content) {
			// remove old content first
			switch (content.tagName.toUpperCase()) {
			case 'OBJECT':
				// if we're in a gallery (i.e. changing and there's a new
				// object) we want the LAST link object
				var link = current_gallery[(obj ? current - 1 : current)];
				if (link.type == 'wmp' && isIE) {
					try {
						shadowbox_content.controls.stop(); // stop the movie
						shadowbox_content.URL = 'non-existent.wmv'; // force
																	// player
																	// refresh
						window.shadowbox_content = function() {
						}; // remove from window
					} catch (e) {
					}
				} else if (link.type == 'qt' && isSafari) {
					try {
						document.shadowbox_content.Stop(); // stop QT movie
					} catch (e) {
					}
					// stop QT audio stream for movies that have not yet loaded
					content.innerHTML = '';
					// console.log(document.shadowbox_content);
				}
				setTimeout( function() { // using setTimeout prevents browser
											// crashes with WMP
							SL.remove(content);
						}, 10);
				break;
			case 'IFRAME':
				SL.remove(content);
				if (isGecko)
					delete window.frames[id]; // needed for Firefox
				break;
			default:
				SL.remove(content);
			}
		}
		if (obj) {
			if (!obj.id)
				obj.id = id;
			return appendHTML('shadowbox_body_inner', Shadowbox.createHTML(obj));
		}
		return null;
	};

	/**
	 * This function is used as the callback after the Shadowbox has been
	 * positioned, resized, and loaded with content.
	 * 
	 * @return void
	 * @private
	 */
	var finishContent = function() {
		var obj = current_gallery[current];
		if (!obj)
			return; // invalid
		hideLoading( function() {
			listenKeyboard(true);
			// fire onFinish handler
			if (options.onFinish && typeof options.onFinish == 'function') {
				options.onFinish(obj);
			}
		});
	};

	/**
	 * Resizes and positions the content box using the given height and width.
	 * If the callback parameter is missing, the transition will not be
	 * animated. If the callback parameter is present, it will be passed the new
	 * calculated dimensions object as its first parameter. Note: the height and
	 * width here should represent the optimal height and width of the box.
	 * 
	 * @param {Function}
	 *            callback A callback function to use when the resize completes
	 * @return void
	 * @private
	 */
	var resizeContent = function(height, width, callback) {
		// update optimal height and width
		optimal_height = height;
		optimal_width = width;
		var resizable = RE.resize.test(current_gallery[current].type);
		var dims = getDimensions(optimal_height, optimal_width, resizable);
		if (callback) {
			var cb = function() {
				callback(dims);
			};
			switch (options.animSequence) {
			case 'hw':
				adjustHeight(dims.height, dims.top, true, function() {
					adjustWidth(dims.width, true, cb);
				});
				break;
			case 'wh':
				adjustWidth(dims.width, true, function() {
					adjustHeight(dims.height, dims.top, true, cb);
				});
				break;
			default: // sync
				adjustWidth(dims.width, true);
				adjustHeight(dims.height, dims.top, true, cb);
			}
		} else { // window resize
			adjustWidth(dims.width, false);
			adjustHeight(dims.height, dims.top, false);
			// resize content images & flash in 'resize' mode
			if (options.handleLgImages == 'resize' && resizable) {
				var content = SL.get('shadowbox_content');
				if (content) { // may be animating, not present
					content.height = dims.i_height;
					content.width = dims.i_width;
				}
			}
		}
	};

	/**
	 * Calculates the dimensions for Shadowbox, taking into account the borders,
	 * margins, and surrounding elements of the shadowbox_body. If the image is
	 * still to large for Shadowbox, and options.handleLgImages is 'resize', the
	 * resized dimensions will be returned (preserving the original aspect
	 * ratio). Otherwise, the originally calculated dimensions will be returned.
	 * The returned object will have the following properties:
	 *  - height: The height to use for shadowbox_body_inner - width: The width
	 * to use for shadowbox - i_height: The height to use for resizable content -
	 * i_width: The width to use for resizable content - top: The top to use for
	 * shadowbox - enableDrag: True if dragging should be enabled (image is
	 * oversized)
	 * 
	 * @param {Number}
	 *            o_height The optimal height
	 * @param {Number}
	 *            o_width The optimal width
	 * @param {Boolean}
	 *            resizable True if the content is able to be resized. Defaults
	 *            to false.
	 * @return {Object} The resize dimensions (see above)
	 * @private
	 */
	var getDimensions = function(o_height, o_width, resizable) {
		if (typeof resizable == 'undefined')
			resizable = false;

		var height = o_height = parseInt(o_height);
		var width = o_width = parseInt(o_width);
		var shadowbox_b = SL.get('shadowbox_body');

		// calculate the max height
		var view_height = SL.getViewportHeight();
		var extra_height = parseInt(SL
				.getStyle(shadowbox_b, 'border-top-width'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'border-bottom-width'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'margin-top'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'margin-bottom'), 10)
				+ getComputedHeight(SL.get('shadowbox_title'))
				+ getComputedHeight(SL.get('shadowbox_toolbar'))
				+ (2 * options.viewportPadding);
		if ((height + extra_height) >= view_height) {
			height = view_height - extra_height;
		}

		// calculate the max width
		var view_width = SL.getViewportWidth();
		var extra_body_width = parseInt(SL.getStyle(shadowbox_b,
				'border-left-width'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'border-right-width'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'margin-left'), 10)
				+ parseInt(SL.getStyle(shadowbox_b, 'margin-right'), 10);
		var extra_width = extra_body_width + (2 * options.viewportPadding);
		if ((width + extra_width) >= view_width) {
			width = view_width - extra_width;
		}

		// handle oversized images & flash
		var enableDrag = false;
		var i_height = o_height;
		var i_width = o_width;
		var handle = options.handleLgImages;
		if (resizable && (handle == 'resize' || handle == 'drag')) {
			var change_h = (o_height - height) / o_height;
			var change_w = (o_width - width) / o_width;
			if (handle == 'resize') {
				if (change_h > change_w) {
					width = Math.round((o_width / o_height) * height);
				} else if (change_w > change_h) {
					height = Math.round((o_height / o_width) * width);
				}
				// adjust image height or width accordingly
				i_width = width;
				i_height = height;
			} else {
				// drag on oversized images only
				var link = current_gallery[current];
				if (link)
					enableDrag = link.type == 'img'
							&& (change_h > 0 || change_w > 0);
			}
		}

		return {
			height :height,
			width :width + extra_body_width,
			i_height :i_height,
			i_width :i_width,
			top :((view_height - (height + extra_height)) / 2)
					+ options.viewportPadding,
			enableDrag :enableDrag
		};
	};

	/**
	 * Centers Shadowbox vertically in the viewport. Needs to be called on
	 * scroll in IE6 because it does not support fixed positioning.
	 * 
	 * @return void
	 * @private
	 */
	var centerVertically = function() {
		var shadowbox = SL.get('shadowbox');
		var scroll = document.documentElement.scrollTop;
		var s_top = scroll
				+ Math
						.round((SL.getViewportHeight() - (shadowbox.offsetHeight || 0)) / 2);
		SL.setStyle(shadowbox, 'top', s_top + 'px');
	};

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

		// update current_height
		current_height = height;

		// adjust the height
		var sbi = SL.get('shadowbox_body_inner');
		if (animate && options.animate) {
			SL.animate(sbi, {
				height : {
					to :height
				}
			}, options.resizeDuration, callback);
		} else {
			SL.setStyle(sbi, 'height', height + 'px');
			if (typeof callback == 'function')
				callback();
		}

		// manually adjust the top because we're using fixed positioning in IE6
		if (absolute_pos) {
			// listen for scroll so we can adjust
			centerVertically();
			SL.addEvent(window, 'scroll', centerVertically);

			// add scroll to top
			top += document.documentElement.scrollTop;
		}

		// adjust the top
		var shadowbox = SL.get('shadowbox');
		if (animate && options.animate) {
			SL.animate(shadowbox, {
				top : {
					to :top
				}
			}, options.resizeDuration);
		} else {
			SL.setStyle(shadowbox, 'top', top + 'px');
		}
	};

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

		// update current_width
		current_width = width;

		var shadowbox = SL.get('shadowbox');
		if (animate && options.animate) {
			SL.animate(shadowbox, {
				width : {
					to :width
				}
			}, options.resizeDuration, callback);
		} else {
			SL.setStyle(shadowbox, 'width', width + 'px');
			if (typeof callback == 'function')
				callback();
		}
	};

	/**
	 * Sets up a listener on the document for keystrokes.
	 * 
	 * @param {Boolean}
	 *            on True to enable the listner, false to turn it off
	 * @return void
	 * @private
	 */
	var listenKeyboard = function(on) {
		if (!options.enableKeys)
			return;
		if (on) {
			document.onkeydown = handleKey;
		} else {
			document.onkeydown = '';
		}
	};

	/**
	 * Asserts the given key or code is present in the array of valid keys.
	 * 
	 * @param {Array}
	 *            valid An array of valid keys and codes
	 * @param {String}
	 *            key The character that was pressed
	 * @param {Number}
	 *            code The key code that was pressed
	 * @return {Boolean} True if the key is valid
	 * @private
	 */
	var assertKey = function(valid, key, code) {
		return (valid.indexOf(key) != -1 || valid.indexOf(code) != -1);
	};

	/**
	 * A listener function that will act on a key pressed.
	 * 
	 * @param {Event}
	 *            e The event object
	 * @return void
	 * @private
	 */
	var handleKey = function(e) {
		var code = e ? e.which : event.keyCode;
		var key = String.fromCharCode(code).toLowerCase();
		if (assertKey(options.keysClose, key, code)) {
			Shadowbox.close();
		} else if (assertKey(options.keysPrev, key, code)) {
			Shadowbox.previous();
		} else if (assertKey(options.keysNext, key, code)) {
			Shadowbox.next();
		} else if (assertKey(options.keysPrint, key, code)) {
			Shadowbox.print();
		}
	};

	/**
	 * Shows and hides elements that are troublesome for modal overlays.
	 * 
	 * @param {Boolean}
	 *            on True to show the elements, false otherwise
	 * @return void
	 * @private
	 */
	var toggleTroubleElements = function(on) {
		var vis = (on ? 'visible' : 'hidden');
		var selects = document.getElementsByTagName('select');
		for (i = 0, len = selects.length; i < len; ++i) {
			selects[i].style.visibility = vis;
		}
		var objects = document.getElementsByTagName('object');
		for (i = 0, len = objects.length; i < len; ++i) {
			objects[i].style.visibility = vis;
		}
		var embeds = document.getElementsByTagName('embed');
		for (i = 0, len = embeds.length; i < len; ++i) {
			embeds[i].style.visibility = vis;
		}
	};

	/**
	 * Fills the Shadowbox with the loading skin.
	 * 
	 * @return void
	 * @private
	 */
	var showLoading = function() {
		var loading = SL.get('shadowbox_loading');
		overwriteHTML(loading, String.format(options.skin.loading,
				options.assetURL + options.loadingImage, options.text.loading,
				options.text.cancel));
		loading.style.visibility = 'visible';
	};

	/**
	 * Hides the Shadowbox loading skin.
	 * 
	 * @param {Function}
	 *            callback The callback function to call after hiding the
	 *            loading skin
	 * @return void
	 * @private
	 */
	var hideLoading = function(callback) {
		var t = current_gallery[current].type;
		var anim = (t == 'img' || t == 'html'); // fade on images & html
		var loading = SL.get('shadowbox_loading');
		if (anim) {
			fadeOut(loading, 0.35, callback);
		} else {
			loading.style.visibility = 'hidden';
			callback();
		}
	};

	/**
	 * Sets the size of the overlay to the size of the document.
	 * 
	 * @return void
	 * @private
	 */
	var resizeOverlay = function() {
		var overlay = SL.get('shadowbox_overlay');
		SL.setStyle(overlay, {
			height :'100%',
			width :'100%'
		});
		SL.setStyle(overlay, 'height', SL.getDocumentHeight() + 'px');
		if (!isSafari3) {
			// Safari3 includes vertical scrollbar in SL.getDocumentWidth()!
			// Leave overlay width at 100% for now...
			SL.setStyle(overlay, 'width', SL.getDocumentWidth() + 'px');
		}
	};

	/**
	 * Used to determine if the pre-made overlay background image is needed
	 * instead of using the trasparent background overlay. A pre-made background
	 * image is used for all but image pieces in FF Mac because it has problems
	 * displaying correctly if the background layer is not 100% opaque. When
	 * displaying a gallery, if any piece in the gallery meets these criteria,
	 * the pre-made background image will be used.
	 * 
	 * @return {Boolean} Whether or not an overlay image is needed
	 * @private
	 */
	var checkOverlayImgNeeded = function() {
		if (!(isGecko && isMac))
			return false;
		for ( var i = 0, len = current_gallery.length; i < len; ++i) {
			if (!RE.overlay.exec(current_gallery[i].type))
				return true;
		}
		return false;
	};

	/**
	 * Activates (or deactivates) the Shadowbox overlay. If a callback function
	 * is provided, we know we're activating. Otherwise, deactivate the overlay.
	 * 
	 * @param {Function}
	 *            callback A callback to call after activation
	 * @return void
	 * @private
	 */
	var toggleOverlay = function(callback) {
		var overlay = SL.get('shadowbox_overlay');
		if (overlay_img_needed == null) {
			overlay_img_needed = checkOverlayImgNeeded();
		}

		if (callback) {
			resizeOverlay(); // size the overlay before showing
			if (overlay_img_needed) {
				SL.setStyle(overlay, {
					visibility :'visible',
					backgroundColor :'transparent',
					backgroundImage :'url(' + options.assetURL
							+ options.overlayBgImage + ')',
					backgroundRepeat :'repeat',
					opacity :1
				});
				callback();
			} else {
				SL.setStyle(overlay, {
					visibility :'visible',
					backgroundColor :options.overlayColor,
					backgroundImage :'none'
				});
				fadeIn(overlay, options.overlayOpacity, options.fadeDuration,
						callback);
			}
		} else {
			if (overlay_img_needed) {
				SL.setStyle(overlay, 'visibility', 'hidden');
			} else {
				fadeOut(overlay, options.fadeDuration);
			}

			// reset for next time
			overlay_img_needed = null;
		}
	};

	/**
	 * Initializes the Shadowbox environment. Appends Shadowbox' HTML to the
	 * document and sets up listeners on the window and overlay element.
	 * 
	 * @param {Object}
	 *            opts The default options to use
	 * @return void
	 * @public
	 * @static
	 */
	Shadowbox.init = function(opts) {
		if (initialized)
			return; // don't initialize twice
		options = apply(options, opts || {});

		// add markup
		appendHTML(document.body, options.skin.main);

		// compile file type regular expressions here for speed
		RE.img = new RegExp('\.(' + options.ext.img.join('|') + ')\s*$', 'i');
		RE.qt = new RegExp('\.(' + options.ext.qt.join('|') + ')\s*$', 'i');
		RE.wmp = new RegExp('\.(' + options.ext.wmp.join('|') + ')\s*$', 'i');
		RE.qtwmp = new RegExp('\.(' + options.ext.qtwmp.join('|') + ')\s*$', 'i');
		RE.iframe = new RegExp('\.(' + options.ext.iframe.join('|') + ')\s*$', 'i');
		
		/*alert("img: " + RE.img);
		alert("qt: " + RE.qt);
		alert("wmp: " + RE.wmp);
		alert("qtwmp: " + RE.qtwmp);
		alert("iframe: " + RE.iframe);*/

		// handle window resize events
		var id = null;
		var resize = function() {
			clearInterval(id);
			id = null;
			resizeOverlay();
			resizeContent(optimal_height, optimal_width);
		};
		SL.addEvent(window, 'resize', function() {
			if (activated) {
				// use event buffering to prevent jerky window resizing
				if (id) {
					clearInterval(id);
					id = null;
				}
				if (!id)
					id = setInterval(resize, 50);
			}
		});

		if (options.listenOverlay) {
			// add a listener to the overlay
			SL.addEvent(SL.get('shadowbox_overlay'), 'click', Shadowbox.close);
		}

		// adjust some positioning if needed
		if (absolute_pos) {
			// give the container absolute positioning
			SL.setStyle(SL.get('shadowbox_container'), 'position', 'absolute');
			// give shadowbox_body "layout"...whatever that is
			SL.setStyle('shadowbox_body', 'zoom', 1);
			// need to listen to the container element because it covers the top
			// half of the page
			SL.addEvent(SL.get('shadowbox_container'), 'click', function(e) {
				var target = SL.getTarget(e);
				if (target.id && target.id == 'shadowbox_container')
					Shadowbox.close();
			});
		}

		// skip setup, will need to be done manually later
		if (!options.skipSetup)
			Shadowbox.setup();
		initialized = true;
	};

	/**
	 * 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 - type: the linked
	 * file type - 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'),
			type :getPlayerType(href),
			options :apply( {}, opts || {}), // break the reference
			content :href
		};

		// remove link-level options from top-level options
		var opt, l_opts = [ 'title', 'type', '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) {
			//alert("rel:" + rel);
			// extract gallery name from shadowbox[name] format
			var match = rel.match(RE.gallery);
			//alert("match:" + match);
			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('o.options = 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) {
		if (activated)
			return; // already open
		activated = true;

		// 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 an object on-the-fly
				obj = this.buildCacheObj(obj, opts);
			} else {
				// link element that has been set up before, get from cache
				obj = cache[obj.shadowboxCacheKey];
			}
		}

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

		// update current & current_gallery
		setupGallery(obj);

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

			// display:block here helps with correct dimension calculations
			SL.setStyle(SL.get('shadowbox'), 'display', 'block');

			toggleTroubleElements(false);
			var dims = getDimensions(options.initialHeight,
					options.initialWidth);
			adjustHeight(dims.height, dims.top);
			adjustWidth(dims.width);
			hideBars(false);

			// show the overlay and load the content
			toggleOverlay( function() {
				SL.setStyle(SL.get('shadowbox'), 'visibility', 'visible');
				showLoading();
				loadContent();
			});
		}
	};

	/**
	 * 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 (!current_gallery)
			return; // no current gallery
		if (!current_gallery[num]) { // index does not exist
			if (!options.continuous) {
				return;
			} else {
				num = (num < 0) ? (current_gallery.length - 1) : 0; // loop
			}
		}

		// update current
		current = num;

		// stop listening for drag
		toggleDrag(false);
		// empty the content
		setContent(null);
		// turn this back on when done
		listenKeyboard(false);

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

		showLoading();
		hideBars(loadContent);
	};
	
	/**
	 * Print a piece current photo of the gallery.
	 * 
	 * @public
	 * @static
	 */
	Shadowbox.print = function() {
		print();
	};

	/**
	 * Jumps to the next piece in the gallery.
	 * 
	 * @return {Boolean} True if the gallery changed to next item, false
	 *         otherwise
	 * @public
	 * @static
	 */
	Shadowbox.next = function() {
		return this.change(current + 1);
	};

	/**
	 * Jumps to the previous piece in the gallery.
	 * 
	 * @return {Boolean} True if the gallery changed to previous item, false
	 *         otherwise
	 * @public
	 * @static
	 */
	Shadowbox.previous = function() {
		return this.change(current - 1);
	};

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

		// stop listening for keys
		listenKeyboard(false);
		// hide
		SL.setStyle(SL.get('shadowbox'), {
			display :'none',
			visibility :'hidden'
		});
		// stop listening for scroll on IE
		if (absolute_pos)
			SL.removeEvent(window, 'scroll', centerVertically);
		// stop listening for drag
		toggleDrag(false);
		// empty the content
		setContent(null);
		// prevent old image requests from loading
		if (preloader) {
			preloader.onload = function() {
			};
			preloader = null;
		}
		// hide the overlay
		toggleOverlay(false);
		// turn on trouble elements
		toggleTroubleElements(true);

		// fire onClose handler
		if (options.onClose && typeof options.onClose == 'function') {
			options.onClose(current_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].shadowboxCacheKey;
			}
		}
		cache = [];
	};

	/**
	 * Generates the markup necessary to embed the movie file with the given
	 * link element. This markup will be browser-specific. Useful for generating
	 * the media test suite.
	 * 
	 * @param {HTMLElement}
	 *            link The link to the media file
	 * @return {Object} The proper markup to use (see above)
	 * @public
	 * @static
	 */
	Shadowbox.movieMarkup = function(obj) {
		// movies default to 300x300 pixels
		var h = obj.height ? parseInt(obj.height, 10) : 300;
		var w = obj.width ? parseInt(obj.width, 10) : 300;

		var autoplay = options.autoplayMovies;
		var controls = options.showMovieControls;
		if (obj.options) {
			if (obj.options.autoplayMovies != null) {
				autoplay = obj.options.autoplayMovies;
			}
			if (obj.options.showMovieControls != null) {
				controls = obj.options.showMovieControls;
			}
		}

		var markup = {
			tag :'object',
			name :'shadowbox_content'
		};

		switch (obj.type) {
		case 'swf':
			var dims = getDimensions(h, w, true);
			h = dims.height;
			w = dims.width;
			markup.type = 'application/x-shockwave-flash';
			markup.data = obj.content;
			markup.children = [ {
				tag :'param',
				name :'movie',
				value :obj.content
			} ];
			break;
		case 'flv':
			autoplay = autoplay ? 'true' : 'false';
			var showicons = 'false';
			var a = h / w; // aspect ratio
			if (controls) {
				showicons = 'true';
				h += 20; // height of JW FLV player controller
			}
			var dims = getDimensions(h, h / a, true); // resize
			h = dims.height;
			w = (h - (controls ? 20 : 0)) / a; // maintain aspect ratio
			var flashvars = [ 'file=' + obj.content, 'height=' + h,
					'width=' + w, 'autostart=' + autoplay,
					'displayheight=' + (h - (controls ? 20 : 0)),
					'showicons=' + showicons,
					'backcolor=0x000000&amp;frontcolor=0xCCCCCC&amp;lightcolor=0x557722' ];
			markup.type = 'application/x-shockwave-flash';
			markup.data = options.assetURL + options.flvPlayer;
			markup.children = [ {
				tag :'param',
				name :'movie',
				value :options.assetURL + options.flvPlayer
			}, {
				tag :'param',
				name :'flashvars',
				value :flashvars.join('&amp;')
			}, {
				tag :'param',
				name :'allowfullscreen',
				value :'true'
			} ];
			break;
		case 'qt':
			autoplay = autoplay ? 'true' : 'false';
			if (controls) {
				controls = 'true';
				h += 16; // height of QuickTime controller
			} else {
				controls = 'false';
			}
			markup.children = [ {
				tag :'param',
				name :'src',
				value :obj.content
			}, {
				tag :'param',
				name :'scale',
				value :'aspect'
			}, {
				tag :'param',
				name :'controller',
				value :controls
			}, {
				tag :'param',
				name :'autoplay',
				value :autoplay
			} ];
			if (isIE) {
				markup.classid = 'clsid:02BF25D5-8C17-4B23-BC80-D3488ABDDC6B';
				markup.codebase = 'http://www.apple.com/qtactivex/qtplugin.cab#version=6,0,2,0';
			} else {
				markup.type = 'video/quicktime';
				markup.data = obj.content;
			}
			break;
		case 'wmp':
			autoplay = autoplay ? 1 : 0;
			markup.children = [ {
				tag :'param',
				name :'autostart',
				value :autoplay
			} ];
			if (isIE) {
				if (controls) {
					controls = 'full';
					h += 70; // height of WMP controller in IE
				} else {
					controls = 'none';
				}
				// markup.type = 'application/x-oleobject';
				markup.classid = 'clsid:6BF52A52-394A-11d3-B153-00C04F79FAA6';
				markup.children[markup.children.length] = {
					tag :'param',
					name :'url',
					value :obj.content
				};
				markup.children[markup.children.length] = {
					tag :'param',
					name :'uimode',
					value :controls
				};
			} else {
				if (controls) {
					controls = 1;
					h += 45; // height of WMP controller in non-IE
				} else {
					controls = 0;
				}
				markup.type = 'video/x-ms-wmv';
				markup.data = obj.content;
				markup.children[markup.children.length] = {
					tag :'param',
					name :'showcontrols',
					value :controls
				};
			}
			break;
		}

		markup.height = h; // new height includes controller
		markup.width = w;

		return markup;
	};

	/**
	 * 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
	 */
	Shadowbox.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 += '/>\n';
		} else {
			html += '>\n';
			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 + '>\n';
		}
		return html;
	};

	/**
	 * 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 current_gallery[current];
	};

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

})();

/**
 * Finds the index of the given object in this array.
 * 
 * @param {mixed}
 *            o The object to search for
 * @return {Number} The index of the given object
 * @public
 */
Array.prototype.indexOf = Array.prototype.indexOf || function(o) {
	for ( var i = 0, len = this.length; i < len; ++i) {
		if (this[i] == o)
			return i;
	}
	return -1;
};

/**
 * Formats a string with the given parameters. The string for format must have
 * placeholders that correspond to the numerical index of the arguments passed
 * in surrounded by curly braces (e.g. 'Some {0} string {1}').
 * 
 * @param {String}
 *            format The string to format
 * @param ...
 *            The parameters to put inside the string
 * @return {String} The string with the specified parameters replaced
 * @public
 * @static
 */
String.format = String.format || function(format) {
	var args = Array.prototype.slice.call(arguments, 1);
	return format.replace(/\{(\d+)\}/g, function(m, i) {
		return args[i];
	});
};
