/****************************************************************************************************************************
 *                                                                                                                          *
 *  GENERAL ELEMENTS AND EXTENSIONS                                                                                         *
 *                                                                                                                          *
 ****************************************************************************************************************************/


/****************************************************************************************************************************
 * String functions
 */

/* Return the length of the string in bytes when UTF8-encoded */
String.implement(
{
  getUTF8Length: function()
  {
    return unescape(encodeURIComponent(this)).length;
  }
});


/****************************************************************************************************************************
 * Input placeholder
 */

/* To set the placeholder text (browser native placeholder will be used if available):
   $('input').set("placeholder", "Insert text here");

   After placeholder text is set, use get("value") and set("value") to retrieve and update the input's value.

   Use the ".-placeholder" CSS class to style the custom placeholder, or a vendor-specific CSS rules like
   ::-webkit-input-placeholder or :-moz-placeholder (Opera doesn't support customization of the placeholder yet)

   Unsupported browsers (I'm looking at you, Internet Explorer) might reapply the input's value after a refresh, which isn't
   picked up by this code (it cannot differentiate between an initial HTML value and the placeholder value applied to the
   input by the browser), so the placeholder will show up as the input's normal value. To detect this, we'll prefix the
   placeholder text with a zero-width space character, so we can recognize it after a refresh.

   ADDME: Support placeholder text for password fields (switch type to display placeholder text instead of stars)
*/
Element.Properties.placeholder =
  { get: function()
    {
      // If this browser supports the placeholder attribute, use that
      if ("placeholder" in this)
        return this.placeholder;

      // If the placeholder data is defined, return it, otherwise return nothing
      if (this.$placeholderdata)
        return this.$placeholderdata.text;
    }
  , set: function(placeholdertext)
    {
      // If this browser supports the placeholder attribute, use that
      if ("placeholder" in this)
      {
        this.placeholder = placeholdertext;
        return this;
      }

      // Check if this element is an input or textarea element with an appropriate type
      // The placeholder attribute is only supported for inputs with types "text", "search", "url", "tel", "email" and
      // "password". Unknown types are treated as "text", so we'll have to check for all known, unsupported types.
      if (this.get("tag") != "input" && this.get("tag") != "textarea")
        return this;
      else if ([ "hidden", "datetime", "date", "month", "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", "file", "submit", "image", "reset", "button" ].indexOf(this.type) >= 0)
        return this;

      // Check for a valid value
      if (typeOf(placeholdertext) == "null")
        placeholdertext = "";
      else if (typeOf(placeholdertext) != "string")
        return this;
      else
        placeholdertext = placeholdertext.split("\x0A").join("").split("\x0D").join(""); // Strip linefeeds from the placeholdertext

      // If the new placeholdertext is empty, remove placeholder data
      if (!placeholdertext)
      {
        if (this.$placeholderdata)
        {
          // Reset browser native placeholder text or call the onfocus handler to reset the value
          if ("placeholder" in this)
            this.placeholder = "";
          else
          {
            this.$placeholderdata.onfocus();

            // Remove the focus and blur events
            this.removeEvents({ focus: this.$placeholderdata.onfocus
                              , blur: this.$placeholderdata.onblur
                              });
          }
          // Clear the placeholder data
          this.$placeholderdata = null;
        }
        return this;
      }

      // Placeholder data object
      this.$placeholderdata = { text: "\u200B" + placeholdertext
                              , active: this.value == ""
                              , onfocus: (function()
                                          {
                                            if (!("placeholder" in this) && this.$placeholderdata && this.$placeholderdata.active)
                                            {
                                              // Focusing when placeholder text was active, reset value
                                              this.value = "";
                                              // Remove our placeholder class
                                              this.removeClass("-placeholder");
                                            }
                                          }).bind(this)
                              , onblur: (function()
                                         {
                                           if (!("placeholder" in this) && this.$placeholderdata)
                                           {
                                             // Placeholder text is active if there is no text entered
                                             this.$placeholderdata.active = this.value == "";
                                             if (this.$placeholderdata.active)
                                             {
                                               // Set the value to the placeholder text
                                               this.value = this.$placeholderdata.text;
                                               // Add our placeholder class
                                               this.addClass("-placeholder");
                                             }
                                             else
                                               // Remove our placeholder class
                                               this.removeClass("-placeholder");
                                           }
                                         }).bind(this)
                              };

      // Add focus and blur events to update the input's value
      this.addEvents({ focus: this.$placeholderdata.onfocus
                     , blur: this.$placeholderdata.onblur
                     });

      // Call onblur once to initialize
      if (this.value.length > 0 && this.value.substr(0, 1) == "\u200B")
        this.value = "";
      this.$placeholderdata.onblur();

      return this;
    }
  };

Element.Properties.value =
  { get: function()
    {
      // If the placeholder is active, return an empty string, otherwise return the value
      return this.$placeholderdata && this.$placeholderdata.active ? "" : this.value;
    }
  , set: function(value)
    {
      // Update the value and call onblur to update the placeholder state
      this.value = value;
      if (this.$placeholderdata)
        this.$placeholderdata.onblur();

      return this;
    }
  };


/****************************************************************************************************************************
 * Checkbox
 */

/* A custom checkbox, implemented using a single image.
   The supplied width and height define the dimensions of the checkbox. The checkbox is rendered as an inline block.
   The image is assumed to have the dimensions width x (4 * height), with the following states from top to bottom: normal
   (unchecked), checked, disabled (unchecked), checked disabled.
   The checkbox fires the following events (listen for them by supplying the handlers in the options):
   onChange - called when the value of the checkbox has changed
*/
WHCheckbox = new Class(
  { Implements: [ Options, Events ]

  /* Initialization */

  , options: { imgsrc: ""     // Checkbox image src url
             , imgwidth: 0    // Checkbox image width
             , imgheight: 0   // Checkbox image height
             , classname: ""  // Classname to apply
             , styles: {}     // Additional CSS styles to apply
             , checked: false // If the checkbox is initially checked
             , enabled: true  // If the checkbox is initially enabled
             , value: ""      // Checkbox value (used to set the hidden input's value)
             , input: null    // The (hidden) input which value should mirror the checkbox's state (id or node)
             }

  , active: false // Mousedown was activated on the control (toggle on mouseup)

  , initialize: function(options)
    {
      this.setOptions(options);
      this.buildNode();
      this.updateState();
    }

  /* Public API */

    /* Toggle the checked status */
  , toggle: function()
    {
      if (this.getEnabled())
        this.setChecked(!this.getChecked(), true);
    }

    /* Get the value */
  , getValue: function()
    {
      return this.options.value;
    }

    /* Set the value */
  , setValue: function(value, withevent)
    {
      this.setOptions({ value: value });
      this.updateState();
    }

    /* Get the checked value */
  , getChecked: function()
    {
      return this.options.checked;
    }

    /* Set the checked value */
  , setChecked: function(checked, withevent)
    {
      if (checked != this.options.checked)
      {
        this.setOptions({ checked: checked });
        this.updateState();
        if (withevent)
          this.fireEvent("change");
      }
    }

    /* Get the enabled state of the checkbox */
  , getEnabled: function()
    {
      return this.options.enabled;
    }

    /* Set the enabled state of the checkbox */
  , setEnabled: function(enabled)
    {
      if (enabled != this.options.enabled)
      {
        this.setOptions({ enabled: enabled });
        this.updateState();
      }
    }

  /* DOM */

  , buildNode: function()
    {
      this.node = new Element("a", { href: "#"
                                   , events: { keydown: this.onKeyDown.bind(this)
                                             , click: function(e) { return e.stop(); } // Don't follow the link
                                             }
                                   , "class": this.options.classname
                                   });
      if ("createTouch" in document)
      {
        this.node.addEvents({ touchstart: this.onMouseDown.bind(this) });
        $(document).addEvents({ touchend: this.onMouseUp.bind(this) });
      }
      else
      {
        this.node.addEvents({ mousedown: this.onMouseDown.bind(this) });
        $(document).addEvents({ mouseup: this.onMouseUp.bind(this) });
      }
      this.img = new WHCroppedImage({ src: this.options.imgsrc
                                    , width: this.options.imgwidth
                                    , height: this.options.imgheight
                                    , imgwidth: this.options.imgwidth
                                    , imgheight: 4 * this.options.imgheight
                                    , left: 0
                                    , node: this.node
                                    });
      this.node.setStyles(this.options.styles);

      // Update when our (replaced) parent input has changed
      var input = $(this.options.input);
      if (input)
      {
        var self = this;
        input.addEvent("change", function() { self.setChecked(this.checked); });
      }
    }

  , toElement: function()
    {
      return this.node;
    }

  , updateState: function()
    {
      var top = 0;
      if (this.getChecked())
        top += this.options.imgheight;
      if (!this.getEnabled())
        top += (2 * this.options.imgheight);
      this.img.setCrop({ top: top });
      this.node.setStyle("cursor", this.getEnabled() ? "pointer" : "default");

      var input = $(this.options.input);
      if (input)
      {
        if (input.get("type") == "checkbox")
          input.checked = this.options.checked;
        else
          input.value = this.options.checked ? this.options.value : "";
      }
    }

  /* Event handlers */

  , onMouseDown: function(event)
    {
      if (this.getEnabled())
      {
        this.active = true;
        try
        {
          this.node.focus();
        }
        catch (e) {}
      }
      return event.stop();
    }

  , onMouseUp: function(event)
    {
      if (this.active)
      {
        this.active = false;
        if (this.getEnabled())
        {
          var node = event.target;
          while (node && node != this.node)
            node = node.parentNode;
          if (node)
            this.toggle();
          return event.stop();
        }
      }
    }

  , onKeyDown: function(event)
    {
      if (this.getEnabled())
      {
        if (event.key == "space" || event.key == "enter")
        {
          this.toggle();
          return event.stop();
        }
      }
    }
  });


