/*  Prototype-UI, version trunk
 *
 *  Prototype-UI is freely distributable under the terms of an MIT-style license.
 *  For details, see the PrototypeUI web site: http://www.prototype-ui.com/
 *
 *--------------------------------------------------------------------------*/

if(typeof Prototype == 'undefined' || !Prototype.Version.match("1.6"))
  throw("Prototype-UI library require Prototype library >= 1.6.0");

(function(p) {
  var b = p.Browser, n = navigator;

  if (b.WebKit) {
    b.WebKitVersion = parseFloat(n.userAgent.match(/AppleWebKit\/([\d\.\+]*)/)[1]);
    b.Safari2 = (b.WebKitVersion < 420);
  }

  if (b.IE) {
    b.IEVersion = parseFloat(n.appVersion.split(';')[1].strip().split(' ')[1]);
    b.IE6 = b.IEVersion == 6;
    b.IE7 = b.IEVersion == 7;
  }

  p.falseFunction = function() { return false };
  p.trueFunction  = function() { return true  };
})(Prototype);

/*
Namespace: UI

  Introduction:
    Prototype-UI is a library of user interface components based on the Prototype framework.
    Its aim is to easilly improve user experience in web applications.

    It also provides utilities to help developers.

  Guideline:
    - Prototype conventions are followed
    - Everything should be unobstrusive
    - All components are themable with CSS stylesheets, various themes are provided

  Warning:
    Prototype-UI is still under deep development, this release is targeted to developers only.
    All interfaces are subjects to changes, suggestions are welcome.

    DO NOT use it in production for now.

  Authors:
    - Sébastien Gruhier, <http://www.xilinus.com>
    - Samuel Lebeau, <http://gotfresh.info>
*/

var UI = {
  Abstract: { },
  Ajax: { }
};
Object.extend(Class.Methods, {
  extend: Object.extend.methodize(),

  addMethods: Class.Methods.addMethods.wrap(function(proceed, source) {
    // ensure we are not trying to add null or undefined
    if (!source) return this;

    // no callback, vanilla way
    if (!source.hasOwnProperty('methodsAdded'))
      return proceed(source);

    var callback = source.methodsAdded;
    delete source.methodsAdded;
    proceed(source);
    callback.call(source, this);
    source.methodsAdded = callback;

    return this;
  }),

  addMethod: function(name, lambda) {
    var methods = {};
    methods[name] = lambda;
    return this.addMethods(methods);
  },

  method: function(name) {
    return this.prototype[name].valueOf();
  },

  classMethod: function() {
    $A(arguments).flatten().each(function(method) {
      this[method] = (function() {
        return this[method].apply(this, arguments);
      }).bind(this.prototype);
    }, this);
    return this;
  },

  // prevent any call to this method
  undefMethod: function(name) {
    this.prototype[name] = undefined;
    return this;
  },

  // remove the class' own implementation of this method
  removeMethod: function(name) {
    delete this.prototype[name];
    return this;
  },

  aliasMethod: function(newName, name) {
    this.prototype[newName] = this.prototype[name];
    return this;
  },

  aliasMethodChain: function(target, feature) {
    feature = feature.camelcase();

    this.aliasMethod(target+"Without"+feature, target);
    this.aliasMethod(target, target+"With"+feature);

    return this;
  }
});
Object.extend(Number.prototype, {
  // Snap a number to a grid
  snap: function(round) {
    return parseInt(round == 1 ? this : (this / round).floor() * round);
  }
});
/*
Interface: String

*/

Object.extend(String.prototype, {
  camelcase: function() {
    var string = this.dasherize().camelize();
    return string.charAt(0).toUpperCase() + string.slice(1);
  },

  /*
    Method: makeElement
      toElement is unfortunately already taken :/

      Transforms html string into an extended element or null (when failed)

      > '<li><a href="#">some text</a></li>'.makeElement(); // => LI href#
      > '<img src="foo" id="bar" /><img src="bar" id="bar" />'.makeElement(); // => IMG#foo (first one)

    Returns:
      Extended element

  */
  makeElement: function() {
    var wrapper = new Element('div'); wrapper.innerHTML = this;
    return wrapper.down();
  }
});
Object.extend(Array.prototype, {
  /**
   * Array#isEmpty() -> Boolean
   * Convenient method to check wether or not array is empty
   * returns: true if array is empty, false otherwise
   **/
  isEmpty: function() {
    return !this.length;
  },

  /**
   * Array#at(index) -> Object
   * Returns the element at the given index or undefined if index is out of range.
   * A negative index counts from the end.
   **/
  at: function(index) {
    return this[index < 0 ? this.length + index : index];
  },

  /**
   * Array#removeAt(index) -> Object | undefined
   * Deletes item at the given index, which may be negative
   * returns: deleted item or undefined if index is out of range
   **/
  removeAt: function(index) {
    if (-index > this.length) return;
    return this.splice(index, 1)[0];
  },

  /**
   * Array#removeIf(iterator[, context]) -> Array
   * Deletes items for which iterator returns a truthy value, bound to optional context
   * returns: array of items deleted
   **/
  removeIf: function(iterator, context) {
    for (var i = this.length - 1, objects = [ ]; i >= 0; i--)
      if (iterator.call(context, this[i], i))
        objects.push(this.removeAt(i));
    return objects.reverse();
  },

  /**
   * Array#remove(object) -> Number
   * Deletes items that are identical to given object
   * returns: number of items deleted
   **/
  remove: function(object) {
    return this.removeIf(function(member) { return member === object }).length;
  },

  /**
   * Array#insert(index, object[, ...])
   * Inserts the given objects before the element with the given index (which may be negative)
   * returns: this
   **/
  insert: function(index) {
    if (index > this.length)
      this.length = index;
    else if (index < 0)
      index = this.length + index + 1;

    this.splice.apply(this, [ index, 0 ].concat($A(arguments).slice(1)));
    return this;
  }
});

// backward compatibility
Array.prototype.empty = Array.prototype.isEmpty;
Element.addMethods({
  getScrollDimensions: function(element) {
    element = $(element);
    return {
      width:  element.scrollWidth,
      height: element.scrollHeight
    }
  },

  getScrollOffset: function(element) {
    element = $(element);
    return Element._returnOffset(element.scrollLeft, element.scrollTop);
  },

  setScrollOffset: function(element, offset) {
    element = $(element);
    if (arguments.length == 3)
      offset = { left: offset, top: arguments[2] };
    element.scrollLeft = offset.left;
    element.scrollTop  = offset.top;
    return element;
  },

  // returns "clean" numerical style (without "px") or null if style can not be resolved
  // or is not numeric
  getNumStyle: function(element, style) {
    var value = parseFloat($(element).getStyle(style));
    return isNaN(value) ? null : value;
  },

  // with courtesy of Tobie Langel
  //   (http://tobielangel.com/2007/5/22/prototype-quick-tip)
  appendText: function(element, text) {
    element = $(element);
    element.appendChild(document.createTextNode(String.interpret(text)));
    return element;
  }
});

document.whenReady = (function() {
  var queue = [ ];

  document.observe('dom:loaded', function() {
    queue.invoke('call', document);
    queue.clear();
    document.whenReady = function(callback) { callback.bind(document).defer() };
  });

  return function(callback) { queue.push(callback) };
})();

Object.extend(document.viewport, {
  // Alias this method for consistency
  getScrollOffset: document.viewport.getScrollOffsets,

  setScrollOffset: function(offset) {
    Element.setScrollOffset(Prototype.Browser.WebKit ? document.body : document.documentElement, offset);
  },

  getScrollDimensions: function() {
    return Element.getScrollDimensions(Prototype.Browser.WebKit ? document.body : document.documentElement);
  }
});

