/// <reference path="~/Abl_Scripts/Source/jquery.js" />

// JSLint
// var $, jQuery, Image, window, document, navigator, escape, alert, console, XMLSerializer;


/*
** Abilitation JavaScript Library
** 
** @author Neil Martin
** @version 1.0
*/ 


// Define standard JavaScript library namespaces.
// Note, additional namespaces may be attached the the root Abl object
var Abl = {};
Abl.Browser = {};
Abl.Cms = {};
Abl.Cms.Editors = {};
Abl.Cookie = {};
Abl.DateTime = {};
Abl.DEBUG = {};
Abl.IO = {};
Abl.Json = {};
Abl.Math = {};
Abl.String = {};
Abl.UI = {};
Abl.WebControls = {};
Abl.Window = {};
Abl.Xml = {};

Abl.version = "1.0.0";

Abl.DEBUG.debug = 1;            // 1==Show in FireFox; 2==Show in IE
Abl.DEBUG.useAlert = false;   // Always use alert()
Abl.DEBUG.trace = function(msg) {
   if (Abl.DEBUG.debug) {
      if ((typeof console === "object") && (typeof console.debug === "function")&&(!Abl.DEBUG.useAlert)) {
         console.debug(msg);
      }
      else if ((Abl.DEBUG.debug > 1)||(Abl.DEBUG.useAlert)) {
         alert(msg);
      }
   }
};
Abl.DEBUG.keyEvent = function(evt) {
   var   key = evt.which,
         msg = "key:" + key + " ";
   if (evt.shiftKey) { msg += "+shift"; }
   if (evt.altKey)   { msg += "+alt"; }
   if (evt.ctrlKey)  { msg += "+ctrl"; }
   Abl.DEBUG.trace(msg);
};


/**
 * Browser Utilities
 */
Abl.Browser.isIE6 = function() {
   // Thanks to jQuery BlockUI http://malsup.com/jquery/block 
   var   mode = document.documentMode || 0,
         ie6 = (($.browser.msie)  && (/MSIE 6\.0/.test(navigator.userAgent)) && (!mode));
   return ie6;
};



/**
 * XML Utilities
 */