/****************************************************************************************************************************
 * Radiobutton
 */

/* A custom radiobutton, implemented using a single image.
   The supplied width and height define the dimensions of the checkbox. The checkbox is rendered as an inline block.
   The image is assumed to have the dimensions width x (4 * height), with the following states from top to bottom: normal
   (unchecked), checked, disabled (unchecked), checked disabled.
   An optional radiogroup can be supplied, so that from all radiobuttons within the group, only one can be selected.
   The radiobutton fires the following events (listen for them by supplying the handlers in the options):
   onChange - called when the value of the radiobutton has changed
*/
WHRadiobutton = new Class(
  { Implements: [ Options, Events ]

  /* Initialization */

  , options: { imgsrc: ""     // Radiobutton image src url
             , imgwidth: 0    // Radiobutton image width
             , imgheight: 0   // Radiobutton image height
             , classname: ""  // Classname to apply
             , styles: {}     // Additional CSS styles to apply
             , checked: false // If the radiobutton is initially selected
             , enabled: true  // If the radiobutton is initially enabled
             , value: ""      // Radiobutton value
             , input: null    // The (hidden) input which value should mirror the radiobutton's state (id or node)
             , radiogroup: "" // The name of the radiogroup this button belongs to (global per page)
             }

  , initialize: function(options)
    {
      this.setOptions(options);

      if (this.options.radiogroup)
        WHRadiobutton.RegisterRadiobuttonToGroup(this, this.options.radiogroup);

      this.buildNode();
      this.updateState();
    }

  /* Public API */

  , toggle: function()
    {
      if (this.getEnabled())
      {
        if (this.options.radiogroup)
          WHRadiobutton.SetRadiobuttonInGroup(this, this.options.radiogroup, true);
        else
          this.setCheckedInternal(true, true);
      }
    }

    /* Get the value */
  , getValue: function()
    {
      return this.options.value;
    }

    /* Set the value */
  , setValue: function(value, withevent)
    {
      this.setOptions({ value: value });
      if (this.options.radiogroup && checked)
        WHRadiobutton.SetRadiobuttonValueGroup(this, this.options.radiogroup);
    }

    /* Get the checked value */
  , getChecked: function()
    {
      return this.options.checked;
    }

    /* Set the checked value
       If this radiobutton belongs to a radiogroup, any other radiobutton within that group will be unchecked. */
  , setChecked: function(checked)
    {
      if (checked != this.options.checked)
      {
        if (this.options.radiogroup && checked)
          WHRadiobutton.SetRadiobuttonInGroup(this, this.options.radiogroup, false);
        else
          this.setCheckedInternal(checked, false);
      }
    }

    /* Get the enabled state of the checkbox */
  , getEnabled: function()
    {
      return this.options.enabled;
    }

    /* Set the enabled state of the checkbox */
  , setEnabled: function(enabled)
    {
      if (enabled != this.options.enabled)
      {
        this.setOptions({ enabled: enabled });
        this.updateState();
      }
    }

  /* DOM */

  , buildNode: function()
    {
      this.node = new Element("a", { href: "#"
                                   , events: { keydown: this.onKeyDown.bind(this)
                                             , click: function(e) { return e.stop(); } // Don't follow the link
                                             }
                                   , "class": this.options.classname
                                   });
      if ("createTouch" in document)
      {
        this.node.addEvents({ touchstart: this.onMouseDown.bind(this) });
        $(document).addEvents({ touchend: this.onMouseUp.bind(this) });
      }
      else
      {
        this.node.addEvents({ mousedown: this.onMouseDown.bind(this) });
        $(document).addEvents({ mouseup: this.onMouseUp.bind(this) });
      }
      this.img = new WHCroppedImage({ src: this.options.imgsrc
                                    , width: this.options.imgwidth
                                    , height: this.options.imgheight
                                    , imgwidth: this.options.imgwidth
                                    , imgheight: 4 * this.options.imgheight
                                    , left: 0
                                    , node: this.node
                                    });
      this.node.setStyles(this.options.styles);

      // Update when our (replaced) parent input has changed
      var input = $(this.options.input);
      if (input)
      {
        var self = this;
        input.addEvent("change", function() { self.setChecked(this.checked); });
      }
    }

  , toElement: function()
    {
      return this.node;
    }

  , updateState: function()
    {
      var top = 0;
      if (this.getChecked())
        top += this.options.imgheight;
      if (!this.getEnabled())
        top += (2 * this.options.imgheight);
      this.img.setCrop({ top: top });
      this.node.setStyle("cursor", this.getEnabled() ? "pointer" : "default");

      if (this.getChecked())
      {
        if (this.options.radiogroup)
          WHRadiobutton.SetRadiobuttonValueInGroup(this, this.options.radiogroup);

        var input = $(this.options.input);
        if (input)
        {
          if (input.get("type") == "radio")
            input.checked = true;
          else
            input.value = this.options.value;
        }
      }
    }

  /* Event handlers */

  , onMouseDown: function(event)
    {
      if (this.getEnabled())
      {
        this.active = true;
        try
        {
          this.node.focus();
        }
        catch (e) {}
      }
      return event.stop();
    }

  , onMouseUp: function(event)
    {
      if (this.active)
      {
        this.active = false;
        if (this.getEnabled())
        {
          var node = event.target;
          while (node && node != this.node)
            node = node.parentNode;
          if (node)
            this.toggle();
          return event.stop();
        }
      }
    }

  , onKeyDown: function(event)
    {
      if (this.getEnabled())
      {
        if (event.key == "space" || event.key == "enter")
        {
          this.toggle();
          return event.stop();
        }
      }
    }

  /* Internal functions */

  , setCheckedInternal: function(checked, withevent)
    {
      if (checked != this.options.checked)
      {
        this.setOptions({ checked: checked });
        this.updateState();
        if (withevent)
          this.fireEvent("change");
        return true;
      }
      return false;
    }
  });

// Store which holds the radiogroups
WHRadiobutton.radiogroups = {};

/* Register a radiobutton object with the given group */
WHRadiobutton.RegisterRadiobuttonToGroup = function(radiobutton, radiogroup)
{
  if (!WHRadiobutton.radiogroups[radiogroup])
    WHRadiobutton.radiogroups[radiogroup] = { radiobuttons: []
                                            , onChange: null
                                            , input: null
                                            };

  WHRadiobutton.radiogroups[radiogroup].radiobuttons.include(radiobutton);
}

/* Define an onChange handler for the given group, which is called once if a radiobutton within the group is checked, with the
   checked radiobutton as argument */
WHRadiobutton.SetRadiogroupOnChangeHandler = function(radiogroup, handler)
{
  if (!WHRadiobutton.radiogroups[radiogroup])
    WHRadiobutton.radiogroups[radiogroup] = { radiobuttons: []
                                            , onChange: handler
                                            , input: null
                                            };
  else
    WHRadiobutton.radiogroups[radiogroup].onChange = handler;
}

/* Set the (hidden) input which value should mirror the radiogroup's value (id or node) */
WHRadiobutton.SetRadiogroupInput = function(radiogroup, input)
{
  if (!WHRadiobutton.radiogroups[radiogroup])
    WHRadiobutton.radiogroups[radiogroup] = { radiobuttons: []
                                            , onChange: null
                                            , input: input
                                            };
  else
    WHRadiobutton.radiogroups[radiogroup].input = input;
}
WHRadiobutton.GetRadiogroupInput = function(radiogroup)
{
  if (WHRadiobutton.radiogroups[radiogroup])
    return WHRadiobutton.radiogroups[radiogroup].input;
}

/* Check a radiobutton within the given group */
WHRadiobutton.SetRadiobuttonInGroup = function(radiobutton, radiogroup, withevent)
{
  if (!WHRadiobutton.radiogroups[radiogroup] || !WHRadiobutton.radiogroups[radiogroup].radiobuttons.contains(radiobutton))
    return;

  WHRadiobutton.radiogroups[radiogroup].radiobuttons.each(function(groupbutton)
  {
    if (groupbutton != radiobutton)
      groupbutton.setCheckedInternal(false, withevent);
  });
  if (radiobutton.setCheckedInternal(true, withevent))
  {
    if (withevent && WHRadiobutton.radiogroups[radiogroup].onChange)
      WHRadiobutton.radiogroups[radiogroup].onChange(radiobutton);
  }
}

WHRadiobutton.SetRadiobuttonValueInGroup = function(radiobutton, radiogroup)
{
  if (!WHRadiobutton.radiogroups[radiogroup] || !WHRadiobutton.radiogroups[radiogroup].radiobuttons.contains(radiobutton))
    return;
  var input = $(WHRadiobutton.radiogroups[radiogroup].input);
  if (input)
    input.value = radiobutton.options.value;
}


/****************************************************************************************************************************
 * Pulldown
 */