document.whenReady(function() {
  window.$head = $(document.getElementsByTagName('head')[0]);
  window.$body = $(document.body);
});
/*
Interface: UI.Options
  Mixin to handle *options* argument in initializer pattern.

  TODO: find a better example than Circle that use an imaginary Point function,
        this example should be used in tests too.

  It assumes class defines a property called *options*, containing
  default options values.

  Instances hold their own *options* property after a first call to <setOptions>.

  Example:
    > var Circle = Class.create(UI.Options, {
    >
    >   // default options
    >   options: {
    >     radius: 1,
    >     origin: Point(0, 0)
    >   },
    >
    >   // common usage is to call setOptions in initializer
    >   initialize: function(options) {
    >     this.setOptions(options);
    >   }
    > });
    >
    > var circle = new Circle({ origin: Point(1, 4) });
    >
    > circle.options
    > // => { radius: 1, origin: Point(1,4) }

  Accessors:
    There are builtin methods to automatically write options accessors. All those
    methods can take either an array of option names nor option names as arguments.
    Notice that those methods won't override an accessor method if already present.

     * <optionsGetter> creates getters
     * <optionsSetter> creates setters
     * <optionsAccessor> creates both getters and setters

    Common usage is to invoke them on a class to create accessors for all instances
    of this class.
    Invoking those methods on a class has the same effect as invoking them on the class prototype.
    See <classMethod> for more details.

    Example:
    > // Creates getter and setter for the "radius" options of circles
    > Circle.optionsAccessor('radius');
    >
    > circle.setRadius(4);
    > // 4
    >
    > circle.getRadius();
    > // => 4 (circle.options.radius)

  Inheritance support:
    Subclasses can refine default *options* values, after a first instance call on setOptions,
    *options* attribute will hold all default options values coming from the inheritance hierarchy.
*/

(function() {
  UI.Options = {
    methodsAdded: function(klass) {
      klass.classMethod($w(' setOptions allOptions optionsGetter optionsSetter optionsAccessor '));
    },

    // Group: Methods

    /*
      Method: setOptions
        Extends object's *options* property with the given object
    */
    setOptions: function(options) {
      if (!this.hasOwnProperty('options'))
        this.options = this.allOptions();

      this.options = Object.extend(this.options, options || {});
    },

    /*
      Method: allOptions
        Computes the complete default options hash made by reverse extending all superclasses
        default options.

        > Widget.prototype.allOptions();
    */
    allOptions: function() {
      var superclass = this.constructor.superclass, ancestor = superclass && superclass.prototype;
      return (ancestor && ancestor.allOptions) ?
          Object.extend(ancestor.allOptions(), this.options) :
          Object.clone(this.options);
    },

    /*
      Method: optionsGetter
        Creates default getters for option names given as arguments.
        With no argument, creates getters for all option names.
    */
    optionsGetter: function() {
      addOptionsAccessors(this, arguments, false);
    },

    /*
      Method: optionsSetter
        Creates default setters for option names given as arguments.
        With no argument, creates setters for all option names.
    */
    optionsSetter: function() {
      addOptionsAccessors(this, arguments, true);
    },

    /*
      Method: optionsAccessor
        Creates default getters/setters for option names given as arguments.
        With no argument, creates accessors for all option names.
    */
    optionsAccessor: function() {
      this.optionsGetter.apply(this, arguments);
      this.optionsSetter.apply(this, arguments);
    }
  };

  // Internal
  function addOptionsAccessors(receiver, names, areSetters) {
    names = $A(names).flatten();

    if (names.empty())
      names = Object.keys(receiver.allOptions());

    names.each(function(name) {
      var accessorName = (areSetters ? 'set' : 'get') + name.camelcase();

      receiver[accessorName] = receiver[accessorName] || (areSetters ?
        // Setter
        function(value) { return this.options[name] = value } :
        // Getter
        function()      { return this.options[name]         });
    });
  }
})();
/*
Namespace: CSS

  Utility functions for CSS/StyleSheet files access

  Authors:
    - Sébastien Gruhier, <http://www.xilinus.com>
    - Samuel Lebeau, <http://gotfresh.info>
*/

