﻿/*
jquery.combobox
version 0.1.2.7

Copyright © 2007,2008 Minel Pather|Ahura Mazda|jquery.sanchezsalvador.com
Dual licensed under MIT and GPL licences:
  * www.opensource.org/licenses/mit-license.php
  * www.gnu.org/licenses/gpl.html
*/
jQuery.fn.combobox =
  function(styles, options)
  {
    var _context = this;
    // create a combobox class instance instead of jQuery.fn.combobox which is a namespace.
    this.combobox = new Function();
    
    // Style Settings that determine the look of the control
    var styleSettings =
    {   
      comboboxContainerClass: null,
      comboboxValueContentContainerClass: null,
      comboboxValueContentClass: null,
      comboboxDropDownButtonClass: null,
      comboboxDropDownClass: null,
      comboboxDropDownItemClass: null,
      comboboxDropDownItemHoverClass: null,
      comboboxDropDownGroupItemHeaderClass: null,
      comboboxDropDownGroupItemContainerClass: null
    };
    
    // Option settings that determine the functionality of the control
    var optionSettings =
    {
      animationType: "slide",
      animationSpeed: "fast", // can be "fast", "slow", or a number in milliseconds
      width: 120
    };
    
    if (styles)
    {
      jQuery.extend(styleSettings, styles);
    }
    
    if (options)
    {
      jQuery.extend(optionSettings, options);
    }
    
    //#start public events
    
    ///<summary>
    /// Called whenever the user selects a different item in the list.
    /// By default, event is not called if it has not been assigned.
    ///</summary>
    this.combobox.onChange = null;
    
    //#end public events
    
    //#start private functions
    
    ///<summary>
    /// Returns the Combobox internal instance
    ///</summary>
    function getInstance(context)
    {
      return context[0].internalCombobox;
    }

    // All make*Function(context) functions, create wrappers around the internal Combobox functions
    // and makes the function element specific.
    function makeRemoveFunction(context)
    {
      return function()
      {
        getInstance(context).remove();
      };
    }
    
    function makeUpdateFunction(context)
    {
      return function()
      {
        getInstance(context).update();
      }
    }
    
    function makeUpdateSelectionFunction(context)
    {
      return function()
      {
        getInstance(context).updateSelection();
      }
    }
    
    function makeAddRangeFunction(context)
    {
      return function(dataSource)
      {
        getInstance(context).addRange(dataSource);
      }
    }
          
    //#end private functions
    
    //#start exposed public methods
        
    // Add functionality to the combobox namespace
    jQuery.fn.extend(
      this.combobox,
      {
        addRange: makeAddRangeFunction(_context),
        remove: makeRemoveFunction(_context),
        update: makeUpdateFunction(_context),
        updateSelection: makeUpdateSelectionFunction(_context)
      });
      
    //#end exposed public methods
    
    return this.each(
      function()
      {
        // Create a new instance of the Combobox Class, intialising it with the DOM element to operate on and
        // attach the instance to the DOM
        this.internalCombobox = new ComboboxClass(this);
        
        // Call the instance to initialise itself
        this.internalCombobox.initialise();
        
        ///<summary>
        ///   a state-based class that is performs all functions necessary for the Combobox to work
        ///</summary>
        function ComboboxClass(elementDOM)
        {
          //#start 'private' variables
          
          // This class can operate on N amount of elements depending how combobox() is called
          // for example $("select").combobox() could return multiple Selects.
          // This variable should always be a Select JQuery element.
          // TODO: Check if select control is used
          var _originalElementJQuery = jQuery(elementDOM);
          var _containerJQuery = null;
          var _containerDefaultStyle = "background-color:#fff;border-left: solid 2px #777;border-top: solid 2px #777;border-right: solid 1px #ccc;border-bottom: solid 1px #ccc;";
          var _containerEnforcedStyle = "padding:0;";
          var _dropDownListJQuery = null;
          var _dropDownListEnforcedStyle = "list-style-type:none;min-height:15px;padding-top:0;margin:0;overflow:auto";
          var _dropDownListDefaultStyle = "cursor:default;padding:2px;background:#fff;border-right:solid 1px #000;border-bottom:solid 1px #000;border-left:solid 1px #aaa;border-top:solid 1px #aaa;";
          var _dropDownListItemEnforcedStyle = "display:block;";
          var _dropDownListItemDefaultStyle = "cursor:default;padding-left:2px;font-weight:normal;font-style:normal;";
          var _dropDownListGroupItemContainerEnforcedStyle = "list-style-type:none;";
          var _dropDownListGroupItemContainerDefaultStyle = "padding-left:10px;margin-left:0;";
          var _dropDownListGroupItemHeaderEnforcedStyle = "";
          var _dropDownListGroupItemHeaderDefaultStyle = "font-style:italic;font-weight:bold;";
          var _dropdownListMaximumHeight = 300; // default max height: 300px
          var _valueContentContainerJQuery = null;
          var _valueContentContainerEnforcedStyle = "position:relative;overflow:hidden;";
          var _valueContentJQuery = null;
          var _valueContentEnforcedStyle = "float:left;position:absolute;cursor:default;overflow:hidden;";
          var _valueContentDefaultStyle = "padding-left:3px;";
          var _dropDownButtonJQuery = null;
          var _dropDownButtonDefaultStyle = "overflow:hidden;width:16px;height:18px;color:#000;background:#D6D3CE;font-family:arial;font-size:8px;cursor:default;text-align:center;vertical-align:middle;";
          var _dropDownButtonEnforcedStyle = "background-repeat:no-repeat;float:right;";
          var _dropDownButtonDefaultUnselectedStyle = "padding-left:0px;padding-top:1px;width:12px;height:13px;border-right:solid 2px #404040;border-bottom:solid 2px #404040;border-left:solid 2px #f0f0f0;border-top:solid 2px #f0f0f0";
          var _dropDownButtonDefaultSelectedStyle = "padding-left:1px;padding-top:3px;width:12px;height:13px;border:solid 1px #808080";
          var _dropDownButtonDefaultCharacter = "&#9660;"; //down-arrow character
          var _lastItemSelectedJQuery = null;
          var _lastItemHoveredJQuery = null;
          var _lastValue = null;
          var _downdownListPositionIsInverted = false;
          var _maximumItemLength = 0;
          var _dropDownListOffset = null;
          var _dropDownListHeight = 0;
          var _dropDownButtonImageDimension = null;
          var _valueContentContainerImageDimension = null;
          var _valueContentMaximumHeight = null;
          
          //#end 'private' variables
          
          //#start 'private' methods
          
          ///<summary>
          /// Function extension to String.
          /// Replaces the placeholder tags '{0}...{n}' with the parameters based on ordinal position of the parameters
          /// Example: String.format("The quick {0} fox {2} over the lazy {1}.", "brown", "Dog", "jumps");
          /// Output: The quick brown fox jumps over the lazy Dog.
          ///</summary>
          String.format =
            function()
            {
              var currentString = null;
              if (arguments.length != 0)
              {
                currentString = arguments[0];
                for (var argumentIndex = 1; argumentIndex < arguments.length; argumentIndex++)
                {
                  var modifiedString = new RegExp('\\{' + (argumentIndex - 1) + '\\}','gm');
                  currentString = currentString.replace(modifiedString, arguments[argumentIndex]);
                }
              }
              
              return currentString;
            };
            
          ///<summary>
          /// Returns the value from a string that has 'px' embedded.
          /// This function is normally used when working with CSS values.
          /// Note: returns null if the extension is not 'px', i.e. it may be 'em', 'pt', etc.
          ///</summary>
          function getPixelValue(object)
          {
            var pixelValue = null;
            
            if (object)
            {
              if (object.substr(-2, 2) == "px")
              {
                pixelValue = object.substr(0, (object.length - 2));
              }
            }
            
            return pixelValue;
          }

          ///<summary>
          /// Sets the width of an element taking into consideration any borders and padding.
          /// Works exactly like Internet Explorers Box Model, where the padding and border is considered
          //  part of the width. For the purposes of this control, it is required in certain circumstances.
          /// Example:
          ///  <div id="container" style="width: 150px; border:solid 2px #000"></div>
          ///   jQuery('#container').width(); // 150px
          ///   jQuery('#container').outerWidth(); // 154px (2px border on the left and right)
          ///   setInnerWidth(jQuery('#container'), 120);
          ///   jQuery('#container').width(); // 116px
          ///   jQuery('#container').outerWidth(); // 120px (2px border on the left and right)
          ///</summary>       
          function setInnerWidth(elementJQuery, width)
          {
            var differenceWidth = (elementJQuery.outerWidth() - elementJQuery.width());
            
            elementJQuery.width(width - differenceWidth);
          }
          
          ///<summary>
          /// Sets the height of an element taking into consideration any borders and padding.
          /// Works exactly like Internet Explorers Box Model, where the padding and border is considered
          //  part of the height. For the purposes of this control, it is required in certain circumstances.      
          ///</summary>       
          function setInnerHeight(elementJQuery, height)
          {
            var differenceheight = (elementJQuery.outerHeight() - elementJQuery.height());
            
            elementJQuery.height(height - differenceheight);
          }
          
          ///<summary>
          /// Applies CSS styling from a string that contains multiple style styleSettings
          /// Example: "background-color:#fff;color:#0f0;border:solid 1px #00f;"
          ///</summary>     
          function applyMultipleStyles(elementJQuery, multipleCSSStyles)
          {
            var stylePairArray = multipleCSSStyles.split(";");
            if (stylePairArray.length > 0)
            {
              for (var stylePairArrayIndex = 0; stylePairArrayIndex < stylePairArray.length; stylePairArrayIndex++)
              {
                var stylePair = stylePairArray[stylePairArrayIndex];
                var splitStylePair = stylePair.split(":");
                
                elementJQuery.css(splitStylePair[0], splitStylePair[1]);
              }
            }
          }
          
          ///<summary>
          /// Calculates the width and height of an image from its URL
          ///</summary>
          function getImageDimension(imageURL)
          {
            var dimension = new Object();
            dimension.width = 0;
            dimension.height = 0;
            
            sizingImageJQuery = jQuery("<img style='border:none;margin:0;padding:0;'></img>");
            sizingImageJQuery.attr("src", imageURL);
            
            _containerJQuery.append(sizingImageJQuery);
            
            dimension.width = sizingImageJQuery.width();
            dimension.height = sizingImageJQuery.height();
            
            sizingImageJQuery.remove();

            return dimension;
          }
        
          ///<summary>
          /// Calculates the background image size for an JQuery element if it has a CSS background-image set.
          ///</summary>
          function calculateIndividualImageDimension(jqueryElement)
          {
            var dimension = null;
            var backgroundImageURL = jqueryElement.css("background-image");
            // Depending on the browser, the URL of the background-image sometimes is padded with extra characters
            backgroundImageURL = backgroundImageURL.replace("url(", "", "gi");
            backgroundImageURL = backgroundImageURL.replace('"', '', "gi");
            backgroundImageURL = backgroundImageURL.replace('\"', '', "gi");
            backgroundImageURL = backgroundImageURL.replace(")", "", "gi");
            
            if (backgroundImageURL != "none")
            {
              dimension = getImageDimension(backgroundImageURL);
            }
            
            return dimension;
          }
          
          ///<summary>
          /// Calculates the background image size for the value display and drop down button.
          /// These dimensions are used for control states, normal, pressed [, and hover]
          ///</summary>
          function calculateImageDimensions()
          {
            _dropDownButtonImageDimension = calculateIndividualImageDimension(_dropDownButtonJQuery);
            _valueContentContainerImageDimension = calculateIndividualImageDimension(_valueContentContainerJQuery);
          }
          
          ///<summary>
          /// Changes the visual of the value container to indicate a state.
          /// If the background-image is set and does not contain additional images for states,
          /// then the image is not changed for the different states. The Select for Safari works like this.
          /// The image states are stored below each other
          /// NOTE: This is different from the Drop Down Button where the images are stored side by side.
          /// for example
          /// A value container has a width of 275 pixels and a height of 35 pixels.
          /// The background-image is set to valuebackground.gif.
          /// valuebackground.gif is 70 pixels in height. The 'pressed' state image is at pixel height 35 in the image.
          /// States are:
          /// Normal = 0
          /// Pressed = 1
          ///</summary>
          function setValueContentContainerState(state)
          {
            if (styleSettings.comboboxValueContentContainerClass)
            {
              // Only process buttomn states if a background-image has been set
              if (_valueContentContainerImageDimension != null)
              {
                var height = _valueContentContainerJQuery.height();
                var offset = (state * height);
                
                // Check if the image is higher than the set height.
                // This signifies that the image file contain different images below each other for different
                // states.
                if (_valueContentContainerImageDimension.height > offset)
                {
                  var background_positionCSS = String.format("0px -{0}px", offset);
                  _valueContentContainerJQuery.css("background-position", background_positionCSS);
                }
              }
            }
          }
          
          ///<summary>
          /// Changes the visual of the drop down button to indicate a state.
          /// If the background-image is not set, then the default style is applied.
          /// If the background-image is set and does not contain additional images for states,
          /// then the image is not changed for the different states. The Select for Safari works like this.
          /// The image states are stored side by side: for example
          /// A drop-down button has a width of 16 pixel. The background-image is set to button.gif
          /// Button.gif is 32 pixels wide. The 'pressed' state image is at pixel position 16 in the image.
          /// States are:
          /// Normal = 0
          /// Pressed = 1
          ///</summary>
          function setDropDownButtonState(state)
          {
            if (styleSettings.comboboxDropDownButtonClass)
            {
              // Only process buttomn states if a background-image has been set
              if (_dropDownButtonImageDimension != null)
              {
                var width = _dropDownButtonJQuery.width();
                var offset = (state * width);
                
                // Check if the image is wider than the set width.
                // This signifies that the image file contain different images next to each other for different
                // states.
                if (_dropDownButtonImageDimension.width > offset)
                {
                  var background_positionCSS = String.format("-{0}px 0px", offset);
                  _dropDownButtonJQuery.css("background-position", background_positionCSS);
                }
              }
            }
            else
            {
              var style = _dropDownButtonDefaultUnselectedStyle;
              
              if (state == 1)
              {
                style = _dropDownButtonDefaultSelectedStyle;
              }
              
              applyMultipleStyles(_dropDownButtonJQuery, style);
            }     
          }
          
          ///<summary>
          /// Changes the visual appearance of the controls to represent the current state.
          /// States are:
          /// Normal = 0
          /// Pressed = 1
          ///</summary>
          function setControlVisualState(state)
          {
            setValueContentContainerState(state);
            
            setDropDownButtonState(state);
          }
          
          ///<summary>
          /// Builds the elements that make up the always visible portion of the control.
          /// The equivalent of a Textbox-type element.
          /// Also creates the DropDownButton
          ///</summary>
          function buildValueContent()
          {
            // A container for the Display Value and DropDownButton. A container is required as the child elements
            // are floated
            var valueContentContainerHTML = "";
            if (styleSettings.comboboxValueContentContainerClass)
            {
              valueContentContainerHTML = String.format("<div class='{0}' style='{1}'></div>", styleSettings.comboboxValueContentContainerClass, _valueContentContainerEnforcedStyle);
            }
            else
            {
              valueContentContainerHTML = String.format("<div style='{0}'></div>", _valueContentContainerEnforcedStyle);
            }
            
            // Create the equivalent of the select 'textbox' where the current selected value is shown
            var valueContentHTML = "";
            if (styleSettings.comboboxValueContentClass)
            {
              valueContentHTML = String.format("<div class='{0}' style='{1}'></div>", styleSettings.comboboxValueContentClass, _valueContentEnforcedStyle);
            }
            else
            {
              valueContentHTML = String.format("<div style='{0}'></div>", _valueContentEnforcedStyle + _valueContentDefaultStyle);
            }
            
            var dropdownButtonHTML = "";
            if (styleSettings.comboboxDropDownButtonClass)
            {
              dropdownButtonHTML = String.format("<div class='{1}' style='{0}'></div>",_dropDownButtonEnforcedStyle, styleSettings.comboboxDropDownButtonClass);
            }
            else
            {
              dropdownButtonHTML = String.format("<div style='{0}'>{1}</div>", (_dropDownButtonEnforcedStyle + _dropDownButtonDefaultStyle), _dropDownButtonDefaultCharacter);
            }
            
            _valueContentJQuery = jQuery(valueContentHTML);
            _dropDownButtonJQuery = jQuery(dropdownButtonHTML);
            _valueContentContainerJQuery = jQuery(valueContentContainerHTML);
            
            _valueContentContainerJQuery.appendTo(_containerJQuery);
            _valueContentJQuery.appendTo(_valueContentContainerJQuery);
            _dropDownButtonJQuery.appendTo(_valueContentContainerJQuery);
            
            calculateImageDimensions();
            
            _valueContentMaximumHeight = getPixelValue(_valueContentJQuery.css("max-height"));
          
            // Set control to normal state
            setControlVisualState(0);
          }
          
          ///<summary>
          /// Build a drop down list element populating it will values, text, styles and class
          /// depending on the source value type. The source value (childJQuery) can be an option or
          /// and optgroup element.
          ///</summary>
          function buildDropDownItem(childJQuery)
          {
            var dataItemHTML = "";
            var dataItemClass = null;
            var dataItemText = "";
            var dataItemTitle = "";
            var dataItemValue = null;
            var dataItemStyle = "";
            var dataItemType = "option";
            var childElement = childJQuery[0];
            
            if (childElement.title)
            {
              if (childElement.title != "")
              {
                dataItemTitle = childElement.title;
              }
            }
            
            if (childJQuery.is('option'))
            {
              if (childElement.dataText)
              {
                dataItemText = childElement.dataText;
              }
              else
              {
                dataItemText = childJQuery.text();
              }
              dataItemValue = childJQuery.val();
              
              if (styleSettings.comboboxDropDownItemClass)
              {
                dataItemClass = styleSettings.comboboxDropDownItemClass;
                dataItemStyle = _dropDownListItemEnforcedStyle;
              }
              else
              {
                dataItemStyle = (_dropDownListItemEnforcedStyle + _dropDownListItemDefaultStyle);
              }
              
              if (dataItemClass)
              {           
                dataItemHTML = String.format("<li style='{0}' class='{1}'>{2}</li>", dataItemStyle, dataItemClass, dataItemText);
              }
              else
              {
                dataItemHTML = String.format("<li style='{0}'>{1}</li>", dataItemStyle, dataItemText);
              }
              
            }
            else
            {
              if (childJQuery[0].dataText)
              {
                dataItemText = childJQuery[0].dataText;
              }
              else
              {
                dataItemText = childJQuery.attr('label');
              }
              dataItemValue = childJQuery.attr('class');
              dataItemType = "optgroup";
              
              if (styleSettings.comboboxDropDownGroupItemHeaderClass)
              {
                dataItemClass = styleSettings.comboboxDropDownGroupItemHeaderClass;
                dataItemStyle = _dropDownListGroupItemHeaderEnforcedStyle;
              }
              else
              {
                dataItemStyle = (_dropDownListGroupItemHeaderEnforcedStyle + _dropDownListGroupItemHeaderDefaultStyle);
              }
              
              if (dataItemClass)
              {           
                dataItemHTML = String.format("<li><span style='{0}' class='{1}'>{2}</span></li>", dataItemStyle, dataItemClass, dataItemText);
              }
              else
              {
                dataItemHTML = String.format("<li><span style='{0}'>{1}</span></li>", dataItemStyle, dataItemText);
              }
            }
            
            var dataItemJQuery = jQuery(dataItemHTML);
            
            // The element's style is set to inline for the calculation of the true width
            // The element is then reset to its enforced style (display:block) later
            dataItemJQuery.css("display", "inline");
            // Store the value with the DOMElement which is persisted with the Browser
            dataItemJQuery[0].dataText = dataItemText;
            dataItemJQuery[0].dataValue = dataItemValue;
            dataItemJQuery[0].dataType = dataItemType;
            if (dataItemTitle == "")
            {
              dataItemTitle = dataItemText
            }
            dataItemJQuery[0].title = dataItemTitle;
            
            return dataItemJQuery;
          }
          
          ///<summary>
          /// Recusively build the drop down list elements based on the options and optgroups contained
          /// with the original Select element
          ///</summary>
          function recursivelyBuildList(parentJQuery, childrenOptionsJQuery)
          {
            childrenOptionsJQuery.each(
              function()
              {
                var childJQuery = jQuery(this);
                var builtDropDownItemJQuery = buildDropDownItem(childJQuery);
                parentJQuery.append(builtDropDownItemJQuery);
                
                // Calculate the true width of the item taking into consideration the offset from the left-edge
                // of the drop-down list.
                // Get the left offset of the DropDown list container and subtract that from the builtDropDownItemJQuery left offset
                //  to get the distance the builtDropDownItemJQuery is from its container
                var offsetLeft = builtDropDownItemJQuery.offset().left;
                
                offsetLeft -= _dropDownListOffset.left;
                
                if (offsetLeft < 0)
                {
                  offsetLeft = 0;
                }
                
                var width = (offsetLeft + builtDropDownItemJQuery.outerWidth());
                if (width > _maximumItemLength)
                {
                  _maximumItemLength = width;
                }
                
                // Set the enforced style of display:block after the width has been calculated.
                applyMultipleStyles(builtDropDownItemJQuery, _dropDownListItemEnforcedStyle);
                
                if (childJQuery.is('optgroup'))
                {
                  var dataGroupItemHTML = "";
                  if (styleSettings.comboboxDropDownGroupItemContainerClass)
                  {
                    dataGroupItemHTML = String.format("<ul style='{0}' class='{1}'></ul>", _dropDownListGroupItemContainerEnforcedStyle, styleSettings.comboboxDropDownGroupItemContainerClass);
                  }
                  else
                  {
                    dataGroupItemHTML = String.format("<ul style='{0}'></ul>", (_dropDownListGroupItemContainerEnforcedStyle + _dropDownListGroupItemContainerDefaultStyle));
                  }
                  
                  var dataGroupItemJQuery = jQuery(dataGroupItemHTML);
                  builtDropDownItemJQuery.append(dataGroupItemJQuery);
                  
                  // If not an option, then the child of a Select must be an optgroup element
                  recursivelyBuildList(dataGroupItemJQuery, childJQuery.children());
                }
              });
          }
          
          ///<summary>
          /// Creates an unordered list of values from the original Select control
          ///</summary>
          function buildDropDownList()
          {
            var originalElementChildrenJQuery = _originalElementJQuery.children();
            _lastItemSelectedJQuery = null;
            _lastValue = null;

            // If the Drop Down List container already exists, recreate only the items,
            // else create the container and the items as well.
            if (_dropDownListJQuery)
            {
              // Clear out any existing children elements
              _dropDownListJQuery.empty();
            }
            else
            {
              var dropDownHTML = "";
              if (styleSettings.comboboxDropDownClass)
              {
                dropDownHTML = String.format("<ul class='{0}' style='{1}'></ul>", styleSettings.comboboxDropDownClass, _dropDownListEnforcedStyle);
              }
              else
              {
                dropDownHTML = String.format("<ul style='{0}'></ul>", (_dropDownListEnforcedStyle + _dropDownListDefaultStyle));
              }
              
              _dropDownListJQuery = jQuery(dropDownHTML);
              // Create the equivalent of the drop down list where the all the values are shown
              _dropDownListJQuery.appendTo(_containerJQuery);
              
              // Enable the Drop Down List to be able to receive focus and key events
              _dropDownListJQuery.attr("tabIndex", 0);
            }
            
            // Create the internal list of values if they exist
            if (originalElementChildrenJQuery.length > 0)
            {
              _maximumItemLength = 0;
              _dropDownListOffset = _dropDownListJQuery.offset();
                
              recursivelyBuildList(_dropDownListJQuery, originalElementChildrenJQuery);
            }
            
            // Check if the max-height has been set as a CSS setting
            // If it has, determine if the current height of the dropdown list does not exceed it and if 
            // it does, reset the height to match the setting.
            var maximumHeight = getPixelValue(_dropDownListJQuery.css("max-height"));
                    
            // Only use the maximum height if it has been set correctly
            if (maximumHeight)
            {
              _dropdownListMaximumHeight = maximumHeight;
            }
            
            var dropdownListHeight = _dropDownListJQuery.height();
            if (dropdownListHeight > _dropdownListMaximumHeight)
            {
              _dropDownListJQuery.height(_dropdownListMaximumHeight);
            }
            
            // Store the height because the browser flashes (FF) when accessing this function
            _dropDownListHeight = _dropDownListJQuery.height();
          }
          
          ///<summary>
          /// Adjust the width of the DropDown list based on the widest item or the set width (options), whichever
          /// is larger.
          ///</summary>
          function updateDropDownListWidth()
          {
            //Drop down list element
            var dropdownListWidth = _containerJQuery.outerWidth();
            if (dropdownListWidth < _maximumItemLength)
            {
              dropdownListWidth = _maximumItemLength;
            }
            
            _dropDownListJQuery.width(dropdownListWidth);
          }
          
          ///<summary>
          /// Repositions the display value based on height of the element.
          /// Note: the height will only have meaning if the display value element has text
          ///</summary>
          function positionDisplayValue()
          {
            // Set the height to the default and allow it to fill the height to accomodate the content
            _valueContentJQuery.height("auto");
            var displayValueHeight = _valueContentJQuery.outerHeight();
            var displayContainerHeight = _valueContentContainerJQuery.height();
            
            // Check if the developer wants to clip the content within a region
            if (_valueContentMaximumHeight)
            {
              // Set the height of the content to the maximumContentHeight if it is less
              // than the current height of the content
              if (_valueContentMaximumHeight < displayValueHeight)
              {
                displayValueHeight = _valueContentMaximumHeight;
                _valueContentJQuery.height(displayValueHeight);
              }
            }
            
            var difference = ((displayContainerHeight - displayValueHeight) / 2);
            
            if (difference < 0)
            {
              difference = 0;
            }
            
            //TODO: add other alignments for the user, such as left, top, middle, bottom, etc
            _valueContentJQuery.css("top", difference);
          }
          
          ///<summary>
          /// Applies custom layout position and sizing to the controls
          ///</summary>
          function applyLayout()
          {
            _containerJQuery.width(optionSettings.width);
            
            // Removes any units and retrieves only the value of width
            var controlWidth = _containerJQuery.width();
            setInnerWidth(_valueContentContainerJQuery, controlWidth);
            
            var displayValueWidth = (_valueContentContainerJQuery.width() - _dropDownButtonJQuery.outerWidth());
            setInnerWidth(_valueContentJQuery, displayValueWidth);
            var dropDownButtonHeight = _dropDownButtonJQuery.outerHeight();
            setInnerHeight(_valueContentContainerJQuery, dropDownButtonHeight);
            
            _dropDownListJQuery.css("position", "absolute");
            _dropDownListJQuery.css("z-index", "20000");
            
            updateDropDownListWidth();
            
            // Position the drop down list correctly, taking the main display control border into consideration
            var currentLeftPosition = _dropDownListJQuery.offset().left;
            var leftPosition = (currentLeftPosition - (_containerJQuery.outerWidth() - _containerJQuery.width()));
            _dropDownListJQuery.css("left", leftPosition + 1);
            
            _dropDownListJQuery.hide();
          }

          ///<summary>
          ///   Sets the value both internally and visually to the user
          ///</summary>
          function setContentDisplay()
          {
            var valueHasChanged = false;
            var originalElement = _originalElementJQuery[0];
            var dataItemJQuery;
            
            if (originalElement.length > 0)
            {
              //var selectedText = originalElement[originalElement.selectedIndex].text;
              var selectedDropDownListItemJQuery = jQuery("li[dataValue='" + _originalElementJQuery.val() + "']", _dropDownListJQuery);
              
              _valueContentJQuery.html(selectedDropDownListItemJQuery[0].dataText);
              _valueContentJQuery.attr("title", selectedDropDownListItemJQuery[0].title);
              
              // Reposition the display value based on height of the element after the text has changed
              positionDisplayValue();
              
              if (_lastValue)
              {
                if (_lastValue != _originalElementJQuery.val())
                {
                  valueHasChanged = true;
                }
              }
              
              _lastValue = _originalElementJQuery.val();
              
              //  If the selected value has changed since the last click, fire the onChange event
              if (valueHasChanged)
              {
                // Check if the onChange event is being consumed, otherwise it will be undefined
                if (_context.combobox.onChange)
                {
                  _context.combobox.onChange(_context);
                }
              }
              
              // If _lastItemSelectedJQuery has been set, remove the highlight from it, before setting it to the current
              // value
              if (_lastItemSelectedJQuery)
              {
                toggleItemHighlight(_lastItemSelectedJQuery, false);
              }
              
              // Find the DropDown Item Element that corresponds to the current value in the Select element
              _lastItemSelectedJQuery = selectedDropDownListItemJQuery;
              
              toggleItemHighlight(_lastItemSelectedJQuery, true);
            }
          }
          
          ///<summary>
          /// Forces the a drop down list item to be visible on screen.
          /// This applies to containers that have scrollbars and elements within it
          /// are out of vision.
          /// Only scrolls an item into place if it not visible on screen.
          ///</summary>
          function scrollDropDownListItemIntoView(dropdownListItemJQuery)
          {
            //TODO: Not working correctly in IE.
            // Moving up does not immediately show the hidden item above
            if (dropdownListItemJQuery)
            {
              if (_dropDownListHeight >= _dropdownListMaximumHeight)
              {
                var offset = dropdownListItemJQuery.offset();

                // Only scroll if the item is below the height of the ddl
                // or above the top of it or the height of a DDL item
                if (
                    (offset.top > _dropDownListHeight)
                    ||
                    (offset.top <= dropdownListItemJQuery.outerHeight())
                   )
                {
                  //dropdownListItemJQuery[0].scrollIntoView();
                }
              }
            }     
          }
          
          ///<summary>
          /// Highlights/Unhighlights a specific option.
          /// If a class is not set, then the background and foreground colours are inverted
          ///</summary>
          function toggleItemHighlight(elementJQuery, isHighlighted)
          {
            if (elementJQuery)
            {
              if (styleSettings.comboboxDropDownItemHoverClass)
              {
                if (isHighlighted)
                {
                  elementJQuery.addClass(styleSettings.comboboxDropDownItemHoverClass);
                }
                else
                {
                  elementJQuery.removeClass(styleSettings.comboboxDropDownItemHoverClass);
                }
              }
              else
              {
                if (isHighlighted)
                {
                  elementJQuery.css("background", "#000");
                  elementJQuery.css("color", "#fff");
                }
                else
                {
                  elementJQuery.css("background", "");
                  elementJQuery.css("color", "");
                }
              }
            }
          }

          ///<summary>
          /// Builds the Outermost control and swaps out the original Select element.
          /// The Select element then becomes an hidden control within.
          ///</summary>
          function buildContainer()
          {
            var containerHTML = "";
            if (styleSettings.comboboxContainerClass)
            {
              containerHTML = String.format("<div class='{0}' style='{1}'></div>", styleSettings.comboboxContainerClass, _containerEnforcedStyle);
            }
            else
            {
              containerHTML = String.format("<div style='{0}' style='{1}'></div>", _containerDefaultStyle, _containerEnforcedStyle);
            }
            _containerJQuery = jQuery(containerHTML);
            _originalElementJQuery.before(_containerJQuery);
            _containerJQuery.append(_originalElementJQuery);
            _originalElementJQuery.hide();
            
            // Allow the custom jquery.combobox be able to receive focus and key events
            _containerJQuery.attr("tabIndex", 0);
          }
          
          ///<summary>
          /// Converts an existing Select element to a JQuery.combobox.
          /// The Select element is kept and updated accordingly, but visually is represented
          /// by other richer HTML elements
          ///</summary>
          this.initialise =
            function ()
            {
              buildContainer();
              
              buildValueContent();
              
              buildDropDownList();
              
              applyLayout();
              
              bindEvents();
              
              setContentDisplay();
            };
          
          ///<summary>
          /// Focus must be set to the DropDown list element only after it has shown.
          /// This is due to IE executing the Blur event before the list has immediately shown
          ///</summary>
          function postDropDownListShown()
          {
            _dropDownListJQuery.focus();
            scrollDropDownListItemIntoView(_lastItemSelectedJQuery);
          }

          ///<summary>
          /// Focus set to the Combobox Container
          ///</summary>
          function setAndBindContainerFocus()
          {
            _containerJQuery.focus();
            bindContainerClickEvent();
          }
          
          ///<summary>
          /// Slides up the DropDownlist when it is to be placed above the CB
          ///</summary>
          function slideUp(newTop)
          {
            _dropDownListJQuery.animate(
              {
                height: "toggle",
                top: newTop
              },
              optionSettings.animationSpeed,
              postDropDownListShown);
          }
          
          ///<summary>
          /// Slides closed the DropDownlist when it is placed above the CB.
          /// Binds the CB Container click event after the DDL is hidden to avoid a bug in IE
          /// where the click event fires re-opening the DDL.
          ///</summary>
          function slideDown(newTop)
          {
            _dropDownListJQuery.animate(
              {
                height: "toggle",
                opacity: "toggle",
                top: newTop
              },
              optionSettings.animationSpeed,
              setAndBindContainerFocus);
          }
          
          ///<summary>
          /// Toggles the slide with a fade and returning execution to the callback function when down
          ///</summary>
          function slideToggle(callback)
          {
            _dropDownListJQuery.animate(
              {
                height: "toggle",
                opacity: "toggle"
              },
              optionSettings.animationSpeed,
              callback);
          }
          
          ///<summary>
          /// Get the proposed top position of the drop down list container.
          /// Also sets whether the drop down list is inverted. Inverted means that the
          /// list is shown above the container as opposed to the normal position of below the combobox 
          /// container
          ///</summary>
          function getDropDownListTop()
          {
            var comboboxTop = _containerJQuery.position().top;
            var dropdownListHeight = _dropDownListJQuery.outerHeight();
            var comboboxBottom = (comboboxTop + _containerJQuery.outerHeight());
            var windowScrollTop = jQuery(window).scrollTop();
            var windowHeight = jQuery(window).height(); 
            var availableSpaceBelow = (windowHeight - (comboboxBottom - windowScrollTop));
            var dropdownListTop;

            // Set values to display dropdown list below combobox as default        
            dropdownListTop = comboboxBottom;
            _downdownListPositionIsInverted = false;

            // Check if there is enough space below to display the full height of the drop down list
            /*if (availableSpaceBelow < dropdownListHeight)
            {
              // There is no available space below the combobox to display the dropdown list
              // Check if there is available space above. If not, then display below as default
              if ((comboboxTop - windowScrollTop)> dropdownListHeight)
              {
                // There is space above
                dropdownListTop = (comboboxTop - dropdownListHeight);
                _downdownListPositionIsInverted = true;
              }
            }*/
            
            return dropdownListTop;
          }
          
          ///<summary>
          /// Hides/Shows the list of values.
          /// The method of display or hiding is specified as optionSettings.animationType.
          /// This method also changes the button state
          ///</summary>         
          function toggleDropDownList(isShown)
          {
            if (isShown)
            {
              if (_dropDownListJQuery.is(":hidden"))
              {
                // Remove the click event from the container because when the dropdown list is shown
                // and the container is clicked, the dropdownlist blur event is fired which hides the control
                // and the container click is fired after which will show the list again (error);
                unbindContainerClickEvent();
                
                // Remove the highlight from the last item hovered before the DDL was retracted
                toggleItemHighlight(_lastItemHoveredJQuery, false);
                
                // When the DropDown list is shown, highlist the current value in the list
                toggleItemHighlight(_lastItemSelectedJQuery, true);
        
                setControlVisualState(1);

                _dropDownListJQuery.parent().addClass("comboboxContainerOpen");
                var dropdownListTop = getDropDownListTop();
                _dropDownListJQuery.css("top", dropdownListTop);
                _dropDownListJQuery.css("left", _containerJQuery.offset().left);
                
                switch (optionSettings.animationType)
                {
                  case "slide":
                    if (_downdownListPositionIsInverted)
                    {
                      var comboboxTop = _containerJQuery.position().top;
                      var containerHeight = _containerJQuery.outerHeight();

                      _dropDownListJQuery.css("top", (comboboxTop - containerHeight));

                      slideUp(dropdownListTop);
                    }
                    else
                    {
                      slideToggle(postDropDownListShown);
                    }
                    break;
                    
                  case "fade":
                    _dropDownListJQuery.fadeIn(optionSettings.animationSpeed, postDropDownListShown);
                    break;
                    
                  default:
                    // Bug: if show() is used and postDropDownListShown() is immediately after,
                    // the focus hides the DropDownList. Show(1, xxx) uses a callback which seems to work
                    _dropDownListJQuery.show(1, postDropDownListShown);
                }
              }
            }
            else
            {
              if (_dropDownListJQuery.is(":visible"))
              {
                setControlVisualState(0);
                
                switch (optionSettings.animationType)
                {
                  case "slide":
                    if (_downdownListPositionIsInverted)
                    {
                      comboboxTop = _containerJQuery.position().top;
                      dropdownListHeight = _dropDownListJQuery.height();

                      slideDown(comboboxTop - _containerJQuery.outerHeight());
                    }
                    else
                    {
                      slideToggle(setAndBindContainerFocus);
                    }
                    break;
                    
                  case "fade":
                    _dropDownListJQuery.fadeOut(optionSettings.animationSpeed, setAndBindContainerFocus);
                    _dropDownListJQuery.parent().removeClass("comboboxContainerOpen");
                    break;
                    
                  default:
                    _dropDownListJQuery.hide();
                    setAndBindContainerFocus();
                }
              }
            }
          }
          
          ///<summary>
          /// Sets the internal select element (original) to match the visually changes made by the user.
          /// This ensures that any legacy code working with the original select is kept up to date with changes
          /// Either selectedIndex or selectedValue can be used, not both at the same time.
          ///</summary>
          function setOriginalSelectItem(selectedIndex, selectedValue)
          {
            var originalElementDOM = _originalElementJQuery[0];
            
            if (selectedValue == null)
            {
              originalElementDOM.selectedIndex = selectedIndex;
            }
            else
            {
              originalElementDOM.value = selectedValue;
            }
            
            // Fire the OnChange event for the original select element
            if (originalElementDOM.onchange)
            {
              originalElementDOM.onchange();
            }
            
            setContentDisplay();
          }

          ///<summary>
          /// Selects a value from the list of options from the original Select options.
          /// Does not use JQuery Selectors ':last' and ':first' because they take optgroup elements into
          /// account.
          ///</summary>         
          function selectValue(subSelector)
          {
            var originalElement = _originalElementJQuery[0];
            var currentIndex = originalElement.selectedIndex;
            var newIndex = -1;
            // {select}.length returns the array size of the options. Does not count optgroup elements
            var optionCountZeroBased = originalElement.length - 1;
            
            switch (subSelector)
            {
              case ":next":
                newIndex = currentIndex + 1;
                if (newIndex > optionCountZeroBased)
                {
                  newIndex = optionCountZeroBased;
                }
                break;
              
              case ":previous":
                newIndex = currentIndex - 1;
                if (newIndex < 0)
                {
                  newIndex = 0;
                }

                break;
                
              case ":first":
                newIndex = 0;
                
                break;
                
              case ":last":
                newIndex = optionCountZeroBased;
                
                break;
            }

            setOriginalSelectItem(newIndex, null);
            
            scrollDropDownListItemIntoView(_lastItemSelectedJQuery);
          }
          
          ///<summary>
          /// Returns true if the DropDownList visible on screen, else false
          ///</summary>
          function isDropDownVisible()
          {
            return _dropDownListJQuery.is(":visible");
          }
          
          ///<summary>
          /// Bind all items to mouse events except for UL elements
          /// and LI elements that are option group labels
          ///</summary>     
          function bindItemEvents()
          {
            jQuery("li", _dropDownListJQuery).not("ul").not("span").not("[dataType='optgroup']").each(
              function()
              {
                var itemJQuery = jQuery(this);
                itemJQuery.click(
                  function(clickEvent)
                  {
                    // Stops the click event propagating to the Container and the Container.onClick firing
                    clickEvent.stopPropagation();
                    
                    dropdownList_onItemClick(itemJQuery);
                  });
                
                itemJQuery.mouseover(
                  function()
                  {
                    dropdownList_onItemMouseOver(itemJQuery);
                  });
                  
                itemJQuery.mouseout(
                  function()
                  {
                    dropdownList_onItemMouseOut(itemJQuery);
                  });
              });     
          }

          ///<summary>
          ///   Bind the dropdown list control blur event to a function
          ///</summary>
          function bindBlurEvent()
          {
            _dropDownListJQuery.blur(
              function(blurEvent)
              {
                blurEvent.stopPropagation();
                
                dropdownList_onBlur();
              });
          }
          
          ///<summary>
          /// Bind the click event of the container to a function
          ///</summary>
          function bindContainerClickEvent()
          {
            _containerJQuery.click(
              function()
              {
                container_onClick();
              });
          }

          ///<summary>
          /// Remove the binding of a custom function from the container's click event
          ///</summary>
          function unbindContainerClickEvent()
          {
            _containerJQuery.unbind("click");
          }
                
          ///<summary>
          ///   Bind this control to the events to custom functions
          ///</summary>
          function bindEvents()
          {
            _containerJQuery.keydown(
              function(keyEvent)
              {
                keyEvent.preventDefault();
                container_onKeyDown(keyEvent)
              });
              
            bindContainerClickEvent();
              
            bindBlurEvent();
              
            bindItemEvents();
          }       
          
          //#end 'private' functions
          
          //#start private events
          
          ///<summary>
          /// If the drop down list is retracted, it is shown,
          /// else if shown, it is retracted
          ///</summary>
          function container_onClick()
          {
            if (_dropDownListJQuery.is(":hidden"))
            {
              toggleDropDownList(true);
            }
            else
            {
              toggleDropDownList(false);
            }
          }
          
          ///<summary>
          /// Fires when the drop down list loses focus.
          /// On Blur, the drop down list is retracted
          ///</summary>
          function dropdownList_onBlur()
          {
            if (_dropDownListJQuery.is(":visible"))
            {
              toggleDropDownList(false);
            }
          }
          
          ///<summary>
          /// Retrieves the value of the item clicked, sets the content to that value
          /// and retracts the drop-down list
          ///</summary>
          function dropdownList_onItemClick(itemJQuery)
          {
            setOriginalSelectItem(null, itemJQuery[0].dataValue); 
            
            toggleDropDownList(false);
          }
          
          ///<summary>
          /// Highlights the Drop Down List item currently under the mouse.
          /// Removes the highlist from the previous selected item as well.
          ///</summary>
          function dropdownList_onItemMouseOver(itemJQuery)
          {
            // An item may be selected from the previous selection and will require
            // to be set to normal.
            // TODO: find a better method of matching _lastItemSelectedJQuery to itemJQuery and optimising the removal
            // of the class, instead of removing it consistently
            toggleItemHighlight(_lastItemSelectedJQuery, false);
            
            toggleItemHighlight(_lastItemHoveredJQuery, false);
            
            toggleItemHighlight(itemJQuery, true);
          }
          
          ///<summary>
          ///   Removes the highlight from the selected item
          ///</summary>
          function dropdownList_onItemMouseOut(itemJQuery)
          {
            //toggleItemHighlight(itemJQuery, false);
            _lastItemHoveredJQuery = itemJQuery;
          }
          
          ///<summary>
          /// Handles the keyboard navigation aspect of the combobox.
          /// Note: Does not jump to item if the first letter is pressed.
          ///</summary>
          //TODO: Correctly support page-up and page-down, esp. with scrolling
          function container_onKeyDown(keyEvent)
          {
            switch (keyEvent.which)
            {
              case 33:
                //Page Up
              case 36:
                //Home
                selectValue(":first");
                break;
              
              case 34:
                //Page Down
              case 35:
                //End
                selectValue(":last");
                break;

              case 37:
                //Left
                selectValue(":previous");
                break;
                
              case 38:
                //Up
                if (keyEvent.altKey)
                {
                  // alt-up
                  // If DDL is hidden, then it is shown and vice-versa
                  toggleDropDownList(!(isDropDownVisible()));
                }
                else
                {
                  selectValue(":previous");
                }
                break;

              case 39:
                //Right
                selectValue(":next");
                break;
                
              case 40:
                //Down
                if (keyEvent.altKey)
                {
                  // alt-down
                  // If DDL is hidden, then it is shown and vice-versa
                  toggleDropDownList(!(isDropDownVisible()));
                }
                else
                {
                  selectValue(":next");
                }
                break;
                
              case 27:
              case 13:
                // Escape
                toggleDropDownList(false);
                break;

              case 9:
                // Tab
                //TODO: Support alt-tab
                //TODO: Does not truly leave the Combobox if the DropDown is visible
                _dropDownListJQuery.blur();
                
                // This is required in Internet Explorer as the blur() order is different
                jQuery(window)[0].focus();
                
                break;
            }
          }
          //#end private events
          
          //#start public methods
          
          ///<summary>
          /// Updates the combobox with the current selected item.
          /// This function is called if the original Select element selection has been changed
          ///</summary>
          this.updateSelection = 
            function()
            {
              setContentDisplay();
            };
            
          ///<summary>
          /// The Drop Down List Container will already have been created.
          /// This function recreates the items and binds the events to them.
          /// Thereafter, the current selection is set.
          ///</summary>
          this.update =
            function()
            {
              buildDropDownList();
              updateDropDownListWidth();
              bindItemEvents();
              setContentDisplay();
            };
            
          ///<summary>
          /// Removes the jquery.combobox leaving the original select element
          ///</summary>         
          this.remove =
            function()
            {
              //Move the original element to a position before the jquery.combobox      
              _containerJQuery.before(_originalElementJQuery);
              _containerJQuery.remove();
              
              // Remove the internal object property from the DOM
              //TODO: next statement does not work in Internet Explorer 6.
              //delete _originalElementJQuery[0].internalCombobox;
              _originalElementJQuery[0].internalCombobox = null;
              
              _originalElementJQuery.show();
            };
            
          ///<summary>
          /// Adds a range of options into the combobox.
          /// Using this function bypasses the browsers restriction of adding
          /// html as text values. This allows customisation of the display text
          /// Format of dataSource
          /// [
          ///   {
          ///     value: object, // usual a unique string value
          ///     text: object,  // can be normal text or html
          ///     title: string  // optional
          ///   }
          /// ]
          /// Note: Still in development
          ///</summary>
          this.addRange =
            function(dataSource)
            {
              if (dataSource)
              {
                var originalOptions = _originalElementJQuery[0].options;
                var optionTotal = originalOptions.length;
                for (optionIndex in dataSource)
                {
                  var option = dataSource[optionIndex];
                  var optionElement = document.createElement("option");
                  optionElement.value = option.value;
                  optionElement.text = option.text;
                  // Store the raw text data. Option.text removes all HTML content
                  optionElement.dataText = option.text;
                  if (option.title)
                  {
                    optionElement.title = option.title;
                  }
                  
                  originalOptions[optionTotal + optionIndex] = optionElement;
                }
                
                _originalElementJQuery.combobox.update();
              }
            };
          
          //#end public methods
          
        }
      });
  }
/*
TODOS:
- look to moving functions to outside the context and use a state based object to track individual elements [0]
*/