/* A custom pulldown.
   The pulldown fires the following events (listen for them by supplying the handlers in the options):
   onChange - called when the value of the pulldown has changed
   onFocus - called when the pulldown receives the focus
   onBlur - called when the pulldown loses the focus
*/
WHPulldown = new Class(
  { Implements: [ Options, Events ]

  /* Initialization */

  , options: { width: 0            // Pulldown width (leave 0 for auto)
             , height: 0           // Pulldown height (leave 0 for auto)
             , imgsrc: ""          // Pulldown arrow image src url
             , imgwidth: 0         // Pulldown arrow image width
             , imgheight: 0        // Pulldown arrow image height
             , classname: ""       // Classname to apply
             , styles: {}          // Additional CSS styles to apply
             , enabled: true       // If the pulldown is initially enabled
             , values: []          // { value, title?, selected? } options
             , valuespopup: false  // If the options should popup (instead of pulldown)
             , valuesbehind: false // If the options should be displayed behind the value node
             , input: null         // The (hidden) input which value should mirror the select's value (id or node)
             }

  , focused: false
  , valuesvisible: false
  , selectedvalue: null
  , beforedeactivate: false

  , pagesize: 1 // Number of items to skip when using Page Up/Page Down

  , initialize: function(options)
    {
      this.setOptions(options);

      this.buildNode();
      this.updateState();
    }

  /* Public API */

  , toggle: function()
    {
      //ADDME: What to do? For now, we'll just activate the pulldown...
      if (this.getEnabled())
      {
        try
        {
          this.onMouseDown();
        }
        catch(e) {}
      }
    }

    /* Get the value */
  , getValue: function()
    {
      var selected = this.getSelectedValue();
      return selected ? selected.value : null;
    }

    /* Set the value */
  , setValue: function(newvalue, withevent)
    {
      if (newvalue != this.getValue())
      {
        this.options.values.each(function(value)
        {
          value.selected = value.value == newvalue;
        }, this);
        this.updateState();
        if (withevent)
          this.fireEvent("change");
      }
    }

    /* Get the enabled state of the checkbox */
  , getEnabled: function()
    {
      return this.options.enabled;
    }

    /* Set the enabled state of the checkbox */
  , setEnabled: function(enabled)
    {
      if (enabled != this.options.enabled)
      {
        this.setOptions({ enabled: enabled });
        this.updateState();
      }
    }

  /* DOM */

  , buildNode: function()
    {
      this.node = new Element("div", { "class": this.options.classname });

      this.linknode = new Element("a", { href: "#" // Necessary to be recognized as hyperlink in Firefox
                                       , events: { keydown: this.onKeyDown.bind(this)
                                                 , focus: this.onFocus.bind(this)
                                                 , blur: this.onBlur.bind(this)
                                                 , click: function(e) { return e.stop(); } // Don't follow the link
                                                 }
                                       }).inject(this.node);
      if ("createTouch" in document)
      {
        this.linknode.addEvents({ touchstart: this.onMouseDown.bind(this)
                                , touchmove: function(e) { return e.stop(); }
                                });
      }
      else
      {
        this.linknode.addEvents({ mousedown: this.onMouseDown.bind(this)
                                , mouseenter: this.onMouseEnter.bind(this)
                                , mousemove: function(e) { return e.stop(); }
                                });
      }
      // To prevent IE from blurring the <a> when clicking it (or a value within it), we'll register the (IE-only)
      // beforedeactivate event, which can be cancelled to prevent a blur on the element. The mousedown event handlers set
      // a preventblur flag which is used to check if the event should be cancelled.
      this.linknode.setAttribute("onbeforedeactivate", "return;");
      this.beforedeactivate = typeOf(this.linknode.onbeforedeactivate) == "function";
      if (this.beforedeactivate)
        this.linknode.onbeforedeactivate = this.onBeforeDeactivate.bind(this);
      else
        this.linknode.removeAttribute("onbeforedeactivate");

      this.node.style.cssText = "display: inline-block; zoom: 1; *display: inline"; // "inline-block" workaround for IE7
      this.node.setStyles({ position: "relative"
                          });
      if (this.options.width)
        this.node.setStyle("width", this.options.width);
      if (this.options.height)
        this.node.setStyle("height", this.options.height);

      this.linknode.style.cssText = "display: inline-block; zoom: 1; *display: inline"; // "inline-block" workaround for IE7
      this.linknode.setStyles({ position: "relative"
                              , "vertical-align": "baseline"
                              , "text-decoration": "none"
                              , "z-index": 2
                              //, border: "1px solid #999999"
                              });
      this.linknode.setStyles(this.options.styles);

      this.valuenode = new Element("div", { styles: { "float": "left"
                                                    }
                                          , "class": "value-selected"
                                          }).inject(this.linknode);

      this.imgnode = new Element("img", { src: this.options.imgsrc
                                        , width: this.options.imgwidth
                                        , height: this.options.imgheight
                                        , styles: { "vertical-align": "middle"
                                                  , border: "none"
                                                  }
                                        }).inject(this.linknode);

      this.valuesnode = new Element("div", { styles: { position: "absolute"
                                                     , left: 0
                                                     , display: "none"
                                                     , "white-space": "nowrap"
                                                     , "outline-style": "none"
                                                     , "z-index": this.options.valuesbehind ? 1 : 3
                                                     }
                                           , "class": "values"
                                           }).inject(this.node);
      this.options.values.each(function(value, idx)
      {
        value.idx = idx;
        value.node = new Element("div", { html: value.title ? value.title : value.value
                                        , "data-value": value.value
                                        , "class": "value"
                                        }).inject(this.valuesnode);
        if ("createTouch" in document)
        {
          value.node.addEvents({ touchstart: this.onValueMouseDown.bind(this)
                               , touchend: this.onValueMouseUp.bind(this)
                               , touchmove: this.onValueMouseOver.bind(this)
                               });
        }
        else
        {
          value.node.addEvents({ mousedown: this.onValueMouseDown.bind(this)
                               , mouseup: this.onValueMouseUp.bind(this)
                               , mouseover: this.onValueMouseOver.bind(this)
                               });
        }
      }, this);

      // Update when our (replaced) parent input has changed
      var input = $(this.options.input);
      if (input)
      {
        var self = this;
        input.addEvent("change", function() { self.setChecked(this.checked); });
      }

      this.updateState();
    }

  , toElement: function()
    {
      return this.node;
    }

  , updateState: function()
    {
      this.linknode.setStyles({ cursor: this.getEnabled() ? "pointer" : "default"
                              , "background-color": this.options.enabled ? "" : "#cccccc"
                              , "color": this.options.enabled ? "#000000" : "#666666"
                              });

      var selected = this.getSelectedValue();
      this.valuenode.set("html", selected ? (selected.title ? selected.title : selected.value) : "<!--empty-->");
      var input = $(this.options.input);
      if (input)
        input.value = selected ? selected.value : null;
    }

  /* Event handlers */

  , onFocus: function(event)
    {
      if (!this.focused)
      {
        // Yes, we're focused now
        this.focused = true;
        this.fireEvent("focus");
      }
    }

    // Only called in IE
  , onBeforeDeactivate: function()
    {
      // If the preventblur flag is set, the <a> should not be blurred (an option or the pulldown itself was clicked)
      if (this.preventblur)
      {
        this.preventblur = false;
        // Because we're not specifiying event as a function argument, we can directly use the global IE variable event
        event.cancelBubble = true;
        return false;
      }
    }

  , onBlur: function(event)
    {
      if (this.focused)
      {
        // We're no longer focused, hide the options
        this.focused = false;
        this.setValuesDisplay(false);
        this.fireEvent("blur");
      }
    }

  , onKeyDown: function(event)
    {
      if (!this.focused || !this.getEnabled())
        return;

      var nextidx = -1;
      var setvalue = !this.valuesvisible;
      switch (event.key)
      {
        case "up":
          if (this.valuesvisible)
          {
            // Select previous item
            nextidx = this.selectedvalue ? this.selectedvalue.idx - 1: this.options.values.length - 1;
          }
          else
          {
            // Set previous value
            var selected = this.getSelectedValue();
            nextidx = selected ? selected.idx - 1: this.options.values.length - 1;
          }
          break;

        case "down":
          if (this.valuesvisible)
          {
            // Select next item
            nextidx = this.selectedvalue ? this.selectedvalue.idx + 1: 0;
          }
          else
          {
            // Set next value
            var selected = this.getSelectedValue();
            nextidx = selected ? selected.idx + 1: 0;
          }
          break;

        case "enter":
          if (this.valuesvisible)
          {
            // Set the currently selected option as new value
            this.setValue(this.selectedvalue.value, true);
            this.setValuesDisplay(false);
            return event.stop();
          }
          break;

        case "esc":
          this.setValuesDisplay(false);
          break;

        default:
          switch (event.code)
          {
            case 33: // Page Up
              if (this.valuesvisible)
              {
                // Select previous item
                nextidx = this.selectedvalue ? Math.max(this.selectedvalue.idx - this.pagesize, 0) : this.options.values.length - 1;
              }
              else
              {
                // Set previous value
                var selected = this.getSelectedValue();
                nextidx = selected ? Math.max(selected.idx - this.pagesize, 0) : this.options.values.length - 1;
              }
              break;

            case 34: // Page Down
              if (this.valuesvisible)
              {
                // Select next item
                nextidx = this.selectedvalue ? Math.min(this.selectedvalue.idx + this.pagesize, this.options.values.length - 1) : 0;
              }
              else
              {
                // Set next value
                var selected = this.getSelectedValue();
                nextidx = selected ? Math.min(selected.idx + this.pagesize, this.options.values.length - 1) : 0;
              }
              break;

            case 36: // Home
              // Select first item/set first value
              nextidx = 0;
              break;

            case 35: // End
              // Select last item/set last value
              nextidx = this.options.values.length - 1;
              break;

            default:
              //ADDME: Jump to typed text?
          }
      }

      // If we have a new valid idx, select it/set new value
      if (nextidx >= 0 && nextidx < this.options.values.length)
      {
        if (setvalue)
          this.setValue(this.options.values[nextidx].value, true);
        else
        {
          this.selectedvalue = this.options.values[nextidx];
          this.showSelectedValue();
        }
      }
    }

  , onMouseDown: function(event)
    {
      if (this.getEnabled())
      {
        if (!this.focused)
        {
          try
          {
            this.linknode.focus();
          }
          catch (e) {}
        }
        // Prevent closing the values directly if values are positioned before the pulldown
        this.preventclose = this.options.valuespopup && !this.options.valuesbehind;
        this.setValuesDisplay();
        if (this.beforedeactivate)
          this.preventblur = true;
      }
      return event ? event.stop() : null;
    }

  , onMouseEnter: function(event)
    {
      if (this.valuesvisible)
      {
        this.selectedvalue = null;
        this.showSelectedValue();
      }
      return event.stop();
    }

  , onValueMouseDown: function(event)
    {
      this.preventclose = false;
      this.preventblur = true;
      return event.stop();
    }

  , onValueMouseUp: function(event)
    {
      var value = this.getEventValue(event);
      if (value)
      {
        // Don't hide the values if the values are positioned before the pulldown and the value wasn't changed
        if (this.preventclose && value.value == this.getValue())
          return event.stop();
        this.setValue(value.value, true);
      }
      this.setValuesDisplay(false);
    }

  , onValueMouseOver: function(event)
    {
      var value = this.getEventValue(event);
      // If hovered over another value, the values may be closed
      if (this.preventclose && (!value || value.value != this.getValue()))
        this.preventclose = false;
      this.selectedvalue = value;
      this.showSelectedValue();
    }

  /* Internal functions */

  , setValuesDisplay: function(show)
    {
      if (typeOf(show) == "null")
        show = !this.valuesvisible;
      this.valuesvisible = show;

      var top = 0;
      if (this.valuesvisible)
      {
        if (this.options.valuespopup)
        {
          // Get the position of the node holding the selected value
          var selected = this.getSelectedValue();
          this.valuesnode.setStyles({ display: "block", visibility: "hidden" });
          top = -selected.node.offsetTop - this.valuesnode.getStyle("border-top-width").toInt();
          this.valuesnode.setStyles({ display: "none", visibility: "" });
        }
        else
          top = this.node.getSize().y - 1; // Make borders collapse
      }

      this.valuesnode.setStyles({ display: this.valuesvisible ? "block" : "none"
                                , top: top
                                });
      if (this.valuesvisible)
      {
        // If we don't have an active option yet, make the selected value active
        if (!(this.options.valuespopup && this.options.valuesbehind) && !this.selectedvalue)
          this.selectedvalue = this.getSelectedValue();
        else
          this.selectedvalue = null;
        this.showSelectedValue();
      }
      else
      {
        // reset
        this.selectedvalue = null;
      }
    }

  , showSelectedValue: function()
    {
      this.options.values.each(function(value)
      {
        value.node.toggleClass("value-hovered", value == this.selectedvalue);
      }, this);
    }

  , getSelectedValue: function()
    {
      var selected = this.options.values.filter(function(value) { return value.selected; }).pick();
      if (selected)
        return selected;
      return this.options.values.length ? this.options.values[0] : null;
    }

  , getEventValue: function(event)
    {
      var node = $(event.target);
      while (node && !node.get("data-value"))
        node = $(node.parentNode);
      return this.options.values.filter(function(value) { return value.node == node; }).pick();
    }
  });