var CSS = (function() {
  // Code based on:
  //   - IE5.5+ PNG Alpha Fix v1.0RC4 (c) 2004-2005 Angus Turnbull http://www.twinhelix.com
  //   - Whatever:hover - V2.02.060206 - hover, active & focus (c) 2005 - Peter Nederlof * Peterned - http://www.xs4all.nl/~peterned/
  function fixPNG() {
   parseStylesheet.apply(this, $A(arguments).concat(fixRule));
  };

  function parseStylesheet() {
    var patterns = $A(arguments);
    var method = patterns.pop();

    // To avoid flicking background
    // if (Prototype.Browser.IE)
    //   document.execCommand("BackgroundImageCache", false, true);
    // Parse all document stylesheets
    var styleSheets = $A(document.styleSheets);
    if (patterns.length > 0) {
      styleSheets = styleSheets.select(function(css) {
        return patterns.any(function(pattern) {
          return css.href && css.href.match(pattern)
          });
      });
    }
    styleSheets.each(function(styleSheet) {fixStylesheet.call(this, styleSheet, method)});
  };

  // Fixes a stylesheet
  function fixStylesheet(stylesheet, method) {
    // Parse import files
    if (stylesheet.imports)
      $A(stylesheet.imports).each(fixStylesheet);

    var href = stylesheet.href || document.location.href;
    var docPath = href.substr(0, href.lastIndexOf('/'));
	  // Parse all CSS Rules
    $A(stylesheet.rules || stylesheet.cssRules).each(function(rule) { method.call(this, rule, docPath) });
  };

  var filterPattern = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="#{src}",sizingMethod="#{method}")';

  // Fixes a rule if it has a PNG background
  function fixRule(rule, docPath) {
    var bgImg = rule.style.backgroundImage;
    // Rule with PNG background image
    if (bgImg && bgImg != 'none' && bgImg.match(/^url[("']+(.*\.png)[)"']+$/i)) {
      var src = RegExp.$1;
      var bgRepeat = rule.style.backgroundRepeat;
      // Relative path
      if (src[0] != '/')
        src = docPath + "/" + src;
      // Apply filter
      rule.style.filter = filterPattern.interpolate({
        src:    src,
        method: bgRepeat == "no-repeat" ? "crop" : "scale" });
      rule.style.backgroundImage = "none";
    }
  };

  var preloadedImages = new Hash();

  function preloadRule(rule, docPath) {
    var bgImg = rule.style.backgroundImage;
    if (bgImg && bgImg != 'none'  && bgImg != 'initial' ) {
      if (!preloadedImages.get(bgImg)) {
        bgImg.match(/^url[("']+(.*)[)"']+$/i);
        var src = RegExp.$1;
        // Relative path
        if (!(src.substr(0, 1) == '/' || src.match(/^file:/) || src.match(/^https?:/)))
          src = docPath + "/" + src;
        preloadedImages.set(bgImg, true);
        var image = new Image();
        image.src = src;
      }
    }
  }

  return {
    /*
       Method: fixPNG
         Fix transparency of PNG background of document stylesheets.
         (only on IE version<7, otherwise does nothing)

         Warning: All png background will not work as IE filter use for handling transparency in PNG
         is not compatible with all background. It does not support top/left position (so no CSS sprite)

         I recommend to create a special CSS file with png that needs to be fixed and call CSS.fixPNG on this CSS

         Examples:
          > CSS.fixPNG() // To fix all css
          >
          > CSS.fixPNG("mac_shadow.css") // to fix all css files with mac_shadow.css so mainly only on file
          >
          > CSS.fixPNG("shadow", "vista"); // To fix all css files with shadow or vista in their names

       Parameters
         patterns: (optional) list of pattern to filter css files
    */
    fixPNG: (Prototype.Browser.IE && Prototype.Browser.IEVersion < 7) ? fixPNG : Prototype.emptyFunction,

    // By Tobie Langel (http://tobielangel.com)
    //   inspired by http://yuiblog.com/blog/2007/06/07/style/
    addRule: function(css) {
      var style = new Element('style', { type: 'text/css', media: 'screen' });
      $head.insert(style);
      if (style.styleSheet) style.styleSheet.cssText = css;
      else style.appendText(css);
      return style;
    },

    preloadImages: function() {
      // Does not work with FF3!!
      if (navigator.userAgent.match(/Firefox\/3/))
        return;

      parseStylesheet.apply(this, $A(arguments).concat(preloadRule));
    }
  };
})();
UI.PullDown = Class.create(UI.Options, {
  options: {
    className:   '',
    shadow:      false,
    position:    'over',
    cloneWidth:   false,
    beforeShow:   null,
    afterShow:    null,
    beforeUpdate: null,
    afterUpdate:  null,
    afterCreate:  null
  },

  initialize: function(container, options){
    this.setOptions(options);
		this.container = $(container);

    this.element = new Element('div', {
      className: 'ui_pulldown ' + this.options.className,
      style: 'z-index:999999;position:absolute;'
    }).hide();

    if (this.options.shadow)
      this.shadow = new UI.Shadow(this.element, {theme: this.options.shadow}).hide();
    else
      this.iframe = Prototype.Browser.IE ? new UI.IframeShim() : null;

    this.outsideClickHandler = this.outsideClick.bind(this);
    this.placeHandler        = this.place.bind(this);
    this.hideHandler         = this.hide.bind(this);
  },

  destroy: function(){
 		if (this.active)
      this.element.remove();
    this.element = null;
    this.stopObserving();
  },

  /*
    Method: insert
      Inserts a new Element to the PullDown

    Parameters:
      elem  - an DOM element

    Returns:
      this
   */
   insert: function(elem){
     return this.element.insert(elem);
   },

   /*
    Method: place
      Place the PullDown

    Parameters:
      none

    Returns:
     this
  */
  place: function(){
    this.element.clonePosition(this.container, {
      setHeight: false,
      setWidth:  this.options.cloneWidth,
      offsetTop: this.options.position == 'below' ? this.container.offsetHeight : 0
    });

    var w = this.element.getWidth();
    var h = this.element.getHeight();
    var t = parseInt(this.element.style.top);
    var l = parseInt(this.element.style.left);

    if (this.shadow)
      this.shadow.setBounds({top: t, left: l, width: w, height: h});
    if (this.iframe)
      this.iframe.setPosition(t, l).setSize(w, h);
    return this;
  },

  /*
    Method: show
    Show the PullDown

    Parameters:
      event  - (optional) Event fired the show

    Returns:
     this
  */
  show: function(event){
    if (this.active)
        return this;

    this.active = true;

    if (this.options.beforeShow)
        this.options.beforeShow(this);

    this.element.hide();

    if (this.iframe)
			this.iframe.show();

    document.body.insert(this.element);

    if (this.shadow)
      this.shadow.show();
    this.element.show();

    if (this.options.afterShow)
      this.options.afterShow(this);

    document.observe('mousedown',  this.outsideClickHandler);
		Event.observe(window,'scroll', this.placeHandler);
		Event.observe(window,'resize', this.hideHandler);

		return this;
  },

  outsideClick: function(event) {
    if (event.findElement('.ui_pulldown'))
			return;
    this.hide();
  },

  /*
    Method: hide
      Hide the PullDown

    Returns:
      this
  */
  hide: function(){
		if (this.active) {
      this.active = false;
      if (this.shadow)
        this.shadow.remove();

  		if(this.iframe)
  			this.iframe.remove();

      this.element.remove();

    }
    this.stopObserving();
		return this;
  },

  stopObserving: function() {
		Event.stopObserving(window,'resize', this.hideHandler);
		Event.stopObserving(window,'scroll', this.placeHandler);
    document.stopObserving('click', this.outsideClickHandler);
  }
});
/*
Class: UI.Shadow
  Add shadow around a DOM element. The element MUST BE in ABSOLUTE position.

  Shadow can be skinned by CSS (see mac_shadow.css or drop_shadow.css).
  CSS must be included to see shadow.

  A shadow can have two states: focused and blur.
  Shadow shifts are set in CSS file as margin and padding of shadow_container to add visual information.

  Example:
    > new UI.Shadow("element_id");
*/
UI.Shadow = Class.create(UI.Options, {
  options: {
    theme: "mac_shadow",
    focus: false,
    zIndex: 100,
    withIFrameShim: false
  },

  /*
    Method: initialize
      Constructor, adds shadow elements to the DOM if element is in the DOM.
      Element MUST BE in ABSOLUTE position.

    Parameters:
      element - DOM element
      options - Hashmap of options
        - theme (default: mac_shadow)
        - focus (default: true)
        - zIndex (default: 100)

    Returns:
      this
  */
  initialize: function(element, options) {
    this.setOptions(options);

    this.element = $(element);
    this.create();
    this.iframe = Prototype.Browser.IE && this.options.withIFrameShim ? new UI.IframeShim() : null;

    if (Object.isElement(this.element.parentNode))
      this.render();
  },

  /*
    Method: destroy
      Destructor, removes elements from the DOM
  */
  destroy: function() {
    if (this.shadow.parentNode)
      this.remove();
  },

  // Group: Size and Position
  /*
    Method: setPosition
      Sets top/left shadow position in pixels

    Parameters:
      top -  top position in pixel
      left - left position in pixel

    Returns:
      this
  */
  setPosition: function(top, left) {
    if (this.shadowSize) {
      var shadowStyle = this.shadow.style;
      top =  parseInt(top)  - this.shadowSize.top  + this.shadowShift.top;
      left = parseInt(left) - this.shadowSize.left + this.shadowShift.left;
      shadowStyle.top  = top + 'px';
      shadowStyle.left = left + 'px';
      if (this.iframe)
        this.iframe.setPosition(top, left);
    }
    return this;
  },

  /*
    Method: setSize
      Sets width/height shadow in pixels

    Parameters:
      width  - width in pixel
      height - height in pixel

    Returns:
      this
  */
  setSize: function(width, height) {
    if (this.shadowSize) {
      try {
        var w = Math.max(0, parseInt(width) + this.shadowSize.width - this.shadowShift.width) + "px";
        this.shadow.style.width = w;
        var h = Math.max(0, parseInt(height) - this.shadowShift.height) + "px";

        // this.shadowContents[1].style.height = h;
        this.shadowContents[1].childElements().each(function(e) {e.style.height = h});
        this.shadowContents.each(function(item){ item.style.width = w});
        if (this.iframe)
          this.iframe.setSize(width + this.shadowSize.width - this.shadowShift.width, height + this.shadowSize.height - this.shadowShift.height);
      }
      catch(e) {
        // IE could throw an exception if called to early
      }
    }
    return this;
  },

  /*
    Method: setBounds
      Sets shadow bounds in pixels

    Parameters:
      bounds - an Hash {top:, left:, width:, height:}

    Returns:
      this
  */
  setBounds: function(bounds) {
    return this.setPosition(bounds.top, bounds.left).setSize(bounds.width, bounds.height);
  },

  /*
    Method: setZIndex
      Sets shadow z-index

    Parameters:
      zIndex - zIndex value

    Returns:
      this
  */
  setZIndex: function(zIndex) {
    this.shadow.style.zIndex = zIndex;
    return this;
  },

   // Group: Render
  /*
    Method: show
      Displays shadow

    Returns:
      this
  */
  show: function() {
    this.render();
    this.shadow.show();
    if (this.iframe)
      this.iframe.show();
    return this;
  },

  /*
    Method: hide
      Hides shadow

    Returns:
      this
  */
  hide: function() {
    this.shadow.hide();
    if (this.iframe)
      this.iframe.hide();
    return this;
  },

  /*
    Method: remove
      Removes shadow from the DOM

    Returns:
      this
  */
  remove: function() {
    this.shadow.remove();
    return this;
  },

  // Group: Status
  /*
    Method: focus
      Focus shadow.

      Change shadow shift. Shift values are set in CSS file as margin and padding of shadow_container
      to add visual information of shadow status.

    Returns:
      this
  */
  focus: function() {
    this.options.focus = true;
    this.updateShadow();
    return this;
  },

  /*
    Method: blur
      Blurs shadow.

      Change shadow shift. Shift values are set in CSS file as margin and padding of shadow_container
      to add visual information of shadow status.

    Returns:
      this
  */
  blur: function() {
    this.options.focus = false;
    this.updateShadow();
    return this;
  },

  // Private Functions
  // Adds shadow elements to DOM, computes shadow size and displays it
  render: function() {
    if (this.element.parentNode && !Object.isElement(this.shadow.parentNode)) {
      this.element.parentNode.appendChild(this.shadow);
      this.computeSize();
      this.setBounds(Object.extend(this.element.getDimensions(), this.getElementPosition()));
      this.shadow.show();
    }
    return this;
  },

  // Creates HTML elements without inserting them into the DOM
  create: function() {
    var zIndex = this.element.getStyle('zIndex');
    if (!zIndex)
      this.element.setStyle({zIndex: this.options.zIndex});
    zIndex = (zIndex || this.options.zIndex) - 1;

    this.shadowContents = new Array(3);
    this.shadowContents[0] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper"}).insert(new Element("div", {className: "n_shadow"})))
      .insert(new Element("div", {className: "shadow_right ne_shadow"}))
      .insert(new Element("div", {className: "shadow_left nw_shadow"}));

    this.shadowContents[1] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper c_shadow"}))
      .insert(new Element("div", {className: "shadow_right e_shadow"}))
      .insert(new Element("div", {className: "shadow_left w_shadow"}));
    this.centerElements = this.shadowContents[1].childElements();

    this.shadowContents[2] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper"}).insert(new Element("div", {className: "s_shadow"})))
      .insert(new Element("div", {className: "shadow_right se_shadow"}))
      .insert(new Element("div", {className: "shadow_left sw_shadow"}));

    this.shadow = new Element("div", {className: "shadow_container " + this.options.theme,
                                      style: "position:absolute; top:-10000px; left:-10000px; display:none; z-index:" + zIndex })
      .insert(this.shadowContents[0])
      .insert(this.shadowContents[1])
      .insert(this.shadowContents[2]);
  },

  // Compute shadow size
  computeSize: function() {
    if (this.focusedShadowShift)
      return;
    this.shadow.show();

    // Trick to get shadow shift designed in CSS as padding
    var content = this.shadowContents[1].select("div.c_shadow").first();
    this.unfocusedShadowShift = {};
    this.focusedShadowShift = {};

    $w("top left bottom right").each(function(pos) {this.unfocusedShadowShift[pos] = content.getNumStyle("padding-" + pos) || 0}.bind(this));
    this.unfocusedShadowShift.width  = this.unfocusedShadowShift.left + this.unfocusedShadowShift.right;
    this.unfocusedShadowShift.height = this.unfocusedShadowShift.top + this.unfocusedShadowShift.bottom;

    $w("top left bottom right").each(function(pos) {this.focusedShadowShift[pos] = content.getNumStyle("margin-" + pos) || 0}.bind(this));
    this.focusedShadowShift.width  = this.focusedShadowShift.left + this.focusedShadowShift.right;
    this.focusedShadowShift.height = this.focusedShadowShift.top + this.focusedShadowShift.bottom;

    this.shadowShift = this.options.focus ? this.focusedShadowShift : this.unfocusedShadowShift;

    // Get shadow size
    this.shadowSize  = {top:    this.shadowContents[0].childElements()[1].getNumStyle("height"),
                        left:   this.shadowContents[0].childElements()[1].getNumStyle("width"),
                        bottom: this.shadowContents[2].childElements()[1].getNumStyle("height"),
                        right:  this.shadowContents[0].childElements()[2].getNumStyle("width")};

    this.shadowSize.width  = this.shadowSize.left + this.shadowSize.right;
    this.shadowSize.height = this.shadowSize.top + this.shadowSize.bottom;

    // Remove padding
    content.setStyle("padding:0; margin:0");
    this.shadow.hide();
  },

  // Update shadow size (called when it changes from focused to blur and vice-versa)
  updateShadow: function() {
    this.shadowShift = this.options.focus ? this.focusedShadowShift : this.unfocusedShadowShift;
    var shadowStyle = this.shadow.style, pos  = this.getElementPosition(), size = this.element.getDimensions();

    shadowStyle.top  =  pos.top    - this.shadowSize.top   + this.shadowShift.top   + 'px';
    shadowStyle.left  = pos.left   - this.shadowSize.left  + this.shadowShift.left  + 'px';
    shadowStyle.width = size.width + this.shadowSize.width - this.shadowShift.width + "px";
    var h = size.height - this.shadowShift.height + "px";
    this.centerElements.each(function(e) {e.style.height = h});

    var w = size.width + this.shadowSize.width - this.shadowShift.width+ "px";
    this.shadowContents.each(function(item) { item.style.width = w });
  },

  // Get element position in integer values
  getElementPosition: function() {
    return {top: this.element.getNumStyle("top"), left: this.element.getNumStyle("left")}
  }
});

// Set theme and focus as read/write accessor
//document.whenReady(function() { CSS.fixPNG("shadow") });

/*
  Credits:
  - Idea: Facebook + Apple Mail
  - Guillermo Rauch: Original MooTools script
  - InteRiders: Prototype version  <http://interiders.com/>
*/

Object.extend(Event, {
  KEY_SPACE: 32,
  KEY_COMMA:  188
});

UI.AutoComplete = Class.create(UI.Options, {
  // Group: Options
  options: {
    className: "pui-autocomplete",         // CSS class name prefix
    max: {selection: 10, selected:false},  // Max values fort autocomplete,
                                           // selection : max item in pulldown menu
                                           // selected  : max selected items (false = no limit)
    separator: ',',                        // separator character for hidden input
    url: false,                            // Url for ajax completion
    delay: 0.2,                            // Delay before running ajax request
    shadow: false,                         // Shadow theme name (false = no shadow)
    caseinsensitive: true,				   // Highlight search string in list caseinsensitive
    highlight: false,                      // Highlight search string in list
    tokens: false,                         // Tokens used to automatically adds a new entry (ex tokens:[KEY_SPACE, KEY_COMMA] for comma and spaces)
    returnType: 'text',                    // Return following type in input:
                                           // 'text'  : return text of selected items
                                           // 'value' : return value of selected items - not possible if token is active
    unique: true                           // Do not display in suggestion a selected value
  },

  initialize: function(element, options) {
    this.setOptions(options);
    if(typeof(this.options.tokens) == 'number')
      this.options.tokens = $A([this.options.tokens]);
    this.element = $(element);

    this.render();
    this.updateInputSize();
    this.nbSelected = 0;
    this.list = [];

    this.keydownHandler  = this.keydown.bindAsEventListener(this);
    document.observe('keydown', this.keydownHandler);
  },

  destroy:function() {
    this.autocompletion.destroy();
    this.input.stopObserving();
    document.stopObserving('keypress', this.keydownHandler);
    this.container.remove();
    this.element.show();
  },

  init: function(tokens) {
    tokens = tokens || this.options.tokens;
    var values = this.input.value.split(tokens.first());
    values.each(function(text) {if (!text.empty()) this.add(text)}.bind(this));
    this.input.clear();

    return this;
  },

  add: function(text, value, options) {
    // No more than max
    if (!this.canAddMoreItems())
      return;

    // Create a new li
    var li = new Element("li", Object.extend({className: this.getClassName("box")}, options || {}));
    li.observe("click",     this.focus.bindAsEventListener(this, li))
      .observe("mouseover", this.over.bindAsEventListener(this, li))
      .observe("mouseout",  this.out.bindAsEventListener(this, li));

    // Close button
    var close = new Element('a', {'href': '#', 'class': 'closebutton'});
    li.insert(new Element("span").update(text).insert(close));
    /*
    .insert(new Element('input', {
      type: 'hidden',
      name: this.element.name,
      value: value
    }));
    */
    if (value)
      li.writeAttribute("pui-autocomplete:value", value);

    close.observe("click", this.remove.bind(this, li));

    this.input.parentNode.insert({before: li});
    this.nbSelected++;
    this.updateSelectedText().updateHiddenField();

    this.updateInputSize();
    if (!this.canAddMoreItems())
      this.hideAutocomplete().fire("selection:max_reached");
    else
      this.hideAutocomplete().fire("input:empty");

    this.fire("element:added", {element: li, text: text, value: value});

    return this;
  },

  remove: function(element) {
    element.stopObserving();

    element.remove();
    this.nbSelected--;
    this.updateSelectedText().updateHiddenField();

    this.updateInputSize();
    this.input.focus();
    this.fire("element:removed", {element: element});

    return this;
  },

  removeLast: function() {
    var element = this.container.select("li." + this.getClassName("box")).last();
    if (element)
      this.remove(element);
  },

  removeSelected: function(event) {
    if (event.element().readAttribute("type") != "text" && event.keyCode == Event.KEY_BACKSPACE) {
      this.container.select("li." + this.getClassName("box")).each(function(element) {
        if (this.isSelected(element))
          this.remove(element);
      }.bind(this));
      if (event)
        event.stop();
    }
    return this;
  },

  focus: function(event, element) {
    if (event)
      event.stop();

    // Multi selection with shift
    if (event && !event.shiftKey)
      this.deselectAll();

    element = element || this.input;
    if (element == this.input && !this.input.readAttribute("focused")) {
      this.input.writeAttribute("focused", true);
      this.input.focus();
      this.displayCompletion();
    }
    else {
      this.out(event, element);
      element.addClassName(this.getClassName("selected"));

      // Blur input field
      if (element != this.input)
        this.blur();
    }
    return this.fire("element:focus", {element: element});
  },

  blur: function(event, element) {
    if (event)
      event.stop();

    if (!element)
      this.input.blur();

    this.hideAutocomplete();
    return this.fire("element:blur", {element: element});
  },

  over: function(event, element) {
    if (!this.isSelected(element))
      element.addClassName(this.getClassName("over"));
    if (event)
      event.stop();
    return this.fire("element:over", {element: element});
  },

  out: function(event, element) {
    if (!this.isSelected(element))
      element.removeClassName(this.getClassName("over"));
    if (event)
      event.stop();
    return this.fire("element:out", {element: element});
  },

  isSelected: function(element) {
    return element.hasClassName(this.getClassName("selected"));
  },

  deselectAll: function() {
   this.container.select("li." + this.getClassName("box")).invoke("removeClassName", this.getClassName("selected"));
   return this;
  },

  setAutocompleteList: function(list) {
    this.list = list;
    return this;
  },

  /*
    Method: fire
      Fires a autocomplete custom event automatically namespaced in "autocomplete:" (see Prototype custom events).
      The memo object contains a "autocomplete" property referring to the autocomplete.


    Parameters:
      eventName - an event name
      memo      - a memo object

    Returns:
      fired event
  */
  fire: function(eventName, memo) {
    memo = memo || { };
    memo.autocomplete = this;
    return this.input.fire('autocomplete:' + eventName, memo);
  },

  /*
    Method: observe
      Observe a autocomplete event with a handler function automatically bound to the autocomplete

    Parameters:
      eventName - an event name
      handler   - a handler function

    Returns:
      this
  */
  observe: function(eventName, handler) {
    this.input.observe('autocomplete:' + eventName, handler.bind(this));
    return this;
  },

  /*
    Method: stopObserving
      Unregisters a autocomplete event, it must take the same parameters as this.observe (see Prototype stopObserving).

    Parameters:
      eventName - an event name
      handler   - a handler function

    Returns:
      this
  */
  stopObserving: function(eventName, handler) {
	  this.input.stopObserving('autocomplete:' + eventName, handler);
	  return this;
  },

  // PRIVATE METHOD
  // Move selection. element = nil (highlight first),  "previous"/"next" or selected element
  moveSelection: function(event, element) {
    var current = null;
    // Seletc first
    if (!this.current)
      current = this.autocompletionContainer.firstDescendant();
    else if (element == "next") {
      current = this.current[element]() || this.autocompletionContainer.firstDescendant();
    }
    else if (element == "previous") {
      current = this.current[element]() || this.autocompletionContainer.childElements().last();
    }
    else
      current = element;

    if (this.current)
      this.current.removeClassName(this.getClassName("current"));

    this.current = current;

    if (this.current)
      this.current.addClassName(this.getClassName("current"));
  },

  // Add current selected element from completion to input
  addCurrentSelected: function() {
    if (this.current) {
      // Get selected text
      var index = this.autocompletionContainer.childElements().indexOf(this.current);
      // Clear input
      this.current = null;
      this.input.value = "";

      this.add(this.selectedList[index].text, this.selectedList[index].value);

      // Refocus input
      (function() {this.input.focus()}.bind(this)).defer();
      // Clear completion (force render)
      this.displayCompletion();
    }
  },

  // Display message (info or progress)
  showMessage: function(text) {
    if (text) {
      if (this.hideTimer) {
        clearTimeout(this.hideTimer);
        this.hideTimer = false;
      }
      // udate text
      this.message.update(text);
      this.message.show();
      // Hidden auto complete suggestion
      this.autocompletionContainer.hide();
      this.showAutocomplete();
    }
    else
      this.hideAutocomplete();
  },

  // Run ajax request to get completion values
  runRequest: function(search) {
    this.autocompletionContainer.hide();
    this.fire("request:started");

    new Ajax.Request(this.options.url, {parameters: {search: search, max: this.options.max.selection, "selected[]": this.selectedValues()}, onComplete: function(transport) {
      this.setAutocompleteList(transport.responseText.evalJSON());
      this.timer = null;
      this.fire("request:completed");
      this.displayCompletion();
    }.bind(this)});
  },

  // Get a "namespaced" class name
  getClassName: function(className) {
    return  this.options.className + "-" + className;
  },

  // Key down (for up/down and return key)
  keydown: function(event) {
    if (event.element() != this.input)
      return;

    this.ignoreKeyup = false;
    // Check max
    if (this.options.max.selected && this.nbSelected == this.options.max.selected)
      this.ignoreKeyup = true;

    // Check tokens
    if (this.options.tokens){
      var tokenFound = this.options.tokens.find(function(token){
        return event.keyCode == token;
      });
      if (tokenFound) {
        var value = this.input.value.strip();
        this.ignoreKeyup = true;
        var value = this.input.value;
        this.input.clear();
        if (!value.empty())
          this.add(value);
      }
    }
    switch(event.keyCode) {
     case Event.KEY_UP:
       this.moveSelection(event, 'previous');
       this.ignoreKeyup = true;
       break;
     case Event.KEY_DOWN:
       this.moveSelection(event, 'next');
       this.ignoreKeyup = true;
       break;
     case Event.KEY_RETURN:
       this.addCurrentSelected();
       this.ignoreKeyup = true;
       break;
     case Event.KEY_BACKSPACE:
       if (this.input.getCaretPosition() == 0)
         this.removeLast();
       break;
    }
    if (this.ignoreKeyup) {
      event.stop();
      return false;
    }
    else
      return true;
  },

  // Key to handle completion
  keyup: function(event) {
    if (this.ignoreKeyup) {
      this.ignoreKeyup = false;
      return true;
    }
    else {
      this.updateHiddenField();
      this.displayCompletion(event);
      return true;
    }
  },

  // Update input filed size to fit available width space
  updateInputSize: function() {
    // Get added elements width
    var top;
    var w = this.container.select("li." + this.getClassName("box")).inject(0, function(sum, element) {
      // First element
      if (Object.isUndefined(top))
        top = element.cumulativeOffset().top;
      // New line
      else if (top != element.cumulativeOffset().top) {
        top = element.cumulativeOffset().top;
        sum = 0;
      }
      return sum + element.getWidth() + element.getMarginDimensions().width + element.getBorderDimensions().width;
    });
    var margin = this.container.getMarginDimensions().width + this.container.getBorderDimensions().width + this.container.getPaddingDimensions().width;
    var width = this.container.getWidth() - w - margin;

    if (width < 50)
      width =   this.container.getWidth() - margin;

    this.input.parentNode.style.width = width + "px";
    this.input.style.width = width + "px";
  },

  // Display completion. It could display info or progress message if need be. Info when input field is empty
  // progress when ajax request is running
  displayCompletion: function(event) {
    var value = this.input.value.strip();
    this.current = null;
    if (!this.canAddMoreItems())
      return;

    if (!value.empty()) {
      // Run ajax reqest if need be
      if (event && this.options.url) {
        if (this.timer)
          clearTimeout(this.timer);
        this.timer = this.runRequest.bind(this, value).delay(this.options.delay);
      }
      else {
        this.message.hide();
        if (this.options.url)
          this.selectedList = this.list;
        else {
          this.selectedList = this.list.findAll(function(entry) {return entry.text.match(value)}).slice(0, this.options.max.selection);
          if (this.options.unique) {
            var selected= this.selectedValues();
            if (! selected.empty())
              this.selectedList = this.selectedList.findAll(function(entry) {return !selected.include(entry.value)});
          }
        }
        this.autocompletionContainer.update("");
        if (this.selectedList.empty()) {
          this.hideAutocomplete().fire('selection:empty');
        }
        else {
          this.selectedList.each(function(entry) {
        	var search = ( this.options.caseinsensitive ) ? new RegExp(value,"i") : value;
          	var li = new Element("li").update(this.options.highlight ? entry.text.gsub(search, function (match) { return "<em>" + match[0] + "</em>"; } ) : entry.text);
            li.observe("mouseover", this.moveSelection.bindAsEventListener(this, li))
              .observe("mousedown", this.addCurrentSelected.bindAsEventListener(this));
            this.autocompletionContainer.insert(li);
          }.bind(this));
          this.autocompletionContainer.show();
          this.moveSelection("next");
          this.showAutocomplete();
        }
      }
    }
    else {
      this.hideAutocomplete().fire("input:empty");
    }
  },

  showAutocomplete: function(event){
    this.autocompletion.show(event).place(this.container);
    return this;
  },

  hideAutocomplete: function(){
    if (!this.hideTimer)
      this.hideTimer = (function() {
        this.autocompletionContainer.hide();
        this.autocompletion.hide();
        this.hideTimer = false;
      }).bind(this).defer();
    return this;
  },

  // Create HTML code
  render: function() {
    // GENERATED HTML CODE:
    // <ul class="pui-autocomplete-holder">
    //   <li class="pui-autocomplete-input">
    //     <input type="text"/>
    //   </li>
    // </ul>
    // <div class="pui-autocomplete-result">
    //   <div class="pui-autocomplete-message"></div>
    //   <ul class="pui-autocomplete-results">
    //   </ul>
    // </div>
    //
    this.input = this.element.cloneNode(true);
    this.input.writeAttribute("autocomplete", "off");
    this.input.name = "";

    this.input.observe("focus",    this.focus.bindAsEventListener(this, this.input))
              .observe("blur",     this.blur.bindAsEventListener(this, this.input))
              .observe("keyup",    this.keyup.bindAsEventListener(this));
    this.container = new Element('ul', {className: this.getClassName("holder")})
                       .insert(new Element("li", {className: this.getClassName("input")}).insert(this.input));

    this.autocompletionContainer = new Element("ul",{className: this.getClassName("results")}).hide();

    this.message  = new Element("div", {className: this.getClassName("message")}).hide();
    this.hidden = new Element("input",{type: 'hidden', name: this.element.name});
    this.element.insert({before: this.container}).insert({before: this.hidden});
    this.element.remove();

    this.autocompletion = new UI.PullDown(this.container, {
      className: this.getClassName("result"),
      shadow: this.options.shadow,
      position: 'below',
      cloneWidth: true
    });

    this.autocompletion.insert(this.message).insert(this.autocompletionContainer);
  },

  canAddMoreItems: function() {
    return !(this.options.max.selected && this.nbSelected == this.options.max.selected);
  },

  updateSelectedText: function() {
    var selected = this.container.select("li." + this.getClassName("box"));
    //var content = selected.collect(function(element) {return element.down("span").firstChild.textContent});
    var content = selected.collect(function(element){
		var child = element.down("span").firstChild;
		return child.textContent || child.nodeValue;
	});
    var separator = this.getSeparatorChar();
    this.selectedText = content.empty() ? false : content.join(separator);

    return this;
  },

  updateHiddenField: function() {
    var separator = this.getSeparatorChar();

    if (this.options.returnType == 'value' && this.options.tokens == false) {
      content=this.selectedValues();
      this.hidden.value = content.empty() ? false : values=content.join(separator)
    }
    else
      this.hidden.value = this.selectedText ? $A([this.selectedText, this.input.value]).join(separator) : this.input.value;
  },

  selectedValues: function() {
    var selected = this.container.select("li." + this.getClassName("box"));
    return  selected.collect(function(element) {return element.readAttribute("pui-autocomplete:value")});
  },

  getSeparatorChar: function() {
    var separator = this.options.tokens ? this.options.tokens.first() : " ";
    if (separator == Event.KEY_COMMA)
      separator = ',';
    if (separator == Event.KEY_SPACE)
      separator = ' ';
    return separator;
  }
});

Element.addMethods({
  getCaretPosition: function(element) {
    if (element.createTextRange) {
      var r = document.selection.createRange().duplicate();
        r.moveEnd('character', element.value.length);
        if (r.text === '') return element.value.length;
        return element.value.lastIndexOf(r.text);
    } else return element.selectionStart;
  },

  getAttributeDimensions: function(element, attribut ) {
    var dim = $w('top bottom left right').inject({}, function(dims, key) {
      dims[key] = element.getNumStyle(attribut + "-" + key + (attribut == "border" ? "-width" : ""));
      return dims;
    });
    dim.width  = dim.left + dim.right;
    dim.height = dim.top + dim.bottom;
    return dim;
  },

  getBorderDimensions:  function(element) {return element.getAttributeDimensions("border")},
  getMarginDimensions:  function(element) {return element.getAttributeDimensions("margin")},
  getPaddingDimensions: function(element) {return element.getAttributeDimensions("padding")}
});
/*
  Class: UI.Carousel

  Main class to handle a carousel of elements in a page. A carousel :
    * could be vertical or horizontal
    * works with liquid layout
    * is designed by CSS

  Assumptions:
    * Elements should be from the same size

  Example:
    > ...
    > <div id="horizontal_carousel">
    >   <div class="previous_button"></div>
    >   <div class="container">
    >     <ul>
    >       <li> What ever you like</li>
    >     </ul>
    >   </div>
    >   <div class="next_button"></div>
    > </div>
    > <script>
    > new UI.Carousel("horizontal_carousel");
    > </script>
    > ...
*/
UI.Carousel = Class.create(UI.Options, {
  // Group: Options
  options: {
	// Property: direction
	//   Can be horizontal or vertical, horizontal by default
    direction               : "horizontal",

    // Property: previousButton
    //   Selector of previous button inside carousel element, ".previous_button" by default,
    //   set it to false to ignore previous button
    previousButton          : ".previous_button",

    // Property: nextButton
    //   Selector of next button inside carousel element, ".next_button" by default,
    //   set it to false to ignore next button
    nextButton              : ".next_button",

    // Property: container
    //   Selector of carousel container inside carousel element, ".container" by default,
    container               : ".container",

    // Property: scrollInc
    //   Define the maximum number of elements that gonna scroll each time, auto by default
    scrollInc               : "auto",

    // Property: disabledButtonSuffix
    //   Define the suffix classanme used when a button get disabled, to '_disabled' by default
    //   Previous button classname will be previous_button_disabled
    disabledButtonSuffix : '_disabled',

    // Property: overButtonSuffix
    //   Define the suffix classanme used when a button has a rollover status, '_over' by default
    //   Previous button classname will be previous_button_over
    overButtonSuffix : '_over'
  },

  /*
    Group: Attributes

      Property: element
        DOM element containing the carousel

      Property: id
        DOM id of the carousel's element

      Property: container
        DOM element containing the carousel's elements

      Property: elements
        Array containing the carousel's elements as DOM elements

      Property: previousButton
        DOM id of the previous button

      Property: nextButton
        DOM id of the next button

      Property: posAttribute
        Define if the positions are from left or top

      Property: dimAttribute
        Define if the dimensions are horizontal or vertical

      Property: elementSize
        Size of each element, it's an integer

      Property: nbVisible
        Number of visible elements, it's a float

      Property: animating
        Define whether the carousel is in animation or not
  */

  /*
    Group: Events
      List of events fired by a carousel

      Notice: Carousel custom events are automatically namespaced in "carousel:" (see Prototype custom events).

      Examples:
        This example will observe all carousels
        > document.observe('carousel:scroll:ended', function(event) {
        >   alert("Carousel with id " + event.memo.carousel.id + " has just been scrolled");
        > });

        This example will observe only this carousel
        > new UI.Carousel('horizontal_carousel').observe('scroll:ended', function(event) {
        >   alert("Carousel with id " + event.memo.carousel.id + " has just been scrolled");
        > });

      Property: previousButton:enabled
        Fired when the previous button has just been enabled

      Property: previousButton:disabled
        Fired when the previous button has just been disabled

      Property: nextButton:enabled
        Fired when the next button has just been enabled

      Property: nextButton:disabled
        Fired when the next button has just been disabled

      Property: scroll:started
        Fired when a scroll has just started

      Property: scroll:ended
        Fired when a scroll has been done,
        memo.shift = number of elements scrolled, it's a float

      Property: sizeUpdated
        Fired when the carousel size has just been updated.
        Tips: memo.carousel.currentSize() = the new carousel size
  */

  // Group: Constructor

  /*
    Method: initialize
      Constructor function, should not be called directly

    Parameters:
      element - DOM element
      options - (Hash) list of optional parameters

    Returns:
      this
  */
  initialize: function(element, options) {
    this.setOptions(options);
    this.element = $(element);
    this.id = this.element.id;
    this.container   = this.element.down(this.options.container).firstDescendant();
    this.elements    = this.container.childElements();
    this.previousButton = this.options.previousButton == false ? null : this.element.down(this.options.previousButton);
    this.nextButton = this.options.nextButton == false ? null : this.element.down(this.options.nextButton);

    this.posAttribute = (this.options.direction == "horizontal" ? "left" : "top");
    this.dimAttribute = (this.options.direction == "horizontal" ? "width" : "height");

    this.elementSize = this.computeElementSize();
    this.nbVisible = Math.ceil(this.currentSize() / this.elementSize);

    var scrollInc = this.options.scrollInc;
    if (scrollInc == "auto")
      scrollInc = Math.floor(this.nbVisible);
    [ this.previousButton, this.nextButton ].each(function(button) {
      if (!button) return;
      var className = (button == this.nextButton ? "next_button" : "previous_button") + this.options.overButtonSuffix;
      button.clickHandler = this.scroll.bind(this, (button == this.nextButton ? -1 : 1) * scrollInc * this.elementSize);
      button.observe("click", button.clickHandler)
            .observe("mouseover", function() {button.addClassName(className)}.bind(this))
            .observe("mouseout",  function() {button.removeClassName(className)}.bind(this));
    }, this);
    this.updateButtons();
  },

  // Group: Destructor

  /*
    Method: destroy
      Cleans up DOM and memory
  */
  destroy: function($super) {
    [ this.previousButton, this.nextButton ].each(function(button) {
      if (!button) return;
        button.stopObserving("click", button.clickHandler);
    }, this);
	  this.element.remove();
	  this.fire('destroyed');
  },

  // Group: Event handling

  /*
    Method: fire
      Fires a carousel custom event automatically namespaced in "carousel:" (see Prototype custom events).
      The memo object contains a "carousel" property referring to the carousel.

    Example:
      > document.observe('carousel:scroll:ended', function(event) {
      >   alert("Carousel with id " + event.memo.carousel.id + " has just been scrolled");
      > });

    Parameters:
      eventName - an event name
      memo      - a memo object

    Returns:
      fired event
  */
  fire: function(eventName, memo) {
    memo = memo || { };
    memo.carousel = this;
    return this.element.fire('carousel:' + eventName, memo);
  },

  /*
    Method: observe
      Observe a carousel event with a handler function automatically bound to the carousel

    Parameters:
      eventName - an event name
      handler   - a handler function

    Returns:
      this
  */
  observe: function(eventName, handler) {
    this.element.observe('carousel:' + eventName, handler.bind(this));
    return this;
  },

  /*
    Method: stopObserving
      Unregisters a carousel event, it must take the same parameters as this.observe (see Prototype stopObserving).

    Parameters:
      eventName - an event name
      handler   - a handler function

    Returns:
      this
  */
  stopObserving: function(eventName, handler) {
	  this.element.stopObserving('carousel:' + eventName, handler);
	  return this;
  },

  // Group: Actions

  /*
    Method: checkScroll
      Check scroll position to avoid unused space at right or bottom

    Parameters:
      position       - position to check
      updatePosition - should the container position be updated ? true/false

    Returns:
      position
  */
  checkScroll: function(position, updatePosition) {
    if (position > 0)
      position = 0;
    else {
      var limit = this.elements.last().positionedOffset()[this.posAttribute] + this.elementSize;
      var carouselSize = this.currentSize();

      if (position + limit < carouselSize)
        position += carouselSize - (position + limit);
      position = Math.min(position, 0);
    }
    if (updatePosition)
      this.container.style[this.posAttribute] = position + "px";

    return position;
  },

  /*
    Method: scroll
      Scrolls carousel from maximum deltaPixel

    Parameters:
      deltaPixel - a float

    Returns:
      this
  */
  scroll: function(deltaPixel) {
    if (this.animating)
      return this;

    // Compute new position
    var position =  this.currentPosition() + deltaPixel;

    // Check bounds
    position = this.checkScroll(position, false);

    // Compute shift to apply
    deltaPixel = position - this.currentPosition();
    if (deltaPixel != 0) {
      this.animating = true;
      this.fire("scroll:started");

      var that = this;
      // Move effects
      this.container.morph("opacity:0.5", {duration: 0.2, afterFinish: function() {
        that.container.morph(that.posAttribute + ": " + position + "px", {
          duration: 0.4,
          delay: 0.2,
          afterFinish: function() {
            that.container.morph("opacity:1", {
              duration: 0.2,
              afterFinish: function() {
                that.animating = false;
                that.updateButtons()
                  .fire("scroll:ended", { shift: deltaPixel / that.currentSize() });
              }
            });
          }
        });
      }});
    }
    return this;
  },

  /*
    Method: scrollTo
      Scrolls carousel, so that element with specified index is the left-most.
      This method is convenient when using carousel in a tabbed navigation.
      Clicking on first tab should scroll first container into view, clicking on a fifth - fifth one, etc.
      Indexing starts with 0.

    Parameters:
      Index of an element which will be a left-most visible in the carousel

    Returns:
      this
  */
  scrollTo: function(index) {
    if (this.animating || index < 0 || index > this.elements.length || index == this.currentIndex() || isNaN(parseInt(index)))
      return this;
    return this.scroll((this.currentIndex() - index) * this.elementSize);
  },

  /*
    Method: updateButtons
      Update buttons status to enabled or disabled
      Them status is defined by classNames and fired as carousel's custom events

    Returns:
      this
  */
  updateButtons: function() {
	  this.updatePreviousButton();
    this.updateNextButton();
    return this;
  },

  updatePreviousButton: function() {
    if (!this.previousButton)
      return;
    var position = this.currentPosition();
    var previousClassName = "previous_button" + this.options.disabledButtonSuffix;

    if (this.previousButton.hasClassName(previousClassName) && position != 0) {
      this.previousButton.removeClassName(previousClassName);
      this.fire('previousButton:enabled');
    }
    if (!this.previousButton.hasClassName(previousClassName) && position == 0) {
	    this.previousButton.addClassName(previousClassName);
      this.fire('previousButton:disabled');
    }
  },

  updateNextButton: function() {
    if (!this.nextButton)
      return;
    var lastPosition = this.currentLastPosition();
    var size = this.currentSize();
    var nextClassName = "next_button" + this.options.disabledButtonSuffix;

    if (this.nextButton.hasClassName(nextClassName) && lastPosition != size) {
      this.nextButton.removeClassName(nextClassName);
      this.fire('nextButton:enabled');
    }
    if (!this.nextButton.hasClassName(nextClassName) && lastPosition == size) {
	    this.nextButton.addClassName(nextClassName);
      this.fire('nextButton:disabled');
    }
  },

  // Group: Size and Position

  /*
    Method: computeElementSize
      Return elements size in pixel, height or width depends on carousel orientation.

    Returns:
      an integer value
  */
  computeElementSize: function() {
    return this.elements.first().getDimensions()[this.dimAttribute];
  },

  /*
    Method: currentIndex
      Returns current visible index of a carousel.
      For example, a horizontal carousel with image #3 on left will return 3 and with half of image #3 will return 3.5
      Don't forget that the first image have an index 0

    Returns:
      a float value
  */
  currentIndex: function() {
    return - this.currentPosition() / this.elementSize;
  },

  /*
    Method: currentLastPosition
      Returns the current position from the end of the last element. This value is in pixel.

    Returns:
      an integer value, if no images a present it will return 0
  */
  currentLastPosition: function() {
    if (this.container.childElements().empty())
      return 0;
    return this.currentPosition() +
           this.elements.last().positionedOffset()[this.posAttribute] +
           this.elementSize;
  },

  /*
    Method: currentPosition
      Returns the current position in pixel.
      Tips: To get the position in elements use currentIndex()

    Returns:
      an integer value
  */
  currentPosition: function() {
    return this.container.getNumStyle(this.posAttribute);
  },

  /*
    Method: currentSize
      Returns the current size of the carousel in pixel

    Returns:
      Carousel's size in pixel
  */
  currentSize: function() {
    return this.container.parentNode.getDimensions()[this.dimAttribute];
  },

  /*
    Method: updateSize
      Should be called if carousel size has been changed (usually called with a liquid layout)

    Returns:
      this
  */
  updateSize: function() {
    this.nbVisible = this.currentSize() / this.elementSize;
    var scrollInc = this.options.scrollInc;
    if (scrollInc == "auto")
      scrollInc = Math.floor(this.nbVisible);

    [ this.previousButton, this.nextButton ].each(function(button) {
      if (!button) return;
      button.stopObserving("click", button.clickHandler);
      button.clickHandler = this.scroll.bind(this, (button == this.nextButton ? -1 : 1) * scrollInc * this.elementSize);
      button.observe("click", button.clickHandler);
    }, this);

    this.checkScroll(this.currentPosition(), true);
    this.updateButtons().fire('sizeUpdated');
    return this;
  }
});
/*
  Class: UI.Ajax.Carousel

  Gives the AJAX power to carousels. An AJAX carousel :
    * Use AJAX to add new elements on the fly

  Example:
    > new UI.Ajax.Carousel("horizontal_carousel",
    >   {url: "get-more-elements", elementSize: 250});
*/
UI.Ajax.Carousel = Class.create(UI.Carousel, {
  // Group: Options
  //
  //   Notice:
  //     It also include of all carousel's options
  options: {
	// Property: elementSize
	//   Required, it define the size of all elements
    elementSize : -1,

	// Property: url
	//   Required, it define the URL used by AJAX carousel to request new elements details
    url         : null
  },

  /*
    Group: Attributes

      Notice:
        It also include of all carousel's attributes

      Property: elementSize
        Size of each elements, it's an integer

      Property: endIndex
        Index of the last loaded element

      Property: hasMore
        Flag to define if there's still more elements to load

      Property: requestRunning
        Define whether a request is processing or not

      Property: updateHandler
        Callback to update carousel, usually used after request success

      Property: url
        URL used to request additional elements
  */

  /*
    Group: Events
      List of events fired by an AJAX carousel, it also include of all carousel's custom events

      Property: request:started
        Fired when the request has just started

      Property: request:ended
        Fired when the request has succeed
  */

  // Group: Constructor

  /*
    Method: initialize
      Constructor function, should not be called directly

    Parameters:
      element - DOM element
      options - (Hash) list of optional parameters

    Returns:
      this
  */
  initialize: function($super, element, options) {
    if (!options.url)
      throw("url option is required for UI.Ajax.Carousel");
    if (!options.elementSize)
      throw("elementSize option is required for UI.Ajax.Carousel");

    $super(element, options);

    this.endIndex = 0;
    this.hasMore  = true;

    // Cache handlers
    this.updateHandler = this.update.bind(this);
    this.updateAndScrollHandler = function(nbElements, transport, json) {
	    this.update(transport, json);
	    this.scroll(nbElements);
	  }.bind(this);

    // Run first ajax request to fill the carousel
    this.runRequest.bind(this).defer({parameters: {from: 0, to: Math.ceil(this.nbVisible) - 1}, onSuccess: this.updateHandler});
  },

  // Group: Actions

  /*
    Method: runRequest
      Request the new elements details

    Parameters:
      options - (Hash) list of optional parameters

    Returns:
      this
  */
  runRequest: function(options) {
    this.requestRunning = true;
    new Ajax.Request(this.options.url, Object.extend({method: "GET"}, options));
    this.fire("request:started");
    return this;
  },

  /*
    Method: scroll
      Scrolls carousel from maximum deltaPixel

    Parameters:
      deltaPixel - a float

    Returns:
      this
  */
  scroll: function($super, deltaPixel) {
    if (this.animating || this.requestRunning)
      return this;

    var nbElements = (-deltaPixel) / this.elementSize;
    // Check if there is not enough
    if (this.hasMore && nbElements > 0 && this.currentIndex() + this.nbVisible + nbElements - 1 > this.endIndex) {
      var from = this.endIndex + 1;
      var to   = Math.ceil(from + this.nbVisible - 1);
      this.runRequest({parameters: {from: from, to: to}, onSuccess: this.updateAndScrollHandler.curry(deltaPixel).bind(this)});
      return this;
    }
    else
      $super(deltaPixel);
  },

  /*
    Method: update
      Update the carousel

    Parameters:
      transport - XMLHttpRequest object
      json      - JSON object

    Returns:
      this
  */
  update: function(transport, json) {
    this.requestRunning = false;
    this.fire("request:ended");
    if (!json)
      json = transport.responseJSON;
    this.hasMore = json.more;

    this.endIndex = Math.max(this.endIndex, json.to);
    this.elements = this.container.insert({bottom: json.html}).childElements();
    return this.updateButtons();
  },

  // Group: Size and Position

  /*
    Method: computeElementSize
      Return elements size in pixel

    Returns:
      an integer value
  */
  computeElementSize: function() {
    return this.options.elementSize;
  },

  /*
    Method: updateSize
      Should be called if carousel size has been changed (usually called with a liquid layout)

    Returns:
      this
  */
  updateSize: function($super) {
    var nbVisible = this.nbVisible;
    $super();
    // If we have enough space for at least a new element
    if (Math.floor(this.nbVisible) - Math.floor(nbVisible) >= 1 && this.hasMore) {
      if (this.currentIndex() + Math.floor(this.nbVisible) >= this.endIndex) {
        var nbNew = Math.floor(this.currentIndex() + Math.floor(this.nbVisible) - this.endIndex);
        this.runRequest({parameters: {from: this.endIndex + 1, to: this.endIndex + nbNew}, onSuccess: this.updateHandler});
      }
    }
    return this;
  },

  updateNextButton: function($super) {
    if (!this.nextButton)
      return;
    var lastPosition = this.currentLastPosition();
    var size = this.currentSize();
    var nextClassName = "next_button" + this.options.disabledButtonSuffix;

    if (this.nextButton.hasClassName(nextClassName) && lastPosition != size) {
      this.nextButton.removeClassName(nextClassName);
      this.fire('nextButton:enabled');
    }
    if (!this.nextButton.hasClassName(nextClassName) && lastPosition == size && !this.hasMore) {
	    this.nextButton.addClassName(nextClassName);
      this.fire('nextButton:disabled');
    }
  }
});