Abl.Xml.encode = function(s) {
   // Note that replace("&", "&amp;") has to be the first replace so we don't replace other already escaped &.
   return ((s)&&(typeof s === 'string')) ? s.replace(/\&/g,'&'+'amp;').replace(/</g,'&'+'lt;')
      .replace(/>/g,'&'+'gt;').replace(/\'/g,'&'+'apos;').replace(/\"/g,'&'+'quot;') : "";
};

Abl.Xml.decode = function(s) {
   return ((s)&&(typeof s === 'string')) ? s.replace(/\&lt;/g,'<').replace(/\&gt;/g,'>')
      .replace(/\&apos;/g,'\'').replace(/\&quot;/g,'\"').replace(/\&amp;/g,'&') : "";
};

Abl.Xml.serialize = function (xData) {
   return (xData.xml) ? xData.xml : (new XMLSerializer()).serializeToString(xData);
};



/**
 * Abl.Uri
 * This class supersedes Abl.Window.Uri (the old class will eventually be replaced).
 *
 * The Abl.Uri() class constructor accepts any of the following types and attempts
 * to extract the url/href data property from the object.  This raw string data is
 * then parsed with a regular expression to extract the relevant uri properties.
 *
 * Note, all paths must be specified as absolute, i.e. '/Default.aspx' - relative
 * paths are not supported and will cause parsing errors!
 */
Abl.Uri = function(obj) {
   var   _self = this,
         url, localUrl, host, protocol, domain, port, path, folder, file, fileLessExtension, fragment, query,
         regexUrl = /^(((https?|ftp):\/\/)([^\/:\?\#]*)?(:(\d*))?)?([^\?#]*)?((\?|#)(.*))?$/i,
         regexPath = /^((\/)?.*\/)?(.*)$/i;

   // Url groups (http://www.abldev.com:8080/Admin/Cms/Default.aspx?name=neil):   => url
   // group[1] = 'http://www.abldev.com:8080'                                    => host
   // group[3] = 'http'                                                            => protocol
   // group[4] = 'www.abldev.com'                                                => domain
   // group[6] = '8080'                                                            => port
   // group[7] = '/Admin/Cms/Default.aspx'                                       => path
   // group[9] = '?'                                                               => fragment
   // group[10] = 'name=neil'                                                      => query
   
   // Path groups (/Admin/Cms/Default.aspx):                                       => path
   // group[1] = '/Admin/Cms/'                                                   => folder
   // group[3] = 'Default.aspx'                                                   => file

   /**********************************************************************************************
   * Uri Fragments/Property Accessors                                                            *
   **********************************************************************************************/
   // Example:
   // var uri = new Abl.Uri("http://www.abldev.com:63886/Admin/Cms/Default.aspx?pageid=27&userid=14");
   
   this.getUrl         = function() { return url; };
   this.getLocalUrl   = function() { return localUrl; };
   this.getHost      = function() { return host; };
   this.getProtocol   = function() { return protocol; };
   this.getDomain      = function() { return domain; };
   this.getPort      = function() { return port; };
   this.getPath      = function() { return path; };
   this.getFolder      = function() { return folder; };
   this.getFile      = function() { return file; };
   this.getFragment   = function() { return fragment; };
   this.getQuery      = function() { return query; };
   this.getFileLessExtension = function() { return fileLessExtension; };
   
   this.getProperties = function() {
      return ({
         url: url,
         localUrl: localUrl,
         host: host,
         protocol: protocol,
         domain: domain,
         port: port,
         path: path,
         folder: folder,
         file: file,
         fileLessExtension: fileLessExtension,
         fragment: fragment,
         query: query
      });
   };
   
   this.getParams = function() {
     // define an object to contain the parsed query data
     var   result = {},
         queryString = query.replace('+', ' '),   // replace plus signs in the query string with spaces
         queryComponents, i, keyValuePair, key, value;

      // split the query string around ampersands and semicolons
      queryComponents = queryString.split(/[&;]/g);

      // loop over the query string components
      for (i = 0; i < queryComponents.length; i++){
         // extract this component's key-value pair
         keyValuePair = queryComponents[i].split('=');
         key = decodeURIComponent(keyValuePair[0]).toLowerCase();
         value = decodeURIComponent(keyValuePair[1]);
         result[key] = value;
      }

      // return the parsed query data
      return result;
   };
   
   this.getParam = function(name) {
      var params = _self.getParams();
      return (params) ? params[name.toLowerCase()] : null;
   };
   
   
   this.printProperties = function() {
      var s = "", p, props = this.getProperties();
      for (p in props) {
         if (props.hasOwnProperty(p)) {
            if (s.length) { s += "\r\n"; }
            s += p + ": " + ((props[p]) ? props[p] : "--");
         }
      }
      alert(s);
   };

   

   this.compare = function(target, element, partialMatch) {
      var   targetUri = (target instanceof Abl.Uri) ? target : new Abl.Uri(target);
      element = (element) ? element.toLowerCase() : "url";
      
      // Provide a more sophisticated comparison option for folders.  Partial
      // matching is useful when comparing links with the current page - see
      // 'folderMatch' class attribute in Abl.UI.Menu
      function compareFolder(folderA, folderB, partialMatch) {
         var   compareLength;
         
         if ((typeof folderA !== "string") || (typeof folderB !== "string")) {
            return false;
         }
         
         folderA = folderA.toLowerCase();
         folderB = folderB.toLowerCase();
         
         if (partialMatch) {
            compareLength = Math.min(folderA.length, folderB.length);
            return (folderA.substr(0, compareLength) === folderB.substr(0, compareLength));
         } else {
            return (folderA === folderB);
         }
      }
      
      switch (element) {
         case "url":         return (url.toLowerCase() === targetUri.getUrl().toLowerCase());
         case "localurl":   return (localUrl.toLowerCase() === targetUri.getLocalUrl().toLowerCase());
         case "host":      return (host.toLowerCase() === targetUri.getHost().toLowerCase());
         case "protocol":   return (protocol.toLowerCase() === targetUri.getProtocol().toLowerCase());
         case "domain":      return (domain.toLowerCase() === targetUri.getDomain().toLowerCase());
         case "port":      return (port.toLowerCase() === targetUri.getPort().toLowerCase());
         case "path":      return (path.toLowerCase() === targetUri.getPath().toLowerCase());
         case "folder":      return compareFolder(folder, targetUri.getFolder(), partialMatch);
         case "file":      return (file.toLowerCase() === targetUri.getFile().toLowerCase());
         case "fileLessExtension":   return (fileLessExtension.toLowerCase() === targetUri.getFileLessExtension().toLowerCase());
         case "fragment":   return (fragment.toLowerCase() === targetUri.getFragment().toLowerCase());
         case "query":      return (query.toLowerCase() === targetUri.getQuery().toLowerCase());
         default:
            throw "Illegal comparison element '" + element + "'!";
      }
   };
   

   /**********************************************************************************************
   * Address/Uri Management                                                                      *
   **********************************************************************************************/
   function getLocalUrl(url) {
      var   domain = window.location.protocol + '//' + window.location.hostname;
      
      if (!url) { return ""; }
      if (window.location.port) { domain += ":" + window.location.port; }
      
      if (url.toLowerCase().indexOf(domain.toLowerCase()) === 0) {
         return url.substr(domain.length);
      } else {
         return url;
      }
   }

   function clear() {
      localUrl      = "";
      host         = "";
      protocol      = "";
      domain      = "";
      port         = "";
      path         = "";
      folder      = "";
      file         = "";
      fileLessExtension = "";
      fragment      = "";
      query         = "";
   }

   function setUrl(s) {
      url = s || "";
      localUrl = getLocalUrl(url);
      
      var urlMatch = regexUrl.exec(url), pathMatch;
      if (urlMatch) {
         host         = urlMatch[1]  || "";   // 'http://www.abldev.com:8080'
         protocol      = urlMatch[3]  || "";   // 'http'
         domain      = urlMatch[4]  || "";   // 'www.abldev.com'
         port         = urlMatch[6]  || "";   // '8080'
         path         = urlMatch[7]  || "";   // '/Admin/Cms/Default.aspx'
         fragment      = urlMatch[9]  || "";   // '?'
         query         = urlMatch[10] || "";   // 'name=neil'
         
         pathMatch = regexPath.exec(path);
         folder      = pathMatch[1] || "";   // '/Admin/Cms/'
         file         = pathMatch[3] || "";   // 'Default.aspx'
         fileLessExtension = file.match(/^[^\.]*/)[0];
      } else {
         clear();
      }
   }

   this.setUri = function(obj) {
      if (typeof obj === 'string') {
         setUrl(obj);
      } else 
      if ((typeof obj === 'object') && (obj.href)) {
         setUrl(obj.href);
      } else
      if ((obj) && (obj instanceof Abl.Uri)) {
         setUrl(obj.getUrl());
      } else
      if ((obj) && (obj instanceof jQuery) && (obj[0]) && (obj[0].href)) {
         setUrl(obj[0].href);
      } else {
         setUrl(window.location.href);
      }
   };
   
   
   // Initialise the object
   this.setUri(obj);
};



/******************************************************************
** Abl - Core Functions
******************************************************************/
/**
 * chain()
 *
 * Chains an array of functions together and calls via the context
 * provided
 */
Abl.chain = function(oldFunc, newFunc) {
   var funcList, i;
   if (typeof oldFunc !== 'function') {
      return newFunc;
   } else {
      funcList = [oldFunc, newFunc];
      return function() {  
         for (i = 0; i < funcList.length; i++) {  
            funcList[i]();  
         }
      };
   }
};


/******************************************************************
** Abl.DateTime - Core Functions
******************************************************************/
/**
 * Converts a number representing a time period to milliseconds
 * 
 * @param {Number} n The time to be converted
 * @param {String} t Where 's'==seconds, 'm'==minutes, 'h'==hours, 'd'==days, 'w'==weeks
 */
Abl.DateTime.toMilliseconds = function(n, t) {
   switch (t) {
      case "s": return (n * 1000);
      case "m": return (n * 60 * 1000);
      case "h": return (n * 60 * 60 * 1000);
      case "d": return (n * 24 * 60 * 60 * 1000);
      case "w": return (n * 7 * 24 * 60 * 60 * 1000);
      default: return n;
   }
};


/******************************************************************
** Abl.Cookie - Core Functions
******************************************************************/
/**
 * Stores a named cookie
 * 
 * @param {String} name         The name of the cookie
 * @param {String} value      The cookie's value
 * @param {Number} ms         The cookie's expiration period
 * @param {String} timePeriod   See Abl.DateTime.toMilliseconds()
 */
Abl.Cookie.set = function(name, value, t, timePeriod) {
   var date, expires;
   if (t) {
      date = new Date();
      date.setTime(date.getTime() + Abl.DateTime.toMilliseconds(t, timePeriod));
      expires = "; expires=" + date.toGMTString();
   }
   document.cookie = name + "=" + escape(value) + expires + "; path=/";
};

/**
 * Rerieves a named cookie
 * 
 * @param {String} name         The name of the cookie to be retrieved
 */
Abl.Cookie.get = function(name) {
   var   nameEQ = name + "=",
         ca = document.cookie.split(';'),
         i, c;

   for (i = 0; i < ca.length; i++) {
      c = ca[i];
      while (c.charAt(0) === ' ') {
         c = c.substring(1, c.length);
      }
      if (c.indexOf(nameEQ) === 0) {
         return c.substring(nameEQ.length, c.length);
      }
   }
   return null;
};

/**
 * Retrieves a named cookie as an integer value
 * 
 * @param {String} name         The name of the cookie to be retrieved
 */
Abl.Cookie.getInt = function(name) {
   var x = Abl.Cookie.get(name);
   return (x) ? parseInt(x, 10) : 0;
};

/**
 * Retrieves a named cookie as an float value
 * 
 * @param {String} name         The name of the cookie to be retrieved
 */
Abl.Cookie.getFloat = function(name) {
   var x = Abl.Cookie.get(name);
   return (x) ? parseFloat(x) : 0.0;
};


/**
 * Provides a cross-browser method for obtaining the DOM document of
 * a named frame element
 */
Abl.getIFrameDocument = function(id) {
   var doc = null;

   if (document.frames) {
      doc = document.frames[id].document;
   } else {
      doc = document.getElementById(id).contentDocument;
   }

   return doc;
};

Abl.createIFrame = function(options) {
   return $("<iframe></iframe>")
      .attr({
         "tabindex": options.tabIndex || "-1",
         "frameBorder": options.frameBorder || "0",
         "border": "0 none",
         "src": options.source || "about:blank",
         "width": options.width || 100,
         "height": options.height || 100
      })
      .css({
         "border": "0 none",            // Note the logic on determining a width/height for the iFrame
         "width": options.width || 100,
         "height": options.height || 100
      });
};


/******************************************************************
** Abl.Math - Core Functions
******************************************************************/
/**
 * Forces the supplied value to fall within the specified bounds.
 * 
 * @param {Number} val   The value to be constrained
 * @param {Number} min   The lowest allowable value
 * @param {Number} max   The highest allowable value
 */
Math.constrain = function(val, min, max){
   return Math.min(Math.max(val, min), max);
};

Math.getInt = function(o, radix) {
   var x = parseInt(o, radix || 10);
   return (isNaN(x) ? 0 : x);
};

/**
 * Forces the width/height parameters to fall within the specified range.
 *
 * @param {Integer} width         Original width
 * @param {Integer} height         Original height
 * @param {Integer} maxWidth      Maximum allowable width
 * @param {Integer} maxHeight      Maximum allowable height
 * @return {Array}               An array of the adjusted width/height
 */Math.forceFit = function(width, height, maxWidth, maxHeight) {
   if (width > maxWidth) {
      height = parseInt(height * (maxWidth / width), 10);
      width = maxWidth;
   }
   if (height > maxHeight) {
      width = parseInt(width * (maxHeight / height), 10);
      height = maxHeight;
   }
   return [width, height];
};



/******************************************************************
** Abl.String - Core Functions
******************************************************************/
/**
 * Strips leading and trailing white-space from the string.
 */
String.prototype.trim = function() {
   return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
};

/**
 * Pads the left-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch   The character to use as padding
 * @param {Object} len   The target length of the string
 */
String.prototype.padLeft = function(ch, len) {
   var s = this;
   while (s.length < len) {
      s = ch + s;
   }
   return s;
};

/**
 * Pads the right-hand side of the string to obtain the target
 * length.
 * 
 * @param {String} ch   The character to use as padding
 * @param {Number} len   The target length of the string
 */
String.prototype.padRight = function(ch, len) {
   var s = this;
   while (s.length < len) {
      s += ch;
   }
   return s;
};


/**
 * Truncates a string of text back to the nearest word-break falling
 * on or before the maximum allowable length.  Optionally
 * adds a suffix string to the truncated string.
 *
 * @param {Number}   maxLength   The maximum allowable string length
 * @param {Number}   range      The number of characters to work back
 *                              from the specified break point in order
 *                              to find a word-break
 * @param {String}   suffix   String to be appended to the truncated 
 *                              result
 */
String.prototype.trimToIntro = function(maxLength, range, suffix) {
   var text = this, i;
   if (text.length > maxLength)
   {
      i = maxLength - range;
      if (i < 0) { i = 0; }
      while (((i < maxLength) && (text.substr(i, 1) !== " ")))
      {
         i++;
      }
      text = text.substr(0, i).trim() + suffix;
   }
   return text;
};

String.prototype.isSpace = function() {
   return ((this.length) && (/\s/.test(this)));
};
String.prototype.isUpper = function() {
   return ((this.length) && (this.substr(0,1) >= 'A') && (this.substr(0,1) <= "Z"));
};
String.prototype.isLower = function() {
   return ((this.length) && (this.substr(0,1) >= 'a') && (this.substr(0,1) <= "z"));
};

/**
 * Converts a string to 'proper case' notation and optionally
 * splits words with embedded uppercase characters.
 * "my name isNeil".toProperCase(true) === "My Name Is Neil"
 */
String.prototype.toProperCase = function(split) {
   var s = "", ch = "", inSpace = true, i = 0, len = (this.length - 1);
   for (i = 0; i <= len; i++) {
      ch = this.substr(i,1);
      s += (inSpace) ? ch.toUpperCase() : ch;
      inSpace =  ch.isSpace();
      if ((split) && (i < len)) {
         if ((ch.isLower()) && (this.substr(i+1,1).isUpper())) {
            s += " ";
         }
      }
   }
   return s;
};


/******************************************************************
** Abl.UI - Core Functions
******************************************************************/
/**
 * Returns the height of the document in pixels.
 * @return {Number} height of the document
 */
Abl.UI.getPageHeight = function() {
   return $(document).height();
};

/**
 * Returns the width of the document in pixels.
 * @return {Number} width of the document
 */
Abl.UI.getPageWidth = function() {
   return $(document).width();
};

Abl.UI.getPageMetrics = function() {
   return ({
      size: {width: $(document).width(), height: $(document).height()},
      scroll: {left: $(document).scrollLeft(), top: $(document).scrollTop()}
   });
};



/**
 * Returns the height of the browser viewport in pixels.
 * @return {Number} height of the viewport
 */
Abl.UI.getWindowHeight = function() {
   return $(window).height();
};

/**
 * Returns the width of the browser viewport in pixels.
 * @return {Number} width of the viewport
 */
Abl.UI.getWindowWidth = function() {
   return $(window).width();
};


Abl.UI.getWindowMetrics = function() {
   return ({
      size: {width: $(window).width(), height: $(window).height()},
      scroll: {left: $(window).scrollLeft(), top: $(window).scrollTop()}
   });
};


/******************************************************************
** Abl.Window - Core Functions
******************************************************************/
/**
 * Creates a new Uri() object representing a web address.
 * 
 * The constructor will optionally take a string or a html
 * anchor tag element to specify the address,  If no parameter
 * is specified the current page's address is used (via the
 * window.location.href property)
 * 
 * @constructor
 * @param {String, Object} [The web address reference]
 * @return {Object}
 */
Abl.Window.Uri = function(ref) {
   this.href = '';
   if (typeof ref === 'string') {
      this.href = ref;
   } else 
   if ((typeof ref === 'object')&&(ref.href)) {
      this.href = ref.href;
   } else
   if ((ref)&&(ref instanceof Abl.Window.Uri)) {
      this.href = ref.href;      
   } else
   if ((ref)&&(ref instanceof jQuery)&&(ref[0])&&(ref[0].href)) {
      this.href = ref[0].href;      
   } else {
      this.href = window.location.href;
   }
};

/**
 * Returns the query string element of the Uri object.
 * 
 * @return {string} Returns the Uri's query string
 */
Abl.Window.Uri.prototype.getQueryString = function() {
   var i = this.href.indexOf("?");
   if (i < 0) {
      return '';
   } else {
      return this.href.substr(i+1);
   }
};

/**
 * Returns the local address of the Uri object. For example:
 * 
 *      http://www.myweb.com/ContactUs/index.html?type=enquiry
 *      /ContactUs/index.html?type=enquiry
 * 
 * @return {String} The local address
 */
Abl.Window.Uri.prototype.getLocalUrl = function() {
   return Abl.Window.Uri.stripLocation(this.href);
};

/**
 * Returns the base/core url of the Uri object. For example:
 * 
 *      http://www.myweb.com/ContactUs/index.html?type=enquiry
 *      /ContactUs/index.html
 * 
 * @return {String} The base address
 */
Abl.Window.Uri.prototype.getBaseUrl = function() {
   var s = Abl.Window.Uri.stripLocation(this.href);
   return Abl.Window.Uri.stripQueryString(s);
};

/**
 * Returns the file name of a url
 * Example: getFileName('/Content/Default.aspx') === 'Default.aspx'
 */
Abl.Window.Uri.getFileName = function(ref) {
   var   uri = new Abl.Window.Uri(ref),
         i = uri.href.lastIndexOf("/"),
         s = (i >= 0) ? uri.href.substr(i+1) : uri.href;
   return Abl.Window.Uri.stripQueryString(s);
};

/**
 * Strips the current location's protocol information from
 * the supplied href/url.
 * 
 * @param {Object}   Reference to the href string/Uri() constructor object
 * @return {String}   The original href less the protocol
 */
Abl.Window.Uri.stripProtocol = function(ref) {
   var uri = new Abl.Window.Uri(ref);
   if (uri.href.indexOf(window.location.protocol) === 0) {
      return uri.href.substr(window.location.protocol.length);
   } else {
      return uri.href;
   }
};


/**
 * Strips the current location's protocol, hostname and port 
 * information from the supplied href/url.
 * 
 * @param {Object}   Reference to the href string/Uri() constructor object
 * @return {String}   The original href less the protocol, hostname
 *                     and port information
 */
Abl.Window.Uri.stripLocation = function(ref) {
   var   uri = new Abl.Window.Uri(ref),
         domain = window.location.protocol + '//' + window.location.hostname;

   if (window.location.port) {
      domain += ":" + window.location.port;
   }
   if (uri.href.indexOf(domain) === 0) {
      return uri.href.substr(domain.length);
   } else {
      return uri.href;
   }
};

/**
 * Strips the current location's query string data
 * from the supplied href/url.
 * 
 * @param {Object}   Reference to the href string/Uri() constructor object
 * @return {String}   The original href less the query string/bookmark
 */
Abl.Window.Uri.stripQueryString = function(ref) {
   var uri = new Abl.Window.Uri(ref), i;
   
   // Strip everyting after the first '?' or '#' ...
   i = uri.href.search(/(\?|#)/);   
   if (i >= 0) {
      uri.href = uri.href.substr(0,i);
   }
   return uri.href;
};

/**
 * Strips the current location's protocol, hostname, port 
 * and query string information from the supplied href/url.
 * 
 * @param {Object}   Reference to the href string/Uri() constructor object
 * @return {String}   The original href less the protocol,
 *                     hostname, port and query string information
 */
Abl.Window.Uri.getBaseUrl = function(ref) {
   var   uri = new Abl.Window.Uri(ref),
         s = Abl.Window.Uri.stripLocation(uri.href);
   return Abl.Window.Uri.stripQueryString(s);
};


Abl.Window.Uri.parseQueryString = function(queryString) {

   // define an object to contain the parsed query data
   var   result = {},
      queryComponents,
      i, keyValuePair, key, value;

   // if a query string wasn't specified, use the query string from the URI
   if (typeof queryString === "undefined") {
      queryString = (window.location.search) ? window.location.search : '';
   }

   // remove the leading question mark from the query string if it is present
   if (queryString.charAt(0) === "?") {
      queryString = queryString.substring(1);
   }

   // replace plus signs in the query string with spaces
   queryString = queryString.replace('+', ' ');

   // split the query string around ampersands and semicolons
   queryComponents = queryString.split(/[&;]/g);

   // loop over the query string components
   for (i = 0; i < queryComponents.length; i++){
      // extract this component's key-value pair
      keyValuePair = queryComponents[i].split('=');
      key = decodeURIComponent(keyValuePair[0]);
      value = decodeURIComponent(keyValuePair[1]);
      result[key] = value;
   }

   // return the parsed query data
   return result;
};


Abl.Window.Uri.getParam = function(n, queryString) {
   var qs = Abl.Window.Uri.parseQueryString(queryString);
   return (qs) ? qs[n] : null;
};




/****************************************************************************/
/** Extensions to base jQuery functionality                                **/
/****************************************************************************/
(function($) {

   /*
   ** These five 'message' functions provide easy access to an
   ** xml based message data structure.
   **
   ** See Abl.Web.HttpHandlers.XmlHandler.BuildErrorMessage()
   **
   ** Is the data structure of the form <message />?
   */
   $.fn.isMessage = function() {
      return (this.children(":first").is("message"));
   };

   // Is this an error message <message status="error" />
   $.fn.isErrorMessage = function() {
      return (this.children(":first").attr("status") === "error");
   };

   // Is this a success message <message status="success" />
   $.fn.isSuccessMessage = function() {
      return (this.children(":first").attr("status") === "success");
   };

   // Covert xml to json message format
   $.fn.toJsonMessage = function() {
      var j = {},
            $root = this.children(":first"),
            status = $root.attr("status"),
            $data = $root.children("data");

      if (!$root.is("message")) { throw "Invalid xml message format"; }

      if (status === "error") {
         j.error = $root.children("error").text();
      } else if (this.isSuccessMessage()) {
         j.success = $root.children("success").text();
      } else {
         throw "Unrecognised message status '" + status + "'!";
      }

      if ($data.length > 0) { j.data = $data.text(); }
      return j;
   };


   $.fn.getMessage = function() {
      var j = this.toJsonMessage(),
            msg = j.error || j.success || "";
      if (j.data) {
         msg += "\r\n\r\n" + j.data;
      }
      return msg;
   };


   // Returns the bookmark text from a link element.  If the 'evaluate'
   // argument is true, then the function treats the bookmark as a json
   // string and attempts to evaluate it with eval().
   //
   // <a id=lnk" href="#Fred">Link</a> => $("#lnk").getBookmark() == "Fred"
   //
   // <a id=lnk" href="#{name: 'Neil'}">Link</a> => $("#lnk").getBookmark(true) == {name: 'Neil'}
   $.fn.getBookmark = function(evaluate) {
      var regex = /#(.*)$/,
            $link = this.filter("a").eq(0),
            href = $link.attr("href"),
            match = regex.exec(href);
      if (match) {
         return (evaluate) ? eval("(" + match[1] + ")") : match[1];
      } else {
         return null;
      }
   };


   $.fn.setFocus = function(selectContent) {
      var $ctrl;
      return this.each(function() {
         $ctrl = $(this);
         if (($ctrl.is("textarea, select, :text")) && ($ctrl.is(":visible")) && (!$ctrl.is(":disabled"))) {
            if (this.focus) {
               this.focus();
               if ((selectContent) && (this.select)) {
                  this.select();
               }
            }
         }
      });
   };

   // Sets all the checkboxes in the selection to checked/unchecked
   $.fn.setCheckbox = function(checked) {
      var $chk;
      return this.each(function() {
         $chk = $(this);
         if ($chk.is(":checkbox")) {
            $chk.attr("checked", ((checked) ? "checked" : ""));
         }
      });
   };

   $.fn.enable = function(enabled) {
      var $ctrl;
      return this.each(function() {
         $ctrl = $(this);
         if ($ctrl.is(":input")) {
            if (enabled) {
               $ctrl.attr("disabled", "").removeClass("disabled");
            } else {
               $ctrl.attr("disabled", "disabled").addClass("disabled");
            }
         }
      });
   };



   $.fn.charMonitor = function(options) {
      return this.each(function() {
         var $textbox = $(this),
             maxLength = options.maxLength || 100,
             $wrapper = $("<div></div>").addClass(options.monitorClass || "charMonitor"),
             $monitor = $("<span></span>");

         $textbox.wrap($wrapper).parent().append($monitor);

         $textbox.bind("change keyup keydown mouseup mousedown", function() {
            var $t = $(this),
                $m = $t.next("span"),
                l = $t.val().length || 0;

            if (l > maxLength) {
               $t.val($t.val().substr(0, maxLength));
               l = maxLength;
            }


            $m.text(l + "/" + maxLength + " chars")
            .toggleClass("full", l >= maxLength);

         })
         .trigger("change");
      });
   };



   /**
   * $.alernateRows
   *
   * Adds odd/even styling to all tables in selection.
   */
   $.fn.alternateRows = function() {
      return this.each(function() {
         if ($(this).is("table")) {
            $(this).find("tr:not(:has(th)):odd").addClass("odd");
            $(this).find("tr:not(:has(th)):even").addClass("even");
         }
      });
   };



   /**
    * $.addColumnClass
    * Adds a css class to each column (th, td) of the specified table.
    * The classes are specified in a string array.  If the the array is
    * too short, then the function substitutes 'col1', 'col2', col3' etc.
    * $("table.prices").addColumnClass(["item", "price ralign"]);
    *
    * Note, this function could be slow on large tables!
    */
   $.fn.addColumnClass = function(classes) {
      var $tr, $td, r, c, cssClass;
      return this.each(function() {
         $tr = $(this).find("tr");
         for (r = 0; r < $tr.length; r++) {
            $td = $tr.eq(r).children();
            for (c = 0; c < $td.length; c++) {
               cssClass = ((classes) && (c < classes.length)) ? classes[c] : "col" + (c + 1);
               $td.eq(c).addClass(cssClass);
            }
         }
      });
   };



   /**
   * $.wrapper()
   * 
   * Wraps the inner content of an element with a <div /> for each class of 
   * the classList array.  This method is used, for example, by the panel
   * method to add additional markup for css styling.
   * 
   * @param {Array} classList
   */
   $.fn.wrapper = function(classList) {
      classList = classList || $.fn.wrapper.defaultList;
      return this.each(function() {
         var $this = $(this), i, wrapper;
         for (i = classList.length - 1; i >= 0; i--) {
            wrapper = "<div class='" + classList[i] + "'></div>";
            $this.wrapInner(wrapper);
         }
      });
   };

   /**
   * The default wrapper classList for adding the classic '8-points of the compass'
   * markup.
   */
   $.fn.wrapper.defaultList = ["e", "s", "w", "ne", "se", "sw", "nw", "panelBody"];



   /**
   * Use the method to create a simple top/body/bottom panel structure
   * of the form:
   * <div class='myPanel'>
   *      <div class='topBorder'>
   *            <div class='bottomBorder'>
   *            <div class='panelBody'>Original content goes here</div>
   *         </div>
   *      </div>
   * </div>
   *
   * If the parameter 'stacked' is set to true, the structure is rendered 
   * as follows:
   * <div class='myPanel'>
   *      <div class='topBorder'></div>
   *      <div class='panelBody'>Original content goes here</div>
   *      <div class='bottomBorder'></div>
   * </div>
   */
   $.fn.simplePanel = function(options) {
      var params = $.extend({}, $.fn.simplePanel.defaults, options);
      return this.each(function() {
         if (params.stacked) {
            $(this)
            .wrapInner("<div class='" + params.bodyPanelClass + "'></div>")
            .append("<div class='" + params.bottomBorderClass + "'></div>")
            .prepend("<div class='" + params.topBorderClass + "'></div>");
         } else {
            $(this)
            .wrapInner("<div class='" + params.bodyPanelClass + "'></div>")
            .wrapInner("<div class='" + params.bottomBorderClass + "'></div>")
            .wrapInner("<div class='" + params.topBorderClass + "'></div>");
         }
      });
   };
   $.fn.simplePanel.defaults = {
      stacked: false,
      bodyPanelClass: "panelBody",
      topBorderClass: "topBorder",
      bottomBorderClass: "bottomBorder"
   };


   /**
   * $.panel()
   * 
   * Wraps the inner content of the specified elements with a nested structure
   * of div elements which can then have 8-points of images applies to them.
   * 
   * @param {String} className   The css class name of the panel theme
   * @param {Object} options      Property list of options.
   * 
   * @see jquery.ui.draggable
   */
   $.fn.panel = function(className, options) {
      var params = $.extend({}, $.fn.panel.defaults, options);

      return this.each(function() {
         var $e = $(this),
               $panelBody,
               $titlebar,
               $header,
               $closeLink;

         if (className) { $e.addClass(className); }
         $e.css(params.css);

         // Add additional markup for '8-points of the compass' styling - note
         // use of default $.fn.wrapper.default array.
         $e.wrapper();

         // Get a reference to the inner content         
         $panelBody = $("div.panelBody", $e);


         // Set dimensions
         if (params.width) {
            $e.width(params.width);
         }
         if (params.height) {
            $panelBody.height(params.height);
         }

         if ((params.innerWidth) || (params.innerHeight)) {
            $e.css("position", "absolute");
            $panelBody.width(params.innerWidth);
            $panelBody.height(params.innerHeight);
         }

         // Manage the creation and presentation of the titlebar if specified
         if (params.titlebar) {
            // Create a titlebar and insert it before the existing
            // (inner) content
            $titlebar = $("<div class='titlebar'></div>");
            $panelBody.before($titlebar);

            // If title text has been specified, use it!
            if (params.title) {
               $titlebar.append("<h2 class='title'>" + params.title + "</h2>");
            } else {
               // Otherwise, see if the first element of the (inner) content is
               // a header (h1, h2, h3 etc.).  If it is, move it into the
               // titlebar container - neat eh? :-)
               $header = $panelBody.children(":first-child:header").addClass("title");
               if ($header.length) {
                  $titlebar.append($header);
               }
            }
         }

         if (params.closeLink) {
            $closeLink = $("<a></a>")
            .addClass(params.closeLinkClass)
            .attr("title", params.closeLinkTitle)
            .attr("href", params.closeLinkHref)
            .text(params.closeLinkText)
            .appendTo($titlebar);
         }

         // Configure jquery.ui.draggable features
         if ((params.draggable) && ($e.draggable)) {
            $e.draggable($.extend({ handle: ($titlebar) ? $titlebar : $e }, params.dragOptions));
         }
      });
   };

   /**
   * Default option settings for $panel
   * Note, additional jquery.ui.draggable options may be added to this
   * property list.
   */
   $.fn.panel.defaults = {
      width: null,
      height: null,
      innerWidth: null,
      innerHeight: null,
      titlebar: true,       // Set to 'true' to render a title bar container
      title: "",       // Titlebar text
      closeLink: false,
      closeLinkClass: "close",
      closeLinkTitle: "Close",
      closeLinkHref: "#close",
      closeLinkText: "X",
      css: {},
      draggable: true,       // Set to 'true' to enable dragging
      dragOptions: {                  // jquery.ui.draggable options list
         containment: "parent"      // jquery.ui.draggable option - was 'parent'
      }
   };

   /**
   * Calculates the overall distances (width/height) between the
   * outer container ($panel) and the content (div.panelBody).
   * 
   * @param   {Object} $panel   The jQuery panel to be interrogated
   * @return   {Object}      A width, height dictionary object
   */
   $.fn.panel.getPanelOffsets = function($panel) {
      var width = $panel.width(),
            height = $panel.height(),
            $panelBody = $("div.panelBody", $panel),
            innerWidth = $panelBody.width(),
            innerHeight = $panelBody.height();

      return {
         width: width - innerWidth,
         height: height - innerHeight
      };
   };


   $.fn.panel.setContentArea = function($panel, width, height) {
      var offset = $.fn.panel.getPanelOffsets($panel),
            $panelBody = $("div.panelBody", $panel);

      if (width) {
         $panel.width(width + offset.width);
         $panelBody.width(width);
      }

      if (height) {
         $panel.height(height + offset.height);
         $panelBody.height(height);
      }
   };


   /*
   ** jQuery Clamshell
   ** Provides $.hideClamshell(), $.showClamshell() and $.toggleClamshell() functionality
   ** against a standard nested list structure:
   ** 
   ** <ul>
   **      <li>
   **         <a />
   **         <ul>
   **            <li><a /></li>
   **         </ul>
   **      </li>
   **   </ul>
   **
   ** All methods should be called against a member <li> element.
   */

   /**
   * $.hideClamshell()
   * Hides the sub-content of the referenced <li> element
   */
   $.fn.hideClamshell = function(speed, callback) {
      return this.each(function() {
         var $li = $(this),                // the reference <li> element
               $link = $li.children("a"),    // the link <a> element
               $sub = $li.children("ul");    // the <ul> sub-content

         // The animated version of $.hide() does not work unless the element
         // is already visible - this is of no use to us as we need it to be 
         // collapsed when it does become visible - hence the code branch below.
         $link.attr("title", "Expand");
         $li.removeClass("expanded").addClass("collapsed");
         if ((speed) && ($sub.is(":visible"))) {
            $sub.hide(speed, function() {
               if (typeof callback === 'function') { callback.call(this, false); }
            });
         } else {
            $sub.hide();
            if (typeof callback === 'function') { callback.call(this, false); }
         }
      });
   };


   /**
   * $.showClamshell()
   * Shows the sub-content of the referenced <li> element
   */
   $.fn.showClamshell = function(speed, callback) {

      // Local method to show the sub-content and set the css classes and title attributes
      function show($li, $link, $sub) {
         $link.attr("title", "Collapse");
         $li.removeClass("collapsed").addClass("expanded");
         if ((speed) && (!$sub.is(":visible"))) {
            $sub.show(speed, function() {
               if (typeof callback === 'function') { callback.call(this, true); }
            });
         } else {
            $sub.show();
            if (typeof callback === 'function') { callback.call(this, true); }
         }
      }

      return this.each(function() {
         var $li = $(this),                // the reference <li> element
               $link = $li.children("a"),    // the link <a> element
               $sub = $li.children("ul"),    // the <ul> sub-content
               $parent = $li.parent("ul"); // the starting <ul> parent element - we ultimately
         // want the parent <li> element - if there is one

         // Try and locate the containing parent <li> element.  If it's already
         // visible then we don't need to recurse back up the hierarchy
         $parent = (($parent.length > 0) && (!$parent.is(":visible"))) ? $parent = $parent.parent("li") : null;

         if (($parent) && ($parent.length > 0)) {
            // If we have a valid, invisible <li> container, then we need to show that first (recursively)
            $parent.showClamshell(speed, function() { show($li, $link, $sub); });
         } else {
            // We just need to handle this sub-content
            show($li, $link, $sub);
         }
      });
   };

   /**
   * $.toggleClamshell()
   * Toggles the sub-content visibility of the referenced <li> element
   */
   $.fn.toggleClamshell = function(speed, callback) {
      return this.each(function() {
         var $li = $(this), $sub = $li.children("ul");

         if ($sub.is(":visible")) {
            $li.hideClamshell(speed, function(visible) {
               if (typeof callback === 'function') { callback.call(this, visible); }
            });
         } else {
            $li.showClamshell(speed, function(visible) {
               if (typeof callback === 'function') { callback.call(this, visible); }
            });
         }
      });
   };



   /**
   * Provide IE6 support for min-height
   * @param {Integer} minHeight
   */
   $.fn.minHeight = function(minHeight) {
      return this.each(function() {
         var $elem = jQuery(this);
         if ((!minHeight) || (typeof (minHeight) !== "number")) {
            minHeight = parseInt($elem.css("min-height"), 10);
         }
         if ((minHeight) && (typeof (minHeight) === "number") && ($elem.height() < minHeight)) {
            $elem.height(minHeight);
         }
      });
   };



   /**
   * Provide IE6 support for min-width
   * @param {Integer} minHeight
   */
   $.fn.minWidth = function(minWidth) {
      return this.each(function() {
         var $elem = jQuery(this);
         if ((!minWidth) || (typeof (minWidth) !== "number")) {
            minWidth = parseInt($elem.css("min-width"), 10);
         }
         if ((minWidth) && (typeof (minWidth) === "number") && ($elem.width() < minWidth)) {
            $elem.width(minWidth);
         }
      });
   };

} (jQuery));



/**
 * Objectifies the jQuery.panel() method result to provide an OO panel
 * class.
 * 
 * This class exposes the following jQuery objects:
 * 
 *      .$container      The outer div.className container
 *      .$body         The inner div.panelBody content container
 *      .$titlebar      The div.titlebar container (if specified)
 *      .closeLink      The a.closeLinkClass close link (if specified)
 * 
 * The class maps the following events:
 * 
 *      onClose         Raised when the $closeLink link is clicked
 */
Abl.UI.Panel = function(panel, className, options) {
   var   _self = this,
         params = $.extend(true, {}, $.fn.panel.defaults, Abl.UI.Panel.defaults, options);
   
   this.$ = (panel instanceof jQuery) ? panel : $(panel);
   this.$.panel(className, params);
   this.$body = $("div.panelBody", this.$);
   this.$titlebar = $("div.titlebar", this.$);
   this.$closeLink = $("a." + params.closeLinkClass, this.$).click(function(evt) {
      evt.preventDefault();
      if (typeof params.onClose === 'function') {
         params.onClose.call(_self, evt);
      }      
   });
   
   this.setContentArea = function(width, height) {
      $.panel.setContentArea(this.$, width, height);
   };
   
   this.isVisible = function() {
      return this.$.is(":visible");
   };
   
   
   this.dispose = function() {
      this.$closeLink.unbind("click");
   };
};


/**
 * Extends $.fn.panel.defaults
 */
Abl.UI.Panel.defaults = {
   onClose: null
};



/*
** ImagePreLoader
** Loads an array of images of the form...
**
**      var gallery = [
**         { src: "/img/bathroom.jpg", alt: "Stylish Bathroom" },
**         { src: "/img/tanker.jpg", alt: "Drainage Tanker" }
**      ];
**      var images = new Abl.UI.ImagePreLoader({
**         onComplete: function() {
**            alert("All images loaded");
**         }
**      });
**      images.loadImages(gallery);
**
** Version 1.1
** onLoad() function renamed to onComplete().  onLoad() function now
** triggered for each successful image load
**
** Version 1.2
** Auto gallery loading feature removed from class constructor - use
** loadImages() method instead
*/
Abl.UI.ImagePreLoader = function (options) {
   var _self = this,
         params = null,
         noImages = 0,
         noLoaded = 0,
         noErrors = 0,
         noAborted = 0,
         noProcessed = 0,
         bIsLoaded = false,
         index = 0,
         aImages = [];


   if ((options) && (options.constructor === Array)) {
      throw "Abl.UI.ImagePreLoader::ctor does not accept an image array parameter.  Use the loadImages() method.";
   }

   params = $.extend(true, {}, Abl.UI.ImagePreLoader.defaults, options);


   /*
   ** Event Handlers
   */
   function onComplete() {
      noProcessed++;
      if (noProcessed === noImages) {
         bIsLoaded = true;
         if (typeof params.onComplete === 'function') {
            params.onComplete.call(_self);
         }
      }
   }

   function onLoad() {
      // Abl.DEBUG.trace("Abl.UI.ImagePreLoader.onLoad(" + this.src + ")");
      $(this).data("preload").loaded = true;
      noLoaded++;
      if (typeof params.onLoad === 'function') {
         params.onLoad.call($(this));
      }
      onComplete();
   }

   function onError(evt) {
      // Abl.DEBUG.trace("Abl.UI.ImagePreLoader.onError(" + evt.target.src + ")");
      $(this).data("preload").error = true;
      noErrors++;
      if (typeof params.onError === 'function') {
         params.onError.call($(this));
      }
      onComplete();
   }

   function onAbort() {
      // Abl.DEBUG.trace("Abl.UI.ImagePreLoader.onAbort(" + this.src + ")");
      $(this).data("preload").aborted = true;
      noAborted++;
      if (typeof params.onAbort === 'function') {
         params.onAbort.call($(this));
      }
      onComplete();
   }



   /*
   ** Private Methods
   */
   function loadImage(imageData) {
      var $img = $("<img />");

      if (typeof imageData === 'string') {
         imageData = { src: imageData };
      }

      if (!imageData.src) {
         imageData.src = imageData.url;
      }

      if ((typeof imageData.src !== 'string') || (imageData.src.length < 1)) {
         throw("Abl.UI.ImagePreLoader.loadImage() - imageData does not specify a 'src' attribute!");
      }

      $img.bind("load", onLoad);
      $img.bind("error", onError);
      $img.bind("abort", onAbort);
      $img.data("preload", { loaded: false, error: false, aborted: false });

      // Add expando data if specified - first check for a name/value object
      // and then as a simple data object (possibly a Abl.Web.IO.FileObject)
      if ((imageData.data) && (imageData.data.name) && (imageData.data.value)) {
         $img.data(imageData.data.name, imageData.data.value);
      } else if (params.attachData) {
         $img.data("data", imageData);
      }



      if (imageData.alt) { $img.attr("alt", imageData.alt); }
      $img.attr("src", imageData.src);

      aImages.push($img);
   }



   function clearImages() {
      var i, $img;

      for (i = 0; i < aImages.length; i++) {
         $img = aImages[i];
         $img.removeData("preload").unbind();
      }

      aImages = [];
      index = noImages = noLoaded = noErrors = noAborted = noProcessed = 0;
      bIsLoaded = false;
   }


   /*
   ** Public Properties
   */
   this.isLoaded = function () {
      return bIsLoaded;
   };

   this.getNoImages = function () {
      return noImages;
   };

   this.getNoLoaded = function () {
      return noLoaded;
   };

   this.getNoErrors = function () {
      return noErrors;
   };

   this.getNoAborted = function () {
      return noAborted;
   };

   this.getIndex = function () {
      return index;
   };


   /*
   ** Public Methods
   */
   this.setIndex = function (i) {
      if ((i < 0) || (i >= noImages)) { i = 0; }
      index = i;
   };

   this.setNextImage = function () {
      this.setIndex(index + 1);
   };

   this.getImage = function () {
      return aImages[index];
   };

   this.getNextImage = function () {
      this.setNextImage();
      return this.getImage();
   };

   this.each = function (callback) {
      var i = 0;
      for (i = 0; i < noImages; i++) {
         callback.call(aImages[i], i);
      }
   };

   this.dispose = function () {
      clearImages();
      params.onComplete = null;
      params.onLoaded = null;
      params.onError = null;
      params.onAbort = null;
   };



   this.loadImages = function (images, callback) {
      var i = 0;

      // Abl.DEBUG.trace("Abl.UI.ImagePreLoader.loadImages(" + images.length + ")");
      clearImages();

      if (typeof callback === 'function') {
         params.onComplete = callback;
      }

      noImages = images.length;
      for (i = 0; i < images.length; i++) {
         loadImage(images[i]);
      }
   };

};

Abl.UI.ImagePreLoader.defaults = {
   attachData: false,
   onComplete: null,
   onLoaded: null,
   onError: null,
   onAbort: null
};



Abl.Json.unescape = function(s) {
   if ((s) && (typeof s === 'string')) {
      s = s.replace(/\\\'/g, "\'");      // Replace single quote
      s = s.replace(/\\\"/g, "\"");      // Replace double quote
   }
   return s;
};



/*
** Creates and manages an image thumbnail object.
**
** var thumb = Abl.UI.Thumbnail($container, {
**      onLoad: function() { alert("Image Loaded!"); }
**   });
** thumb.loadImage("/img/HondaSP2.jpg");
*/

Abl.UI.Thumbnail = function (container, options) {
   return (function (container, options) {
      var foo = {},
            $container = (container instanceof jQuery) ? container : $(container),
            img = null;

      foo.params = $.extend(true, {}, Abl.UI.Thumbnail.defaults, options);


      /*******************************************************************************************
      * Image Management                                                                         *
      *******************************************************************************************/
      foo.clearImage = function () {
         if (img) {
            img.onload = null;
            img = null;
         }
         $container.children("img.thumbnail").remove();
         $container.children("div.sizeInfo").remove();
      };



      /*******************************************************************************************
      * Image Access                                                                             *
      *******************************************************************************************/
      foo.getDimensions = function () {
         var dims = { width: 0, height: 0 };

         if (img) {
            dims.width = img.width;
            dims.height = img.height;
         }
         return dims;
      };

      foo.getImage = function () {
         return img;
      };

      // Will accept a width, height array or a data dictionary for comparison
      foo.equalSize = function (dims) {
         var d = (dims instanceof Array) ? { width: dims[0], height: dims[1]} : dims;
         return ((img) && (img.width === d.width) && (img.height === d.height));
      };


      /*******************************************************************************************
      * Event Handlers                                                                           *
      *******************************************************************************************/
      function onLoad() {
         var size = Math.forceFit(img.width, img.height, foo.params.thumbWidth, foo.params.thumbHeight),
               $img = $container.children("img.thumbnail"),
               $sizeInfo = $container.children("div.sizeInfo"),
               $dim, $scale, scale, scaleText;

         if (!$img.length) {
            $img = $("<img />").appendTo($container);
         }

         $img.attr({
            width: size[0],
            height: size[1],
            alt: img.alt,
            title: img.alt,
            src: img.src
         }).css({
            width: size[0] + "px",
            height: size[1] + "px"
         }).addClass("thumbnail");

         if (foo.params.showInfo) {
            if (!$sizeInfo.length) {
               $sizeInfo = $("<div class='sizeInfo'></div>").appendTo($container);
            }

            $dim = $sizeInfo.children("span.dim");
            if (!$dim.length) {
               $dim = $("<span class='dim'></span>").appendTo($sizeInfo);
            }

            $scale = $sizeInfo.children("span.scale");
            if (!$scale.length) {
               $scale = $("<span class='scale'></span>").appendTo($sizeInfo);
            }


            $dim.text("Size: " + img.width + " x " + img.height);
            scale = parseInt((size[0] / img.width) * 100, 10);
            scaleText = (scale === 100) ? "(Shown full size)" : "(Shown at " + scale + "% of actual size)";
            $scale.text(scaleText);
         }

         if (typeof foo.params.onLoad === "function") {
            foo.params.onLoad(img);
         }
      }

      foo.addOnLoadEventHandler = function (fn) {
         foo.params.onLoad = Abl.chain(foo.params.onLoad, fn);
      };


      /*******************************************************************************************
      * Image Loading Methods and (internal) Events                                              *
      *******************************************************************************************/
      foo.loadImage = function (url) {
         if (!img) {
            img = new Image();
            img.onload = onLoad;
         }
         img.src = url;
      };


      /*******************************************************************************************
      * Object Methods                                                                           *
      *******************************************************************************************/
      foo.dispose = function () {
         foo.clearImage();
      };


      return foo;

   } (container, options));
};

Abl.UI.Thumbnail.defaults = {
   thumbWidth: 150,
   thumbHeight: 100,
   showInfo: true
};