/****************************************************************************************************************************
 * Scrollbar
 */

/* Add custom scrollbars and scrolling to an element.
   If the element to scroll is initially hidden, the size can only be determined if MooTools More is loaded with at least the
   Element.Measure component included.
   The scrollbars fire the following events (listen for them by supplying the handlers in the options):
   onChange - called when the user stopped scrolling
   Dragging with momentum inspired by Ben Lenarts' Drag.Flick: http://mootools.net/forge/p/drag_flick
*/
//ADDME: Better documentation on events etc.
WHScrollbars = new Class(
  { Implements: [ Options, Events ]

  /* Initialization */

  , options: { v_class: ""             // Vertical pulldown CSS class name
             , v_class_slider: ""      // Vertical pulldown slider CSS class name
             , v_imgsrc: ""            // Vertical pulldown image src url
             , v_imgwidth: 0           // Vertical pulldown image width
             , v_imgheights: [ 0, 0, 0, 0, 0 ] // Vertical pulldown cumulative image heights (arrow up, arrow down, slider gripper, slider top, slider bottom)
             , v_imgsrc_background: "" // Vertical pulldown background image
             , v_imgsrc_slider: ""     // Vertical pulldown slider background image
             , v_sliderwidth: 0        // Vertical pulldown slider width (defaults to v_imgwidth)

             , h_class: ""             // Horizontal pulldown CSS class name
             , h_class_slider: ""      // Horizontal pulldown slider CSS class name
             , h_imgsrc: ""            // Horizontal pulldown image src url
             , h_imgheight: 0          // Horizontal pulldown image height
             , h_imgwidths: [ 0, 0, 0, 0, 0 ] // Horizontal pulldown cumulative image widths (arrow up, arrow down, slider gripper, slider top, slider bottom)
             , h_imgsrc_background: "" // Horizontal pulldown background image
             , h_imgsrc_slider: ""     // Horizontal pulldown slider background image
             , h_sliderheight: 0       // Horizontal pulldown slider height (defaults to h_imgheight)

             , styles: {}              // Additional CSS styles to apply
             , vertical: true          // Scroll vertically
             , horizontal: false       // Scroll horizontally
             , scrollnode: null        // The node that will be scrolled (usually a <div>)
             , rowdelta: 8             // Number of pixels to scroll per wheel click
             , dragscroll: true        // If the node can be scrolled by dragging it
             , elastic: true           // If the node should be scrolled elastically
             , friction: 0.1           // Friction multiplier
             , duration: 500           // Duration of animation
             , bounce: 0.5             // Bounce factor
             , fps: 50                 // Animation frames per second
             }

  , constants:
    { sampleFrequency: 50              // Number of times per second to update dragging speed
    , sensitivity: 0.3                 // Dragging speed update sensitivity (percentage of most recent delta to apply to current speed)
    , clickTimeout: 250                // Time to wait before repeating click when holding mouse button down
    , clickRepeat: 30                  // Time between repeated clicks when holding mouse button down
    }

  , curscroll: { x: 0, y: 0 }

  , vscrollbar: null
  , hscrollbar: null

  , initialize: function(options)
    {
      this.setOptions(options);

      if (!this.options.v_sliderwidth)
        this.options.v_sliderwidth = this.options.v_imgwidth;
      if (!this.options.h_sliderheight)
        this.options.h_sliderheight = this.options.h_imgheight;

      this.buildNode();
    }

  /* Public API */

  , getScroll: function()
    {
      return { x: Math.abs(this.curscroll.x)
             , y: Math.abs(this.curscroll.y)
             }
    }

  , refresh: function()
    {
      return this.updateState();
    }

  /* DOM */

  , buildNode: function()
    {
      if (this.options.scrollnode && (this.options.vertical || this.options.horizontal))
      {
        // First, we have to determine the size of the scrolling content. We'll do this by requesting the scrollHeight and/or
        // scrollWidth, depending on which scrollbars (vertical and/or horizontal) are requested.

        // The containerdiv will be the outer div, which contents will be moved to an inner contentdiv. For now, we only have
        // the scrollnode, which we use to calculate the scroll sizes.
        this.containerdiv = this.options.scrollnode;
        this.containerdiv.setStyles({ "overflow": "hidden" });
        var paddings = this.containerdiv.getStyle("padding").split(" ");
        paddings.each(function(padding, i) { paddings[i] = padding.toInt(); });
        switch (paddings.length)
        {
          // One value for all sides, duplicate it for top/bottom and right/left
          case 1: paddings.push(paddings[0]);
          // Two values, one for top/bottom and one for right/left, duplicate first value for separate top and bottom
          case 2: paddings.push(paddings[0]);
          // Three values, one for top, one for right/left, one for bottom, duplicate second value for separate right and left
          case 3: paddings.push(paddings[1]);
        }

        // If not scrolling in both directions, make room for the scrollbar which will be inserted
        if (!(this.options.vertical && this.options.horizontal))
        {
          var sizestyle = this.vertical ? "width" : "height";
          var scrollbarsize = this.vertical ? this.options.v_imgwidth : this.options.h_imgheight;

          var styles = this.containerdiv.getStyles(sizestyle);
          styles[this.vertical ? "padding-right" : "padding-bottom"] = paddings[this.vertical ? 1 : 2] + scrollbarsize;
          styles[sizestyle] = styles[sizestyle].toInt() - scrollbarsize;
          this.containerdiv.setStyles(styles);
        }
        this.containerdiv.store("wh-scrollbars", this);

        // Calculate content height and width
        var getScrollSize = function()
        {
          return { x: this.scrollWidth, y: this.scrollHeight };
        };
        var scrollsize;
        if (typeOf(this.containerdiv.measure) == "function")
          scrollsize = this.containerdiv.measure(getScrollSize);
        else
          scrollsize = getScrollSize.apply(this.containerdiv);
        var contentheight = scrollsize.y - paddings[0] - paddings[2];
        var contentwidth = scrollsize.x - paddings[1] - paddings[3];

        // Paddings will be moved from containerdiv to contentdiv, so update height and width
        var sizes = this.containerdiv.getStyles("width", "height");
        this.containerdiv.setStyles({ width: sizes.width.toInt() + paddings[1] + paddings[3]
                                    , height: sizes.height.toInt() + paddings[0] + paddings[2]
                                    });

        // Create a new div, which will hold the content and can be scrolled
        this.contentdiv = new Element("div");

        // Copy relevant styles from containerdiv to contentdiv
        var styles = this.containerdiv.getStyles("background");
        styles["position"] = "absolute";
        styles["padding"] = paddings.join("px ") + "px";
        styles["width"] = Math.max(contentwidth - (this.options.vertical ? this.options.v_imgwidth : 0), 0);
        styles["height"] = Math.max(contentheight - (this.options.horizontal ? this.options.h_imgheight : 0), 0);
        this.contentdiv.setStyles(styles);

        // Clear styles in containerdiv
        styles = { padding: 0
                 , background: ""
                 , position: this.containerdiv.getStyle("position")
                 };
        // Make sure the position is set to "absolute" or "relative"
        if (![ "absolute", "relative" ].contains(styles["position"]))
          styles["position"] = "relative";
        this.containerdiv.setStyles(styles);
        this.containerdiv.setStyles(this.options.styles);

        // Move the contents from the node to scroll to the content div
        while (this.containerdiv.firstChild)
          this.contentdiv.grab(this.containerdiv.firstChild);
        this.containerdiv.grab(this.contentdiv);

        // Add event handlers
        this.containerdiv.addEvents({ mousewheel: this.onMouseWheel.bind(this) });
        if ("createTouch" in document)
        {
          if (this.options.dragscroll)
            this.containerdiv.addEvents({ touchstart: this.onMouseDown.bind(this) });
          $(document).addEvents({ touchmove: this.onMouseMove.bind(this)
                                , touchend: this.onMouseUp.bind(this)
                                });
        }
        else
        {
          if (this.options.dragscroll)
            this.containerdiv.addEvents({ mousedown: this.onMouseDown.bind(this) });
          $(document).addEvents({ mousemove: this.onMouseMove.bind(this)
                                , mouseup: this.onMouseUp.bind(this)
                                });
        }

        if (this.options.vertical)
        {
          this.vscrollbar = { node: this.buildScrollbar(true, this.options.horizontal)
                            , minsize: this.options.v_imgheights[4] - this.options.v_imgheights[1]
                            , maxsize: 0
                            , sliderborder: null
                            , slidersize: 0
                            , sliderpos: 0
                            };
          this.containerdiv.grab(this.vscrollbar.node);
          this.contentdiv.setStyle("padding-right", this.options.v_imgwidth);
        }
        if (this.options.horizontal)
        {
          this.hscrollbar = { node: this.buildScrollbar(false, this.options.vertical)
                            , minsize: this.options.h_imgwidths[4] - this.options.h_imgwidths[1]
                            , maxsize: 0
                            , sliderborder: null
                            , slidersize: 0
                            , sliderpos: 0
                            };
          this.containerdiv.grab(this.hscrollbar.node);
          this.contentdiv.setStyle("padding-bottom", this.options.h_imgheight);
        }

        this.containerdiv.store("wh-scrollbars", this);
      }

      this.updateState();
    }

  , buildScrollbar: function(vertical, both)
    {
      if (vertical)
      {
        // The scrollbar itself (container node for all scrollbar stuff)
        var scrollbarnode = new Element("div", { styles: { position: "absolute"
                                                         , top: 0
                                                         , bottom: both ? this.options.h_imgheight : 0
                                                         , right: 0
                                                         , width: this.options.v_imgwidth
                                                         , overflow: "hidden"
                                                         }
                                               , "class": this.options.v_class
                                               , "data-scroll": "v"
                                               });
        if (this.options.v_imgsrc_background)
          scrollbarnode.setStyle("background", "url(" + this.options.v_imgsrc_background + ") repeat-y");
        if ("createTouch" in document)
          scrollbarnode.addEvents({ touchstart: this.onScrollbarMouseDown.bind(this) });
        else
          scrollbarnode.addEvents({ mousedown: this.onScrollbarMouseDown.bind(this) });

        // The arrow up button
        if (this.options.v_imgheights[0])
        {
          var arrowupnode = new Element("a", { href: "#"
                                             , events: { click: function(e) { return e.stop(); } } // Don't follow the link
                                             , "data-scroll": "v-u" // Recognize as vertical up arrow
                                             });
          if ("createTouch" in document)
            arrowupnode.addEvents({ touchstart: this.onArrowMouseDown.bind(this)
                                  , touchmove: function(e) { return e.stop(); }
                                  });
          else
            arrowupnode.addEvents({ mousedown: this.onArrowMouseDown.bind(this)
                                  , mousemove: function(e) { return e.stop(); }
                                  });
          var arrowup = new WHCroppedImage({ src: this.options.v_imgsrc
                                           , width: this.options.v_imgwidth
                                           , height: this.options.v_imgheights[0]
                                           , imgwidth: this.options.v_imgwidth
                                           , imgheight: this.options.v_imgheights[4]
                                           , top: 0
                                           , node: arrowupnode
                                           , styles: { position: "absolute"
                                                     , top: 0
                                                     }
                                           });
          scrollbarnode.grab(arrowupnode);
        }

        // The arrow down button
        if (this.options.v_imgheights[1] - this.options.v_imgheights[0])
        {
          var arrowdownnode = new Element("a", { href: "#"
                                               , events: { click: function(e) { return e.stop(); } } // Don't follow the link
                                               , "data-scroll": "v-d" // Recognize as vertical down arrow
                                               });
          if ("createTouch" in document)
            arrowdownnode.addEvents({ touchstart: this.onArrowMouseDown.bind(this)
                                    , touchmove: function(e) { return e.stop(); }
                                    });
          else
            arrowdownnode.addEvents({ mousedown: this.onArrowMouseDown.bind(this)
                                    , mousemove: function(e) { return e.stop(); }
                                    });
          var arrowdown = new WHCroppedImage({ src: this.options.v_imgsrc
                                             , width: this.options.v_imgwidth
                                             , height: this.options.v_imgheights[1] - this.options.v_imgheights[0]
                                             , imgwidth: this.options.v_imgwidth
                                             , imgheight: this.options.v_imgheights[4]
                                             , top: this.options.v_imgheights[0]
                                             , node: arrowdownnode
                                             , styles: { position: "absolute"
                                                       , bottom: 0
                                                       }
                                             });
          scrollbarnode.grab(arrowdownnode);
        }

        // The slider
        var slidernode = new Element("div", { styles: { position: "absolute"
                                                      , top: this.options.v_imgheights[0]
                                                      , height: 0
                                                      , width: this.options.v_sliderwidth
                                                      , cursor: "pointer"
                                                      , overflow: "hidden"
                                                      }
                                            , "class": this.options.v_class_slider
                                            , "data-scroll": "v"
                                            });
        if ("createTouch" in document)
          slidernode.addEvents({ touchstart: this.onSliderMouseDown.bind(this)
                               });
        else
          slidernode.addEvents({ mousedown: this.onSliderMouseDown.bind(this)
                               });
        if (this.options.v_imgsrc_slider)
          slidernode.grab(new Element("div", { styles: { background: "url(" + this.options.v_imgsrc_slider + ") repeat-y"
                                                       , position: "absolute"
                                                       , top: this.options.v_imgheights[3] - this.options.v_imgheights[2]
                                                       , bottom: this.options.v_imgheights[4] - this.options.v_imgheights[3]
                                                       , left: 0
                                                       , right: 0
                                                       }}));

        // The slider's top image
        if (this.options.v_imgheights[3] - this.options.v_imgheights[2])
        {
          var slidertop = new WHCroppedImage({ src: this.options.v_imgsrc
                                             , width: this.options.v_imgwidth
                                             , height: this.options.v_imgheights[3] - this.options.v_imgheights[2]
                                             , imgwidth: this.options.v_imgwidth
                                             , imgheight: this.options.v_imgheights[4]
                                             , top: this.options.v_imgheights[2]
                                             , styles: { position: "absolute"
                                                       , top: 0
                                                       }
                                             });
          slidernode.grab($(slidertop));
        }

        // The slider's bottom image
        if (this.options.v_imgheights[4] - this.options.v_imgheights[3])
        {
          var sliderbottom = new WHCroppedImage({ src: this.options.v_imgsrc
                                                , width: this.options.v_imgwidth
                                                , height: this.options.v_imgheights[4] - this.options.v_imgheights[3]
                                                , imgwidth: this.options.v_imgwidth
                                                , imgheight: this.options.v_imgheights[4]
                                                , top: this.options.v_imgheights[3]
                                                , styles: { position: "absolute"
                                                          , bottom: 0
                                                          }
                                                });
          slidernode.grab($(sliderbottom));
        }

        // The slider's gripper image
        if (this.options.v_imgheights[2] - this.options.v_imgheights[1])
        {
          var slidergrip = new WHCroppedImage({ src: this.options.v_imgsrc
                                              , width: this.options.v_sliderwidth
                                              , height: this.options.v_imgheights[2] - this.options.v_imgheights[1]
                                              , imgwidth: this.options.v_imgwidth
                                              , imgheight: this.options.v_imgheights[4]
                                              , top: this.options.v_imgheights[1]
                                              , styles: { position: "absolute"
                                                        , top: 0
                                                        }
                                              });
          slidernode.grab($(slidergrip)); // slidergrip is lastChild of slidernode
        }
        scrollbarnode.grab(slidernode); // slidernode is lastChild of scrollbarnode

        return scrollbarnode;
      }
      else
      {
        // The scrollbar itself (container node for all scrollbar stuff)
        var scrollbarnode = new Element("div", { styles: { position: "absolute"
                                                         , left: 0
                                                         , bottom: 0
                                                         , right: both ? this.options.v_imgwidth : 0
                                                         , height: this.options.h_imgheight
                                                         , overflow: "hidden"
                                                         }
                                               , "class": this.options.h_class
                                               , "data-scroll": "h"
                                               });
        if (this.options.h_imgsrc_background)
          scrollbarnode.setStyle("background", "url(" + this.options.h_imgsrc_background + ") repeat-y");
        if ("createTouch" in document)
          scrollbarnode.addEvents({ touchstart: this.onScrollbarMouseDown.bind(this) });
        else
          scrollbarnode.addEvents({ mousedown: this.onScrollbarMouseDown.bind(this) });

        // The arrow up button
        if (this.options.h_imgwidths[0])
        {
          var arrowupnode = new Element("a", { href: "#"
                                             , events: { click: function(e) { return e.stop(); } } // Don't follow the link
                                             , "data-scroll": "h-u" // Recognize as horizontal up arrow
                                             });
          if ("createTouch" in document)
            arrowupnode.addEvents({ touchstart: this.onArrowMouseDown.bind(this)
                                  , touchmove: function(e) { return e.stop(); }
                                  });
          else
            arrowupnode.addEvents({ mousedown: this.onArrowMouseDown.bind(this)
                                  , mousemove: function(e) { return e.stop(); }
                                  });
          var arrowup = new WHCroppedImage({ src: this.options.h_imgsrc
                                           , width: this.options.h_imgwidths[0]
                                           , height: this.options.h_imgheight
                                           , imgwidth: this.options.h_imgwidths[4]
                                           , imgheight: this.options.h_imgheight
                                           , left: 0
                                           , node: arrowupnode
                                           , styles: { position: "absolute"
                                                     , left: 0
                                                     }
                                           });
          scrollbarnode.grab(arrowupnode);
        }

        // The arrow down button
        if (this.options.h_imgwidths[1] - this.options.h_imgwidths[0])
        {
          var arrowdownnode = new Element("a", { href: "#"
                                               , events: { click: function(e) { return e.stop(); } } // Don't follow the link
                                               , "data-scroll": "h-d" // Recognize as horizontal down arrow
                                               });
          if ("createTouch" in document)
            arrowdownnode.addEvents({ touchstart: this.onArrowMouseDown.bind(this)
                                    , touchmove: function(e) { return e.stop(); }
                                    });
          else
            arrowdownnode.addEvents({ mousedown: this.onArrowMouseDown.bind(this)
                                    , mousemove: function(e) { return e.stop(); }
                                    });
          var arrowdown = new WHCroppedImage({ src: this.options.h_imgsrc
                                             , width: this.options.h_imgwidths[1] - this.options.h_imgwidths[0]
                                             , height: this.options.h_imgheight
                                             , imgwidth: this.options.h_imgwidths[4]
                                             , imgheight: this.options.h_imgheight
                                             , left: this.options.h_imgwidths[0]
                                             , node: arrowdownnode
                                             , styles: { position: "absolute"
                                                       , right: 0
                                                       }
                                             });
          scrollbarnode.grab(arrowdownnode);
        }

        // The slider
        var slidernode = new Element("div", { styles: { position: "absolute"
                                                      , left: this.options.h_imgwidths[0]
                                                      , height: this.options.h_sliderheight
                                                      , width: 0
                                                      , cursor: "pointer"
                                                      , overflow: "hidden"
                                                      }
                                            , "class": this.options.h_class_slider
                                            , "data-scroll": "h"
                                            });
        if ("createTouch" in document)
          slidernode.addEvents({ touchstart: this.onSliderMouseDown.bind(this)
                               });
        else
          slidernode.addEvents({ mousedown: this.onSliderMouseDown.bind(this)
                               });
        if (this.options.h_imgsrc_slider)
          slidernode.grab(new Element("div", { styles: { background: "url(" + this.options.h_imgsrc_slider + ") repeat-x"
                                                       , position: "absolute"
                                                       , top: 0
                                                       , bottom: 0
                                                       , left: this.options.h_imgwidths[3] - this.options.h_imgwidths[2]
                                                       , right: this.options.h_imgwidths[4] - this.options.h_imgwidths[3]
                                                       }}));

        // The slider's top image
        if (this.options.h_imgwidths[3] - this.options.h_imgwidths[2])
        {
          var slidertop = new WHCroppedImage({ src: this.options.h_imgsrc
                                             , width: this.options.h_imgwidths[3] - this.options.h_imgwidths[2]
                                             , height: this.options.h_imgheight
                                             , imgwidth: this.options.h_imgwidths[4]
                                             , imgheight: this.options.h_imgheight
                                             , left: this.options.h_imgwidths[2]
                                             , styles: { position: "absolute"
                                                       , left: 0
                                                       }
                                             });
          slidernode.grab($(slidertop));
        }

        // The slider's bottom image
        if (this.options.h_imgwidths[4] - this.options.h_imgwidths[3])
        {
          var sliderbottom = new WHCroppedImage({ src: this.options.h_imgsrc
                                                , width: this.options.h_imgwidths[4] - this.options.h_imgwidths[3]
                                                , height: this.options.h_imgheight
                                                , imgwidth: this.options.h_imgwidths[4]
                                                , imgheight: this.options.h_imgheight
                                                , left: this.options.h_imgwidths[3]
                                                , styles: { position: "absolute"
                                                          , right: 0
                                                          }
                                                });
          slidernode.grab($(sliderbottom));
        }

        // The slider's gripper image
        if (this.options.h_imgwidths[2] - this.options.h_imgwidths[1])
        {
          var slidergrip = new WHCroppedImage({ src: this.options.h_imgsrc
                                              , width: this.options.h_imgwidths[2] - this.options.h_imgwidths[1]
                                              , height: this.options.h_sliderheight
                                              , imgwidth: this.options.h_imgwidths[4]
                                              , imgheight: this.options.h_imgheight
                                              , left: this.options.h_imgwidths[1]
                                              , styles: { position: "absolute"
                                                        , left: 0
                                                        }
                                              });
          slidernode.grab($(slidergrip)); // slidergrip is lastChild of slidernode
        }
        scrollbarnode.grab(slidernode); // slidernode is lastChild of scrollbarnode

        return scrollbarnode;
      }
    }

  , toElement: function()
    {
      return this.node;
    }

  , updateState: function(dontupdateslider)
    {
      if (this.containerdiv)
      {
        this.limit = { x: [ this.containerdiv.clientWidth - this.contentdiv.offsetWidth, 0 ]
                     , y: [ this.containerdiv.clientHeight - this.contentdiv.offsetHeight, 0 ]
                     };

        if (this.options.vertical)
        {
          if (this.limit.y[0] == 0)
          {
            this.updateEnabled(true, false);
            this.curscroll.y = 0;
          }
          else
          {
            this.updateEnabled(true, true);
            if (typeOf(this.vscrollbar.sliderborder) == "null")
            {
              // Calculate slider border size and update width and gripper position accordingly
              var border = this.vscrollbar.node.lastChild.getStyles("border-width")["border-width"];
              var borderwidth = 0;
              border.split(" ").each(function(width, pos)
              {
                width = parseInt(width);
                if (pos == 0 || pos == 2) // top, bottom
                  this.vscrollbar.sliderborder += width;
                else if (pos == 1) // right
                  borderwidth += width;
                else // left
                {
                  borderwidth += width;
                  if ((this.options.v_imgheights[2] - this.options.v_imgheights[1]))
                    this.vscrollbar.node.lastChild.lastChild.setStyle("left", -width);
                }
              }, this);
              this.vscrollbar.node.lastChild.setStyles({ width: this.options.v_sliderwidth - borderwidth
                                                       , left: Math.floor((this.options.v_imgwidth - this.options.v_sliderwidth) / 2)
                                                       });
            }
            this.vscrollbar.maxsize = this.containerdiv.clientHeight
                                    - this.options.v_imgheights[0]
                                    - (this.options.v_imgheights[1] - this.options.v_imgheights[0])
                                    - (this.options.horizontal ? this.options.h_imgheight : 0);
            this.contentdiv.setStyle("top", this.curscroll.y);

            if (!dontupdateslider)
            {
              var slidersize = Math.max(Math.round(this.vscrollbar.maxsize * this.containerdiv.clientHeight / this.contentdiv.offsetHeight), this.vscrollbar.minsize);
              var sliderpos = Math.floor((this.vscrollbar.maxsize - slidersize) * this.curscroll.y / this.limit.y[0]);
              this.updateSliderState(true, sliderpos, slidersize);
            }
          }
        }
        else
          this.curscroll.y = 0;
        if (this.options.horizontal)
        {
          if (this.limit.x[0] == 0)
          {
            this.updateEnabled(false, false);
            this.curscroll.x = 0;
          }
          else
          {
            this.updateEnabled(false, true);
            if (typeOf(this.hscrollbar.sliderborder) == "null")
            {
              // Calculate slider border size and update width and gripper position accordingly
              var border = this.hscrollbar.node.lastChild.getStyles("border-width")["border-width"];
              var borderheight = 0;
              border.split(" ").each(function(width, pos)
              {
                width = parseInt(width);
                if (pos == 1 || pos == 3) // right, left
                  this.hscrollbar.sliderborder += width;
                else if (pos == 2) // bottom
                  borderheight += width;
                else // top
                {
                  borderheight += width;
                  if ((this.options.vhimgwidths[2] - this.options.h_imgwidths[1]))
                    this.hscrollbar.node.lastChild.lastChild.setStyle("top", -width);
                }
              }, this);
              this.hscrollbar.node.lastChild.setStyle("height", this.options.h_sliderheight - borderheight);
            }
            this.hscrollbar.maxsize = this.containerdiv.clientWidth
                                    - this.options.h_imgwidths[0]
                                    - (this.options.h_imgwidths[1] - this.options.h_imgwidths[0])
                                    - (this.options.vertical ? this.options.v_imgwidth : 0);
            this.contentdiv.setStyle("left", this.curscroll.x);

            if (!dontupdateslider)
            {
              var slidersize = Math.max(Math.round(this.hscrollbar.maxsize * this.containerdiv.clientWidth / this.contentdiv.offsetWidth), this.hscrollbar.minsize);
              var sliderpos = Math.floor((this.hscrollbar.maxsize - slidersize) * this.curscroll.x / this.limit.x[0]);
              this.updateSliderState(false, sliderpos, slidersize);
            }
          }
        }
        else
          this.curscroll.x = 0;
      }
    }

  , updateSliderState: function(vertical, pos, size)
    {
      if (isNaN(pos))
        return;

      if (vertical)
      {
        if (typeOf(size) != "null")
          this.vscrollbar.slidersize = size;
        this.vscrollbar.sliderpos = Math.min(Math.max(pos, 0), (this.vscrollbar.maxsize - this.vscrollbar.slidersize));
        this.vscrollbar.node.lastChild.setStyles({ height: this.vscrollbar.slidersize - this.vscrollbar.sliderborder
                                                 , top: this.vscrollbar.sliderpos + this.options.v_imgheights[0]
                                                 });
        if ((this.options.v_imgheights[2] - this.options.v_imgheights[1]))
          this.vscrollbar.node.lastChild.lastChild.setStyle("top", Math.floor((this.vscrollbar.slidersize - this.vscrollbar.sliderborder - (this.options.v_imgheights[2] - this.options.v_imgheights[1])) / 2));
      }
      else
      {
        if (typeOf(size) != "null")
          this.hscrollbar.slidersize = size;
        this.hscrollbar.sliderpos = Math.min(Math.max(pos, 0), (this.hscrollbar.maxsize - this.hscrollbar.slidersize));
        this.hscrollbar.node.lastChild.setStyles({ width: this.hscrollbar.slidersize - this.hscrollbar.sliderborder
                                                 , left: this.hscrollbar.sliderpos + this.options.h_imgwidths[0]
                                                 });
        if ((this.options.h_imgwidths[2] - this.options.h_imgwidths[1]))
          this.hscrollbar.node.lastChild.lastChild.setStyle("left", Math.floor((this.hscrollbar.slidersize - this.hscrollbar.sliderborder - (this.options.h_imgwidths[2] - this.options.h_imgwidths[1])) / 2));
      }
    }

  , updateEnabled: function(vertical, enabled)
    {
      if (vertical)
      {
        // Show or hide all child elements (arrows and slider)
        for (var node = this.vscrollbar.node.firstChild; node; node = node.nextSibling)
          node.setStyle("display", enabled ? "" : "none");
      }
      else
      {
        // Show or hide all child elements (arrows and slider)
        for (var node = this.hscrollbar.node.firstChild; node; node = node.nextSibling)
          node.setStyle("display", enabled ? "" : "none");
      }
    }

  /* Event handlers */

  , onMouseDown: function(event)
    {
      this.dragging = { type: "content"
                      , startpos: event.client
                      , curpos: event.client
                      , startscroll: { x: this.curscroll.x
                                     , y: this.curscroll.y
                                     }
                      };
      this.samples = {};
      this.speed = { x: 0, y: 0 };
      if (this.options.elastic)
        this.sampleHandle = this.sample.periodical(1000 / this.constants.sampleFrequency, this);
      return event.stop();
    }

  , onMouseMove: function(event)
    {
      this.scrollHandle = clearTimeout(this.scrollHandle);
      if (this.dragging)
      {
        this.dragging.curpos = event.client;
        if (this.dragging.type == "content")
        {
          this.curscroll = { x: this.dragging.startscroll.x + this.dragging.curpos.x - this.dragging.startpos.x
                           , y: this.dragging.startscroll.y + this.dragging.curpos.y - this.dragging.startpos.y
                           };
        }
        else if (this.dragging.type == "slider")
        {
          if (this.dragging.vertical)
          {
            var pos = this.dragging.sliderpos + this.dragging.curpos.y - this.dragging.startpos.y;
            this.updateSliderState(true, pos);

            this.curscroll.y = this.vscrollbar.sliderpos * this.limit.y[0] / (this.vscrollbar.maxsize - this.vscrollbar.slidersize);
          }
          else
          {
            //ADDME: Horizontal scrollbar
          }
        }
        this.updateState(this.dragging.type == "slider");
        return event.stop();
      }
    }

  , onMouseUp: function(event)
    {
      this.scrollHandle = clearTimeout(this.scrollHandle);
      if (this.dragging)
      {
        if (this.dragging.type == "content")
        {
          this.sampleHandle = clearInterval(this.sampleHandle);
          if (this.speed.x || this.speed.y)
            this.moveStart();
          else if (this.dragging.curpos.x != this.dragging.startpos.x || this.dragging.curpos.y != this.dragging.startpos.y)
            this.fireEvent("change");
        }
        else if (this.dragging.type == "slider")
        {
          this.fireEvent("change");
        }
        this.dragging = null;
        return event.stop();
      }
    }

  , onMouseWheel: function(event)
    {
      this.scroll(true, 3 * event.wheel * this.options.rowdelta);
      return event.stop();
    }

  , onScrollbarMouseDown: function(event)
    {
      var node = event.target;
      var scroll = "";
      while (node && !(scroll = node.get("data-scroll")))
        node = node.parentNode;
      if (!scroll)
        return;

      if (scroll == "v")
      {
//ADDME:
//        var amount = scroll[1] == "u" ? this.options.rowdelta : -this.options.rowdelta;
//        this.scroll(true, amount);
//        this.scrollHandle = this.autoScroll.delay(this.constants.clickTimeout, this, [ true, amount ]);
      }
      else
      {
        //ADDME: Horizontal scrollbar
      }

      return event.stop();
    }

  , onArrowMouseDown: function(event)
    {
      var node = event.target;
      var scroll = "";
      while (node && !(scroll = node.get("data-scroll")))
        node = node.parentNode;
      if (!scroll)
        return;
      scroll = scroll.split("-");
      if (scroll[0] == "v")
      {
        var amount = scroll[1] == "u" ? this.options.rowdelta : -this.options.rowdelta;
        this.scroll(true, amount);
        this.scrollHandle = this.autoScroll.delay(this.constants.clickTimeout, this, [ true, amount ]);
      }
      else
      {
        //ADDME: Horizontal scrollbar
      }
      return event.stop();
    }

  , onSliderMouseDown: function(event)
    {
      var node = event.target;
      var scroll = "";
      while (node && !(scroll = node.get("data-scroll")))
        node = node.parentNode;
      if (!scroll)
        return;
      if (scroll == "v")
      {
        this.dragging = { type: "slider"
                        , startpos: event.client
                        , curpos: event.client
                        , sliderpos: this.vscrollbar.sliderpos
                        , vertical: true
                        };
      }
      else
      {
        //ADDME: Horizontal scrollbar
      }
    }

  /* Internal functions */

  , autoScroll: function(vertical, amount)
    {
      if (!this.scrollHandle)
        return;
      this.scrollHandle = clearTimeout(this.scrollHandle);
      this.scroll(vertical, amount);
      this.scrollHandle = this.autoScroll.delay(this.constants.clickRepeat, this, [ vertical, amount ]);
    }

  , scroll: function(vertical, amount)
    {
      var moved = false;
      if (vertical)
      {
        var oldy = this.curscroll.y;
        this.curscroll.y += amount;
        if (this.curscroll.y < this.limit.y[0])
          this.curscroll.y = this.limit.y[0];
        else if (this.curscroll.y > this.limit.y[1])
          this.curscroll.y = this.limit.y[1];
        moved = this.curscroll.y != oldy;
      }
      this.updateState();
      if (moved)
        this.fireEvent("change");
    }

  , sample: function()
    {
      this.samples.last = this.samples.current;
      this.samples.current = Object.merge({ time: Date.now() }, this.dragging.curpos);
      if (this.samples.last)
      {
        var dt = this.samples.current.time - this.samples.last.time;
        var dx = this.samples.current.x - this.samples.last.x;
        var dy = this.samples.current.y - this.samples.last.y;
        var s = this.constants.sensitivity;
        var rs = 1 - s;
        this.speed.x = this.speed.x * rs + (dx / dt) * s;
        this.speed.y = this.speed.y * rs + (dy / dt) * s;
      }
    }

  , moveStart: function()
    {
      var m = this.options.modifiers;
      this.time = Date.now();
      this.stepHandle = this.moveStep.periodical(Math.round(1000 / this.options.fps), this);
      this.lastStepTime = this.time;
    }

  , moveStep: function()
    {
      var time = Date.now();

      if (this.options.friction)
      {
        if ((Math.abs(this.speed.x) + Math.abs(this.speed.y)) < 0.001)
        {
          this.moveComplete();
          return;
        }
      }
      else if (time > this.time + this.options.duration)
      {
        this.moveComplete();
        return;
      }

      if (this.options.friction) {
        var multiplier = 1 - this.options.friction;
        this.speed.x *= multiplier;
        this.speed.y *= multiplier;
      }

      var interval = time - this.lastStepTime;
      this.curscroll.x += this.speed.x * interval;
      this.curscroll.y += this.speed.y * interval;
      if (this.curscroll.x < this.limit.x[0])
      {
        this.curscroll.x = this.limit.x[0];
        this.speed.x = this.options.bounce * -this.speed.x;
      }
      else if (this.curscroll.x > this.limit.x[1])
      {
        this.curscroll.x = this.limit.x[1];
        this.speed.x = this.options.bounce * -this.speed.x;
      }
      if (this.curscroll.y < this.limit.y[0])
      {
        this.curscroll.y = this.limit.y[0];
        this.speed.y = this.options.bounce * -this.speed.y;
      }
      else if (this.curscroll.y > this.limit.y[1])
      {
        this.curscroll.y = this.limit.y[1];
        this.speed.y = this.options.bounce * -this.speed.y;
      }
      this.updateState();
      this.lastStepTime = time;
    }

  , moveComplete: function()
    {
      if (!this.stepHandle)
        return false;
      this.stepHandle = clearInterval(this.stepHandle);
      this.fireEvent("change");
    }
  });


/****************************************************************************************************************************
 * Replace inputs in a form
 */

/* Replace components within a form
   form: The form id or node to replace components in
   options: { checkbox: WHCheckbox options object (enabled, checked, value, classname and input will be overwritten)
            , radio: WHRadiobutton options object (enabled, checked, value, classname and radiogroup will be overwritten)
            , select: WHPulldown options object (enabled, values, classname and input will be overwritten)
            }
*/
function WHReplaceComponentsInForm(form, options)
{
  form = $(form);
  if (!form || !options)
    return false;

  // Check all <input> elements within the form
  form.getElements("input").each(function(input)
  {
    var inputform = input.form;
    if (!inputform)
      return;

    switch (input.get("type"))
    {
      case "checkbox":
      {
        if (options["checkbox"])
        {
          // Create a new checkbox component
          var classname = input.getProperty("class") + " " + options["checkbox"].classname;
          var comp = new WHCheckbox(Object.merge(options["checkbox"], { enabled: !input.disabled
                                                                      , checked: input.checked
                                                                      , value: input.value
                                                                      , input: input
                                                                      , classname: classname
                                                                      }));
          // Replace the current input with the new checkbox's node
          input.setStyle("display", "none")
               .store("wh-replaced", comp)
               .parentNode.insertBefore($(comp), input);
          $(comp).cloneEvents(input);

          // Rebind any labels to the new component
          var labels = input.id ? $$("label[for='" + input.id + "']") : [];
          labels.include(input.getParent("label"))
                .clean()
                .each(function(label){ label.set("for", "").addEvent("click", comp.toggle.bind(comp)) });
        }
      } break;

      case "radio":
      {
        if (options["radio"])
        {
          // Create a new radiobutton component
          var classname = input.getProperty("class") + " " + options["radio"].classname;
          var comp = new WHRadiobutton(Object.merge(options["radio"], { enabled: !input.disabled
                                                                      , checked: input.checked
                                                                      , value: input.value
                                                                      , radiogroup: input.name
                                                                      , input: input
                                                                      , classname: classname
                                                                      }));
          // Replace the current input with the new radiobutton's node
          input.setStyle("display", "none")
               .store("wh-replaced", comp)
               .parentNode.insertBefore($(comp), input);
          $(comp).cloneEvents(input);

          // Rebind any labels to the new component
          var labels = input.id ? $$("label[for='" + input.id + "']") : [];
          labels.include(input.getParent("label"))
                .clean()
                .each(function(label){ label.set("for", "").addEvent("click", comp.toggle.bind(comp)) });
        }
      } break;
    }
  });

  // Check all <select> elements within the form
  if (options["select"])
  {
    form.getElements("select").each(function(select)
    {
      var selectform = select.form;
      if (!selectform)
        return;

      // Make a list of the select's options
      var values = [];
      select.getElements("option").each(function(option)
      {
        var value = option.get("value") || option.get("text");
        var title = option.get("label") || option.get("text");
        values.push({ value: value, title: title, selected: option.get("selected") });
      });

      // Create a new pulldown component
      var classname = select.getProperty("class") + " " + options["select"].classname;
      var comp = new WHPulldown(Object.merge(options["select"], { enabled: !select.disabled
                                                                , values: values
                                                                , input: select
                                                                , classname: classname
                                                                }));
      // Replace the current select with the new pulldown's node
      select.setStyle("display", "none")
            .store("wh-replaced", comp)
            .parentNode.insertBefore($(comp), select);
      $(comp).cloneEvents(select);

      // Rebind any labels to the new component
      var labels = select.id ? $$("label[for='" + select.id + "']") : [];
      labels.include(select.getParent("label"))
            .clean()
            .each(function(label){ label.set("for", "").addEvent("click", comp.toggle.bind(comp)) });
    });
  }

  window.fireEvent("wh-replaced");
}

/****************************************************************************************************************************
 * Cropped image
 */

/* A cropped image, using CSS crop when available, or clipping through a parent element otherwise. */
WHCroppedImage = new Class(
  { Implements: [ Options ]

  /* Initialization */

  , options: { src: ""    // Image src url
             , width: 0   // Cropped image width
             , height: 0  // Cropped image height
             , imgwidth: 0 // Full image width
             , imgheight: 0 // Full image height
             , top: 0     // Top reference point
             , left: 0    // Left reference point
             , styles: {} // Additional CSS styles to apply
             , node: null // Use as parent node
             }

  , initialize: function(options)
    {
      this.setOptions(options);

      // Check for CSS crop support
      if (typeOf(WHCroppedImage.csscropsupport) == "null")
      {
        var div = document.createElement("div");
        WHCroppedImage.csscropsupport = "crop" in div.style || "WebkitCrop" in div.style || "MozCrop" in div.style;
      }

      this.buildNode();
    }

  /* Public API */

  , setCrop: function(options)
    {
      this.setOptions(options);
      this.updateCropStyle();
    }

  /* DOM */

  , buildNode: function()
    {
      if (WHCroppedImage.csscropsupport && !this.options.node)
      {
        this.node = new Element("img", { src: this.options.src
                                       , styles: { border: "none" }
                                       });
      }
      else
      {
        this.node = this.options.node ? this.options.node : new Element("div");
        this.node.style.cssText = "display: inline-block; zoom: 1; *display: inline"; // "inline-block" workaround for IE7
        this.node.setStyles({ position: "relative"
                            , overflow: "hidden"
                            , "vertical-align": "baseline"
                            });
        new Element("img", { src: this.options.src
                           , width: this.options.imgwidth
                           , height: this.options.imgheight
                           , styles: { position: "absolute"
                                     , border: "none"
                                     }
                           }).inject(this.node);
      }
      this.updateCropStyle();
      this.node.setStyles(this.options.styles);
    }

  , updateCropStyle: function()
    {
      if (!this.options.width && !this.options.height)
        return;

      if (WHCroppedImage.csscropsupport && !this.options.node)
      {
        var rect = "rect(" + this.options.top + "px, " + (this.options.left + this.options.width) + "px, " + (this.options.top + this.options.height) + "px, " + this.options.left + "px)";
        this.node.setStyles({ "crop": rect
                            , "WebkitCrop": rect
                            , "MozCrop": rect
                            });
      }
      else
      {
        this.node.setStyles({ width: this.options.width
                            , height: this.options.height
                            });
        this.node.firstChild.setStyles({ top: -this.options.top
                                       , left: -this.options.left
                                       });
      }
    }

  , toElement: function()
    {
      return this.node;
    }
  });

// Check if the browser has CSS crop support
WHCroppedImage.csscropsupport = null;


/****************************************************************************************************************************
 * Register this script
 */

if (typeof $todd != "undefined")
  $todd.MarkScriptComplete("extensions.js");

